Skip to content

Commit 0b75b39

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 2b91d9a commit 0b75b39

File tree

9 files changed

+794
-116
lines changed

9 files changed

+794
-116
lines changed

lua/orgmode/files/file.lua

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,18 +277,28 @@ end
277277

278278
memoize('get_todo_keywords')
279279
function OrgFile:get_todo_keywords()
280-
local todo_directive = self:_get_directive('todo')
281-
if not todo_directive then
280+
local todo_directives = self:_get_directive('todo', true)
281+
282+
-- Fall back to config if no TODO directives were found
283+
if not todo_directives then
282284
return config:get_todo_keywords()
283285
end
284286

285-
local keywords = vim.split(vim.trim(todo_directive), '%s+')
286-
local todo_keywords = require('orgmode.objects.todo_keywords'):new({
287-
org_todo_keywords = keywords,
287+
-- Only one TODO directive defined in file
288+
if type(todo_directives) ~= 'table' then
289+
todo_directives = { todo_directives }
290+
end
291+
292+
local keywords_data = {}
293+
for _, directive in ipairs(todo_directives) do
294+
local keywords = vim.split(vim.trim(directive), '%s+')
295+
table.insert(keywords_data, keywords)
296+
end
297+
298+
return require('orgmode.objects.todo_keywords'):new({
299+
org_todo_keywords = keywords_data,
288300
org_todo_keyword_faces = config.org_todo_keyword_faces,
289301
})
290-
291-
return todo_keywords
292302
end
293303

294304
---@return OrgHeadline[]
@@ -849,7 +859,7 @@ end
849859

850860
memoize('get_directive')
851861
---@param directive_name string
852-
---@return string | nil
862+
---@return string[] | string | nil
853863
function OrgFile:get_directive(directive_name)
854864
return self:_get_directive(directive_name)
855865
end
@@ -867,8 +877,10 @@ function OrgFile:id_get_or_create()
867877
end
868878

869879
---@private
870-
---@return string | nil
871-
function OrgFile:_get_directive(directive_name)
880+
---@param directive_name string
881+
---@param all_matches? boolean If true, returns an array of all matching directive values
882+
---@return string[] | string | nil
883+
function OrgFile:_get_directive(directive_name, all_matches)
872884
self:parse(true)
873885
local directives_body = self.root:field('body')[1]
874886
if not directives_body then
@@ -879,6 +891,22 @@ function OrgFile:_get_directive(directive_name)
879891
return nil
880892
end
881893

894+
if all_matches then
895+
local results = {}
896+
for _, directive in ipairs(directives) do
897+
local name = directive:field('name')[1]
898+
local value = directive:field('value')[1]
899+
900+
if name and value then
901+
local name_text = self:get_node_text(name)
902+
if name_text:lower() == directive_name:lower() then
903+
table.insert(results, self:get_node_text(value))
904+
end
905+
end
906+
end
907+
return #results > 0 and results or nil
908+
end
909+
882910
for _, directive in ipairs(directives) do
883911
local name = directive:field('name')[1]
884912
local value = directive:field('value')[1]

lua/orgmode/files/headline.lua

Lines changed: 14 additions & 0 deletions
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

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@ 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)
15+
-- Normalize input to always be sequences (string[][])
16+
local normalized_keywords = opts.org_todo_keywords
17+
if type(normalized_keywords[1]) ~= 'table' then
18+
normalized_keywords = { normalized_keywords }
19+
end
20+
1421
local this = setmetatable({
15-
org_todo_keywords = opts.org_todo_keywords,
22+
org_todo_keywords = normalized_keywords,
1623
org_todo_keyword_faces = opts.org_todo_keyword_faces,
24+
sequences = {},
1725
}, self)
1826
this:_parse()
1927
return this
@@ -44,6 +52,19 @@ function TodoKeywords:find(keyword)
4452
end)
4553
end
4654

55+
---@param keyword string
56+
---@return number | nil sequence index this keyword belongs to
57+
function TodoKeywords:find_sequence_index(keyword)
58+
for seq_idx, seq in ipairs(self.sequences) do
59+
for _, todo_keyword in ipairs(seq) do
60+
if todo_keyword.value == keyword then
61+
return seq_idx
62+
end
63+
end
64+
end
65+
return nil
66+
end
67+
4768
---@param type OrgTodoKeywordType
4869
---@return OrgTodoKeyword
4970
function TodoKeywords:first_by_type(type)
@@ -60,6 +81,12 @@ function TodoKeywords:all()
6081
return self.todo_keywords
6182
end
6283

84+
---@param sequence_idx? number
85+
---@return OrgTodoKeyword[]
86+
function TodoKeywords:sequence(sequence_idx)
87+
return self.sequences[sequence_idx or 1] or {}
88+
end
89+
6390
---@return OrgTodoKeyword
6491
function TodoKeywords:first()
6592
return self.todo_keywords[1]
@@ -79,29 +106,78 @@ end
79106

80107
---@private
81108
function TodoKeywords:_parse()
82-
local todo, done = self:_split_todo_and_done()
109+
self.todo_keywords = {}
110+
self.sequences = {}
111+
local used_shortcuts = {}
112+
113+
for seq_idx, sequence in ipairs(self.org_todo_keywords) do
114+
local keyword_offset = #self.todo_keywords
115+
local keywords, seq_keywords = self:_parse_sequence(sequence, seq_idx, used_shortcuts, keyword_offset)
116+
117+
-- Add keywords to the main list and the sequence
118+
for _, keyword in ipairs(keywords) do
119+
table.insert(self.todo_keywords, keyword)
120+
end
121+
table.insert(self.sequences, seq_keywords)
122+
end
123+
end
124+
125+
---@private
126+
---@param keyword string
127+
---@param status_type string 'TODO' or 'DONE'
128+
---@param index number
129+
---@param seq_idx number
130+
---@param used_shortcuts table<string, boolean>
131+
---@return OrgTodoKeyword
132+
function TodoKeywords:_create_keyword(keyword, status_type, index, seq_idx, used_shortcuts)
133+
local todo_keyword = TodoKeyword:new({
134+
type = status_type,
135+
keyword = keyword,
136+
index = index,
137+
sequence_index = seq_idx,
138+
})
139+
140+
-- Track used shortcuts to avoid conflicts
141+
if todo_keyword.has_fast_access then
142+
used_shortcuts[todo_keyword.shortcut] = true
143+
elseif not used_shortcuts[todo_keyword.shortcut] and #self.org_todo_keywords > 1 then
144+
-- Auto-assign shortcuts when we have multiple sequences
145+
todo_keyword.has_fast_access = true
146+
used_shortcuts[todo_keyword.shortcut] = true
147+
end
148+
149+
todo_keyword.hl = self:_get_hl(todo_keyword.value, status_type)
150+
return todo_keyword
151+
end
152+
153+
---@private
154+
---@param keywords string[]
155+
---@param seq_idx number
156+
---@param used_shortcuts table<string, boolean>
157+
---@param keyword_offset number
158+
---@return OrgTodoKeyword[] keywords for the sequence
159+
---@return OrgTodoKeyword[] seq_keywords keywords in this sequence
160+
function TodoKeywords:_parse_sequence(keywords, seq_idx, used_shortcuts, keyword_offset)
161+
keyword_offset = keyword_offset or 0
162+
local todo, done = self:_split_todo_and_done(keywords)
83163
local list = {}
164+
local seq_keywords = {}
165+
166+
-- Process TODO keywords
84167
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')
168+
local todo_keyword = self:_create_keyword(keyword, 'TODO', keyword_offset + i, seq_idx, used_shortcuts)
91169
table.insert(list, todo_keyword)
170+
table.insert(seq_keywords, todo_keyword)
92171
end
93172

173+
-- Process DONE keywords
94174
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')
175+
local todo_keyword = self:_create_keyword(keyword, 'DONE', keyword_offset + #todo + i, seq_idx, used_shortcuts)
101176
table.insert(list, todo_keyword)
177+
table.insert(seq_keywords, todo_keyword)
102178
end
103179

104-
self.todo_keywords = list
180+
return list, seq_keywords
105181
end
106182

107183
---@private
@@ -116,9 +192,9 @@ function TodoKeywords:_get_hl(keyword, type)
116192
end
117193

118194
---@private
195+
---@param keywords string[]
119196
---@return string[], string[]
120-
function TodoKeywords:_split_todo_and_done()
121-
local keywords = self.org_todo_keywords
197+
function TodoKeywords:_split_todo_and_done(keywords)
122198
local has_separator = vim.tbl_contains(keywords, '|')
123199
if not has_separator then
124200
return { unpack(keywords, 1, #keywords - 1) }, { keywords[#keywords] }

lua/orgmode/objects/todo_keywords/todo_keyword.lua

Lines changed: 4 additions & 1 deletion
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)