Skip to content

Commit 26af6c0

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 26af6c0

File tree

9 files changed

+781
-117
lines changed

9 files changed

+781
-117
lines changed

lua/orgmode/files/file.lua

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,18 +277,27 @@ 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+
if type(todo_directives) ~= 'table' then
288+
todo_directives = { todo_directives }
289+
end
290+
291+
local keywords_data = {}
292+
for _, directive in ipairs(todo_directives) do
293+
local keywords = vim.split(vim.trim(directive), '%s+')
294+
table.insert(keywords_data, keywords)
295+
end
296+
297+
return require('orgmode.objects.todo_keywords'):new({
298+
org_todo_keywords = keywords_data,
288299
org_todo_keyword_faces = config.org_todo_keyword_faces,
289300
})
290-
291-
return todo_keywords
292301
end
293302

294303
---@return OrgHeadline[]
@@ -849,7 +858,7 @@ end
849858

850859
memoize('get_directive')
851860
---@param directive_name string
852-
---@return string | nil
861+
---@return string[] | string | nil
853862
function OrgFile:get_directive(directive_name)
854863
return self:_get_directive(directive_name)
855864
end
@@ -867,8 +876,10 @@ function OrgFile:id_get_or_create()
867876
end
868877

869878
---@private
870-
---@return string | nil
871-
function OrgFile:_get_directive(directive_name)
879+
---@param directive_name string
880+
---@param all_matches? boolean If true, returns an array of all matching directive values
881+
---@return string[] | string | nil
882+
function OrgFile:_get_directive(directive_name, all_matches)
872883
self:parse(true)
873884
local directives_body = self.root:field('body')[1]
874885
if not directives_body then
@@ -879,6 +890,22 @@ function OrgFile:_get_directive(directive_name)
879890
return nil
880891
end
881892

893+
if all_matches then
894+
local results = {}
895+
for _, directive in ipairs(directives) do
896+
local name = directive:field('name')[1]
897+
local value = directive:field('value')[1]
898+
899+
if name and value then
900+
local name_text = self:get_node_text(name)
901+
if name_text:lower() == directive_name:lower() then
902+
table.insert(results, self:get_node_text(value))
903+
end
904+
end
905+
end
906+
return #results > 0 and results or nil
907+
end
908+
882909
for _, directive in ipairs(directives) do
883910
local name = directive:field('name')[1]
884911
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: 92 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,75 @@ 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+
for _, keyword in ipairs(keywords) do
118+
table.insert(self.todo_keywords, keyword)
119+
end
120+
table.insert(self.sequences, seq_keywords)
121+
end
122+
end
123+
124+
---@private
125+
---@param keyword string
126+
---@param status_type string 'TODO' or 'DONE'
127+
---@param index number
128+
---@param seq_idx number
129+
---@param used_shortcuts table<string, boolean>
130+
---@return OrgTodoKeyword
131+
function TodoKeywords:_create_keyword(keyword, status_type, index, seq_idx, used_shortcuts)
132+
local todo_keyword = TodoKeyword:new({
133+
type = status_type,
134+
keyword = keyword,
135+
index = index,
136+
sequence_index = seq_idx,
137+
})
138+
139+
-- Track used shortcuts to avoid conflicts
140+
if todo_keyword.has_fast_access then
141+
used_shortcuts[todo_keyword.shortcut] = true
142+
elseif not used_shortcuts[todo_keyword.shortcut] and #self.org_todo_keywords > 1 then
143+
-- Auto-assign shortcuts when we have multiple sequences
144+
todo_keyword.has_fast_access = true
145+
used_shortcuts[todo_keyword.shortcut] = true
146+
end
147+
148+
todo_keyword.hl = self:_get_hl(todo_keyword.value, status_type)
149+
return todo_keyword
150+
end
151+
152+
---@private
153+
---@param keywords string[]
154+
---@param seq_idx number
155+
---@param used_shortcuts table<string, boolean>
156+
---@param keyword_offset number
157+
---@return OrgTodoKeyword[] keywords for the sequence
158+
---@return OrgTodoKeyword[] seq_keywords keywords in this sequence
159+
function TodoKeywords:_parse_sequence(keywords, seq_idx, used_shortcuts, keyword_offset)
160+
keyword_offset = keyword_offset or 0
161+
local todo, done = self:_split_todo_and_done(keywords)
83162
local list = {}
163+
local seq_keywords = {}
164+
84165
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')
166+
local todo_keyword = self:_create_keyword(keyword, 'TODO', keyword_offset + i, seq_idx, used_shortcuts)
91167
table.insert(list, todo_keyword)
168+
table.insert(seq_keywords, todo_keyword)
92169
end
93170

94171
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')
172+
local todo_keyword = self:_create_keyword(keyword, 'DONE', keyword_offset + #todo + i, seq_idx, used_shortcuts)
101173
table.insert(list, todo_keyword)
174+
table.insert(seq_keywords, todo_keyword)
102175
end
103176

104-
self.todo_keywords = list
177+
return list, seq_keywords
105178
end
106179

107180
---@private
@@ -116,9 +189,9 @@ function TodoKeywords:_get_hl(keyword, type)
116189
end
117190

118191
---@private
192+
---@param keywords string[]
119193
---@return string[], string[]
120-
function TodoKeywords:_split_todo_and_done()
121-
local keywords = self.org_todo_keywords
194+
function TodoKeywords:_split_todo_and_done(keywords)
122195
local has_separator = vim.tbl_contains(keywords, '|')
123196
if not has_separator then
124197
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)