Skip to content

Commit fc18036

Browse files
author
Sebastian Flügge
committed
feat: allow to define multiple todo keyword sequences
They can be defined in the config or within an org file.
1 parent 15d66ea commit fc18036

File tree

9 files changed

+850
-136
lines changed

9 files changed

+850
-136
lines changed

lua/orgmode/files/file.lua

+45-17
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,28 @@ end
271271

272272
memoize('get_todo_keywords')
273273
function OrgFile:get_todo_keywords()
274-
local todo_directive = self:_get_directive('todo')
275-
if not todo_directive then
274+
local todo_directives = self:_get_directive('todo', true)
275+
276+
-- Fall back to config if no TODO directives were found
277+
if not todo_directives then
276278
return config:get_todo_keywords()
277279
end
278280

279-
local keywords = vim.split(vim.trim(todo_directive), '%s+')
280-
local todo_keywords = require('orgmode.objects.todo_keywords'):new({
281-
org_todo_keywords = keywords,
281+
-- Only one TODO directive defined in file
282+
if type(todo_directives) ~= 'table' then
283+
todo_directives = { todo_directives }
284+
end
285+
286+
local keywords_data = {}
287+
for _, directive in ipairs(todo_directives) do
288+
local keywords = vim.split(vim.trim(directive), '%s+')
289+
table.insert(keywords_data, keywords)
290+
end
291+
292+
return require('orgmode.objects.todo_keywords'):new({
293+
org_todo_keywords = keywords_data,
282294
org_todo_keyword_faces = config.org_todo_keyword_faces,
283295
})
284-
285-
return todo_keywords
286296
end
287297

288298
---@return OrgHeadline[]
@@ -843,7 +853,7 @@ end
843853

844854
memoize('get_directive')
845855
---@param directive_name string
846-
---@return string | nil
856+
---@return string[] | string | nil
847857
function OrgFile:get_directive(directive_name)
848858
return self:_get_directive(directive_name)
849859
end
@@ -861,8 +871,10 @@ function OrgFile:id_get_or_create()
861871
end
862872

863873
---@private
864-
---@return string | nil
865-
function OrgFile:_get_directive(directive_name)
874+
---@param directive_name string
875+
---@param all_matches? boolean If true, returns an array of all matching directive values
876+
---@return string[] | string | nil
877+
function OrgFile:_get_directive(directive_name, all_matches)
866878
self:parse(true)
867879
local directives_body = self.root:field('body')[1]
868880
if not directives_body then
@@ -873,14 +885,30 @@ function OrgFile:_get_directive(directive_name)
873885
return nil
874886
end
875887

876-
for _, directive in ipairs(directives) do
877-
local name = directive:field('name')[1]
878-
local value = directive:field('value')[1]
888+
if all_matches then
889+
local results = {}
890+
for _, directive in ipairs(directives) do
891+
local name = directive:field('name')[1]
892+
local value = directive:field('value')[1]
879893

880-
if name and value then
881-
local name_text = self:get_node_text(name)
882-
if name_text:lower() == directive_name:lower() then
883-
return self:get_node_text(value)
894+
if name and value then
895+
local name_text = self:get_node_text(name)
896+
if name_text:lower() == directive_name:lower() then
897+
table.insert(results, self:get_node_text(value))
898+
end
899+
end
900+
end
901+
return #results > 0 and results or nil
902+
else
903+
for _, directive in ipairs(directives) do
904+
local name = directive:field('name')[1]
905+
local value = directive:field('value')[1]
906+
907+
if name and value then
908+
local name_text = self:get_node_text(name)
909+
if name_text:lower() == directive_name:lower() then
910+
return self:get_node_text(value)
911+
end
884912
end
885913
end
886914
end

lua/orgmode/files/headline.lua

+14
Original file line numberDiff line numberDiff line change
@@ -1200,4 +1200,18 @@ function Headline:_handle_promote_demote(recursive, modifier, dryRun)
12001200
return self:refresh()
12011201
end
12021202

1203+
---@param drawer_name string
1204+
---@param content string
1205+
---@return OrgHeadline
1206+
function Headline:add_to_drawer(drawer_name, content)
1207+
local append_line = self:get_drawer_append_line(drawer_name)
1208+
local bufnr = self.file:get_valid_bufnr()
1209+
1210+
-- Add the content indented appropriately
1211+
local indented_content = self:_apply_indent(content) --[[ @as string ]]
1212+
vim.api.nvim_buf_set_lines(bufnr, append_line, append_line, false, { indented_content })
1213+
1214+
return self:refresh()
1215+
end
1216+
12031217
return Headline

lua/orgmode/objects/todo_keywords/init.lua

+115-18
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ local utils = require('orgmode.utils')
22
local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword')
33

44
---@class OrgTodoKeywords
5-
---@field org_todo_keywords string[]
5+
---@field org_todo_keywords string[][]|string[]
66
---@field org_todo_keyword_faces table<string, string>
77
---@field todo_keywords OrgTodoKeyword[]
8+
---@field sequences OrgTodoKeyword[][] Array of todo keyword sequences
89
local TodoKeywords = {}
910
TodoKeywords.__index = TodoKeywords
1011

11-
---@param opts { org_todo_keywords: string[], org_todo_keyword_faces: table<string, string> }
12+
---@param opts { org_todo_keywords: string[][]|string[], org_todo_keyword_faces: table<string, string> }
1213
---@return OrgTodoKeywords
1314
function TodoKeywords:new(opts)
1415
local this = setmetatable({
1516
org_todo_keywords = opts.org_todo_keywords,
1617
org_todo_keyword_faces = opts.org_todo_keyword_faces,
18+
sequences = {},
1719
}, self)
1820
this:_parse()
1921
return this
@@ -44,6 +46,19 @@ function TodoKeywords:find(keyword)
4446
end)
4547
end
4648

49+
---@param keyword string
50+
---@return number | nil sequence index this keyword belongs to
51+
function TodoKeywords:find_sequence_index(keyword)
52+
for seq_idx, seq in ipairs(self.sequences) do
53+
for _, todo_keyword in ipairs(seq) do
54+
if todo_keyword.value == keyword then
55+
return seq_idx
56+
end
57+
end
58+
end
59+
return nil
60+
end
61+
4762
---@param type OrgTodoKeywordType
4863
---@return OrgTodoKeyword
4964
function TodoKeywords:first_by_type(type)
@@ -60,6 +75,12 @@ function TodoKeywords:all()
6075
return self.todo_keywords
6176
end
6277

78+
---@param sequence_idx? number
79+
---@return OrgTodoKeyword[]
80+
function TodoKeywords:sequence(sequence_idx)
81+
return self.sequences[sequence_idx or 1] or {}
82+
end
83+
6384
---@return OrgTodoKeyword
6485
function TodoKeywords:first()
6586
return self.todo_keywords[1]
@@ -79,29 +100,105 @@ end
79100

80101
---@private
81102
function TodoKeywords:_parse()
82-
local todo, done = self:_split_todo_and_done()
103+
local first_sequence = self.org_todo_keywords[1]
104+
105+
if type(first_sequence) ~= 'table' then
106+
self:_parse_single_sequence(self.org_todo_keywords)
107+
return
108+
end
109+
110+
if #self.org_todo_keywords == 1 then
111+
self:_parse_single_sequence(first_sequence)
112+
return
113+
end
114+
115+
self:_parse_multiple_sequences(self.org_todo_keywords)
116+
end
117+
118+
---@private
119+
---@param keyword string
120+
---@param status_type string 'TODO' or 'DONE'
121+
---@param index number
122+
---@param seq_idx number
123+
---@param used_shortcuts table<string, boolean>
124+
---@return OrgTodoKeyword
125+
function TodoKeywords:_create_keyword(keyword, status_type, index, seq_idx, used_shortcuts)
126+
local todo_keyword = TodoKeyword:new({
127+
type = status_type,
128+
keyword = keyword,
129+
index = index,
130+
sequence_index = seq_idx,
131+
})
132+
133+
-- Track used shortcuts to avoid conflicts
134+
if todo_keyword.has_fast_access then
135+
used_shortcuts[todo_keyword.shortcut] = true
136+
elseif not used_shortcuts[todo_keyword.shortcut] then
137+
-- Mark it as a fast access key when we have multiple sequences
138+
if type(self.org_todo_keywords[1]) == 'table' and #self.org_todo_keywords > 1 then
139+
todo_keyword.has_fast_access = true
140+
used_shortcuts[todo_keyword.shortcut] = true
141+
end
142+
end
143+
144+
todo_keyword.hl = self:_get_hl(todo_keyword.value, status_type)
145+
return todo_keyword
146+
end
147+
148+
---@private
149+
---@param keywords string[]
150+
---@param seq_idx number
151+
---@param used_shortcuts table<string, boolean>
152+
---@param keyword_offset number
153+
---@return OrgTodoKeyword[] keywords for the sequence
154+
---@return OrgTodoKeyword[] seq_keywords keywords in this sequence
155+
function TodoKeywords:_parse_sequence(keywords, seq_idx, used_shortcuts, keyword_offset)
156+
keyword_offset = keyword_offset or 0
157+
local todo, done = self:_split_todo_and_done(keywords)
83158
local list = {}
159+
local seq_keywords = {}
160+
161+
-- Process TODO keywords
84162
for i, keyword in ipairs(todo) do
85-
local todo_keyword = TodoKeyword:new({
86-
type = 'TODO',
87-
keyword = keyword,
88-
index = i,
89-
})
90-
todo_keyword.hl = self:_get_hl(todo_keyword.value, 'TODO')
163+
local todo_keyword = self:_create_keyword(keyword, 'TODO', keyword_offset + i, seq_idx, used_shortcuts)
91164
table.insert(list, todo_keyword)
165+
table.insert(seq_keywords, todo_keyword)
92166
end
93167

168+
-- Process DONE keywords
94169
for i, keyword in ipairs(done) do
95-
local todo_keyword = TodoKeyword:new({
96-
type = 'DONE',
97-
keyword = keyword,
98-
index = #todo + i,
99-
})
100-
todo_keyword.hl = self:_get_hl(todo_keyword.value, 'DONE')
170+
local todo_keyword = self:_create_keyword(keyword, 'DONE', keyword_offset + #todo + i, seq_idx, used_shortcuts)
101171
table.insert(list, todo_keyword)
172+
table.insert(seq_keywords, todo_keyword)
102173
end
103174

104-
self.todo_keywords = list
175+
return list, seq_keywords
176+
end
177+
178+
---@param todo_keywords string[]
179+
function TodoKeywords:_parse_single_sequence(todo_keywords)
180+
local keywords, seq_keywords = self:_parse_sequence(todo_keywords, 1, {}, 0)
181+
self.todo_keywords = keywords
182+
self.sequences = { seq_keywords }
183+
end
184+
185+
---@param todo_keywords string[][]
186+
---@private
187+
function TodoKeywords:_parse_multiple_sequences(todo_keywords)
188+
self.todo_keywords = {}
189+
self.sequences = {}
190+
local used_shortcuts = {}
191+
192+
for seq_idx, sequence in ipairs(todo_keywords) do
193+
local keyword_offset = #self.todo_keywords
194+
local keywords, seq_keywords = self:_parse_sequence(sequence, seq_idx, used_shortcuts, keyword_offset)
195+
196+
-- Add keywords to the main list and the sequence
197+
for _, keyword in ipairs(keywords) do
198+
table.insert(self.todo_keywords, keyword)
199+
end
200+
table.insert(self.sequences, seq_keywords)
201+
end
105202
end
106203

107204
---@private
@@ -116,9 +213,9 @@ function TodoKeywords:_get_hl(keyword, type)
116213
end
117214

118215
---@private
216+
---@param keywords string[]
119217
---@return string[], string[]
120-
function TodoKeywords:_split_todo_and_done()
121-
local keywords = self.org_todo_keywords
218+
function TodoKeywords:_split_todo_and_done(keywords)
122219
local has_separator = vim.tbl_contains(keywords, '|')
123220
if not has_separator then
124221
return { unpack(keywords, 1, #keywords - 1) }, { keywords[#keywords] }

lua/orgmode/objects/todo_keywords/todo_keyword.lua

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88
---@field shortcut string
99
---@field hl string
1010
---@field has_fast_access boolean
11+
---@field sequence_index number The sequence this keyword belongs to
1112
local TodoKeyword = {}
1213
TodoKeyword.__index = TodoKeyword
1314

14-
---@param opts { type: OrgTodoKeywordType, keyword: string, index: number }
15+
---@param opts { type: OrgTodoKeywordType, keyword: string, index: number, sequence_index?: number }
1516
---@return OrgTodoKeyword
1617
function TodoKeyword:new(opts)
1718
local this = setmetatable({
1819
keyword = opts.keyword,
1920
type = opts.type,
2021
index = opts.index,
2122
has_fast_access = false,
23+
sequence_index = opts.sequence_index or 1,
2224
}, self)
2325
this:parse()
2426
return this
@@ -32,6 +34,7 @@ function TodoKeyword:empty()
3234
index = 1,
3335
has_fast_access = false,
3436
hl = '',
37+
sequence_index = 1,
3538
}, self)
3639
end
3740

0 commit comments

Comments
 (0)