Skip to content

Commit 689f20a

Browse files
seflueSebastian Flüggekristijanhusak
authored
feat(todos): support file-specific todo definitions (#956)
* feat(todos): support file-specific todo definitions Support to overwrite the globally defined todo keywords with a file specific one. While emacs orgmode actually supports multiple todo keyword sequences, this implementation explicitly focuses on one sequence, because the plugin currently also supports only one sequence to be defined in the configuration. To support the full emacs functionality this needs to be extended later. * fix: meomize parsed todo keywords Co-authored-by: Kristijan Husak <[email protected]> * fix: trim spaces Co-authored-by: Kristijan Husak <[email protected]> * fix: support file specific todos in all todo-related commands * test: add permutation test for file specific todos --------- Co-authored-by: Sebastian Flügge <[email protected]> Co-authored-by: Kristijan Husak <[email protected]>
1 parent 2929054 commit 689f20a

File tree

6 files changed

+212
-7
lines changed

6 files changed

+212
-7
lines changed

lua/orgmode/files/file.lua

+16
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,22 @@ function OrgFile:find_headline_by_title(title)
269269
end)
270270
end
271271

272+
memoize('get_todo_keywords')
273+
function OrgFile:get_todo_keywords()
274+
local todo_directive = self:_get_directive('todo')
275+
if not todo_directive then
276+
return config:get_todo_keywords()
277+
end
278+
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,
282+
org_todo_keyword_faces = config.org_todo_keyword_faces,
283+
})
284+
285+
return todo_keywords
286+
end
287+
272288
---@return OrgHeadline[]
273289
function OrgFile:get_unfinished_todo_entries()
274290
if self:is_archive_file() then

lua/orgmode/files/headline.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ function Headline:get_todo()
379379
return nil, nil, nil
380380
end
381381

382-
local todo_keywords = config:get_todo_keywords()
382+
local todo_keywords = self.file:get_todo_keywords()
383383

384384
local text = self.file:get_node_text(todo_node)
385385
local keyword_by_value = todo_keywords:find(text)

lua/orgmode/objects/todo_state.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword')
77
---@field todos OrgTodoKeywords
88
local TodoState = {}
99

10-
---@param data { current_state: string | nil }
10+
---@param data { current_state: string | nil, todos: table | nil }
1111
---@return OrgTodoState
1212
function TodoState:new(data)
1313
local opts = {}
14-
opts.todos = config:get_todo_keywords()
14+
opts.todos = data.todos or config:get_todo_keywords()
1515
opts.current_state = data.current_state and opts.todos:find(data.current_state) or TodoKeyword:empty()
1616
setmetatable(opts, self)
1717
self.__index = self

lua/orgmode/org/mappings.lua

+7-4
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ function OrgMappings:toggle_heading()
379379
line = line:gsub('^(%s*)', '')
380380
if line:match('^[%*-]%s') then -- handle lists
381381
line = line:gsub('^[%*-]%s', '') -- strip bullet
382-
local todo_keywords = config:get_todo_keywords()
382+
local todo_keywords = self.files:get_current_file():get_todo_keywords()
383383
line = line:gsub('^%[([X%s])%]%s', function(checkbox_state)
384384
if checkbox_state == 'X' then
385385
return todo_keywords:first_by_type('DONE').value .. ' '
@@ -731,12 +731,14 @@ function OrgMappings:insert_heading_respect_content(suffix)
731731
end
732732

733733
function OrgMappings:insert_todo_heading_respect_content()
734-
return self:insert_heading_respect_content(config:get_todo_keywords():first_by_type('TODO').value .. ' ')
734+
local todo_keywords = self.files:get_current_file():get_todo_keywords()
735+
return self:insert_heading_respect_content(todo_keywords:first_by_type('TODO').value .. ' ')
735736
end
736737

737738
function OrgMappings:insert_todo_heading()
738739
local item = self.files:get_closest_headline_or_nil()
739-
local first_todo_keyword = config:get_todo_keywords():first_by_type('TODO')
740+
local todo_keywords = self.files:get_current_file():get_todo_keywords()
741+
local first_todo_keyword = todo_keywords:first_by_type('TODO')
740742
if not item then
741743
self:_insert_heading_from_plain_line(first_todo_keyword.value .. ' ')
742744
return vim.cmd([[startinsert!]])
@@ -1048,7 +1050,8 @@ end
10481050
function OrgMappings:_change_todo_state(direction, use_fast_access)
10491051
local headline = self.files:get_closest_headline()
10501052
local current_keyword = headline:get_todo()
1051-
local todo_state = TodoState:new({ current_state = current_keyword })
1053+
local todos = headline.file:get_todo_keywords()
1054+
local todo_state = TodoState:new({ current_state = current_keyword, todos = todos })
10521055
local next_state = nil
10531056
if use_fast_access and todo_state:has_fast_access() then
10541057
next_state = todo_state:open_fast_access()

tests/plenary/files/file_spec.lua

+48
Original file line numberDiff line numberDiff line change
@@ -830,4 +830,52 @@ describe('OrgFile', function()
830830
assert.are.same('somevalue', file:get_directive('somedirective'))
831831
end)
832832
end)
833+
834+
describe('get_todos', function()
835+
local has_correct_type = function(todos)
836+
assert.are.same('TODO', todos.todo_keywords[1].type)
837+
assert.are.same('TODO', todos.todo_keywords[2].type)
838+
assert.are.same('DONE', todos.todo_keywords[3].type)
839+
assert.are.same('DONE', todos.todo_keywords[4].type)
840+
end
841+
842+
local has_correct_values = function(todos)
843+
assert.are.same('OPEN', todos.todo_keywords[1].value)
844+
assert.are.same('DOING', todos.todo_keywords[2].value)
845+
assert.are.same('FINISHED', todos.todo_keywords[3].value)
846+
assert.are.same('ABORTED', todos.todo_keywords[4].value)
847+
end
848+
it('should get todo keywords from config by default', function()
849+
config:extend({
850+
org_todo_keywords = { 'TODO', 'DOING', '|', 'DONE', 'CANCELED' },
851+
})
852+
local file = load_file_sync({
853+
'* TODO Headline 1',
854+
})
855+
local todos = file:get_todo_keywords()
856+
assert.are.same({ 'TODO', 'DOING', '|', 'DONE', 'CANCELED' }, todos.org_todo_keywords)
857+
end)
858+
859+
it('should parse custom todo keywords from file directive', function()
860+
local file = load_file_sync({
861+
'#+TODO: OPEN DOING | FINISHED ABORTED',
862+
'* OPEN Headline 1',
863+
})
864+
local todos = file:get_todo_keywords()
865+
has_correct_type(todos)
866+
has_correct_values(todos)
867+
assert.are.same({ 'OPEN', 'DOING', '|', 'FINISHED', 'ABORTED' }, todos.org_todo_keywords)
868+
end)
869+
870+
it('should handle todo keywords with shortcut keys', function()
871+
local file = load_file_sync({
872+
'#+TODO: OPEN(o) DOING(d) | FINISHED(f) ABORTED(a)',
873+
'* OPEN Headline 1',
874+
})
875+
local todos = file:get_todo_keywords()
876+
has_correct_type(todos)
877+
has_correct_values(todos)
878+
assert.are.same({ 'OPEN(o)', 'DOING(d)', '|', 'FINISHED(f)', 'ABORTED(a)' }, todos.org_todo_keywords)
879+
end)
880+
end)
833881
end)

tests/plenary/ui/mappings/todo_spec.lua

+138
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,142 @@ describe('Todo mappings', function()
451451
'** Non-todo item',
452452
}, vim.api.nvim_buf_get_lines(0, 0, 6, false))
453453
end)
454+
455+
it('should respect file-local todo keywords', function()
456+
helpers.create_file({
457+
'#+TODO: OPEN DOING | FINISHED ABORTED',
458+
'* OPEN Test with file-local todo keywords',
459+
'** DOING Subtask',
460+
})
461+
462+
vim.fn.cursor(2, 1)
463+
vim.cmd([[norm cit]])
464+
assert.are.same({
465+
'#+TODO: OPEN DOING | FINISHED ABORTED',
466+
'* DOING Test with file-local todo keywords',
467+
'** DOING Subtask',
468+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
469+
470+
vim.cmd([[norm cit]])
471+
local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false)
472+
assert.are.same('#+TODO: OPEN DOING | FINISHED ABORTED', lines[1])
473+
assert.are.same('* FINISHED Test with file-local todo keywords', lines[2])
474+
assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil)
475+
assert.are.same('** DOING Subtask', lines[4])
476+
477+
vim.cmd([[norm cit]])
478+
lines = vim.api.nvim_buf_get_lines(0, 0, 4, false)
479+
assert.are.same('#+TODO: OPEN DOING | FINISHED ABORTED', lines[1])
480+
assert.are.same('* ABORTED Test with file-local todo keywords', lines[2])
481+
assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil)
482+
assert.are.same('** DOING Subtask', lines[4])
483+
484+
vim.cmd([[norm cit]])
485+
assert.are.same({
486+
'#+TODO: OPEN DOING | FINISHED ABORTED',
487+
'* Test with file-local todo keywords',
488+
'** DOING Subtask',
489+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
490+
491+
vim.cmd([[norm cit]])
492+
assert.are.same({
493+
'#+TODO: OPEN DOING | FINISHED ABORTED',
494+
'* OPEN Test with file-local todo keywords',
495+
'** DOING Subtask',
496+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
497+
end)
498+
it('should consider locally defined permutation of globally defined todo keywords', function()
499+
local local_todo_definition = '#+TODO: DONE OPEN | DOING'
500+
config:extend({
501+
org_todo_keywords = { 'OPEN', 'DOING', '|', 'DONE' },
502+
org_log_into_drawer = 'LOGBOOK',
503+
org_todo_repeat_to_state = 'MEET',
504+
})
505+
helpers.create_file({
506+
local_todo_definition,
507+
'* Test with file-local todo keywords',
508+
'** DOING Subtask',
509+
})
510+
511+
vim.fn.cursor(2, 1)
512+
vim.cmd([[norm cit]])
513+
assert.are.same({
514+
local_todo_definition,
515+
'* DONE Test with file-local todo keywords',
516+
'** DOING Subtask',
517+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
518+
519+
vim.cmd([[norm cit]])
520+
assert.are.same({
521+
local_todo_definition,
522+
'* OPEN Test with file-local todo keywords',
523+
'** DOING Subtask',
524+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
525+
526+
vim.cmd([[norm cit]])
527+
local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false)
528+
assert.are.same(local_todo_definition, lines[1])
529+
assert.are.same('* DOING Test with file-local todo keywords', lines[2])
530+
assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil)
531+
assert.are.same('** DOING Subtask', lines[4])
532+
533+
vim.cmd([[norm cit]])
534+
assert.are.same({
535+
local_todo_definition,
536+
'* Test with file-local todo keywords',
537+
'** DOING Subtask',
538+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
539+
540+
vim.cmd([[norm cit]])
541+
assert.are.same({
542+
local_todo_definition,
543+
'* DONE Test with file-local todo keywords',
544+
'** DOING Subtask',
545+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
546+
end)
547+
548+
local todos_with_shortcuts = '#+TODO: OPEN(o) DOING(d) | FINISHED(f) ABORTED(a)'
549+
it('should respect file-local todo keywords with shortcut keys', function()
550+
helpers.create_file({
551+
todos_with_shortcuts,
552+
'* OPEN Test with file-local todo keywords',
553+
'** DOING Subtask',
554+
})
555+
556+
vim.fn.cursor(2, 1)
557+
vim.cmd([[norm citd]])
558+
assert.are.same({
559+
todos_with_shortcuts,
560+
'* DOING Test with file-local todo keywords',
561+
'** DOING Subtask',
562+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
563+
564+
vim.cmd([[norm citf]])
565+
local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false)
566+
assert.are.same(todos_with_shortcuts, lines[1])
567+
assert.are.same('* FINISHED Test with file-local todo keywords', lines[2])
568+
assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil)
569+
assert.are.same('** DOING Subtask', lines[4])
570+
571+
vim.cmd([[norm cita]])
572+
lines = vim.api.nvim_buf_get_lines(0, 0, 4, false)
573+
assert.are.same(todos_with_shortcuts, lines[1])
574+
assert.are.same('* ABORTED Test with file-local todo keywords', lines[2])
575+
assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil)
576+
assert.are.same('** DOING Subtask', lines[4])
577+
578+
vim.cmd([[exe "norm cit\<Space>"]])
579+
assert.are.same({
580+
todos_with_shortcuts,
581+
'* Test with file-local todo keywords',
582+
'** DOING Subtask',
583+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
584+
585+
vim.cmd([[norm cito]])
586+
assert.are.same({
587+
todos_with_shortcuts,
588+
'* OPEN Test with file-local todo keywords',
589+
'** DOING Subtask',
590+
}, vim.api.nvim_buf_get_lines(0, 0, 3, false))
591+
end)
454592
end)

0 commit comments

Comments
 (0)