diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index 30046d3a6..98a01fc0e 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -269,6 +269,22 @@ function OrgFile:find_headline_by_title(title) end) end +memoize('get_todo_keywords') +function OrgFile:get_todo_keywords() + local todo_directive = self:_get_directive('todo') + if not todo_directive then + return config:get_todo_keywords() + end + + local keywords = vim.split(vim.trim(todo_directive), '%s+') + local todo_keywords = require('orgmode.objects.todo_keywords'):new({ + org_todo_keywords = keywords, + org_todo_keyword_faces = config.org_todo_keyword_faces, + }) + + return todo_keywords +end + ---@return OrgHeadline[] function OrgFile:get_unfinished_todo_entries() if self:is_archive_file() then diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index 45245f2cb..79ab092d7 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -379,7 +379,7 @@ function Headline:get_todo() return nil, nil, nil end - local todo_keywords = config:get_todo_keywords() + local todo_keywords = self.file:get_todo_keywords() local text = self.file:get_node_text(todo_node) local keyword_by_value = todo_keywords:find(text) diff --git a/lua/orgmode/objects/todo_state.lua b/lua/orgmode/objects/todo_state.lua index 7a7b6b086..41ef7e643 100644 --- a/lua/orgmode/objects/todo_state.lua +++ b/lua/orgmode/objects/todo_state.lua @@ -7,11 +7,11 @@ local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword') ---@field todos OrgTodoKeywords local TodoState = {} ----@param data { current_state: string | nil } +---@param data { current_state: string | nil, todos: table | nil } ---@return OrgTodoState function TodoState:new(data) local opts = {} - opts.todos = config:get_todo_keywords() + opts.todos = data.todos or config:get_todo_keywords() opts.current_state = data.current_state and opts.todos:find(data.current_state) or TodoKeyword:empty() setmetatable(opts, self) self.__index = self diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index df4fe0db1..3b35105ab 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -379,7 +379,7 @@ function OrgMappings:toggle_heading() line = line:gsub('^(%s*)', '') if line:match('^[%*-]%s') then -- handle lists line = line:gsub('^[%*-]%s', '') -- strip bullet - local todo_keywords = config:get_todo_keywords() + local todo_keywords = self.files:get_current_file():get_todo_keywords() line = line:gsub('^%[([X%s])%]%s', function(checkbox_state) if checkbox_state == 'X' then return todo_keywords:first_by_type('DONE').value .. ' ' @@ -731,12 +731,14 @@ function OrgMappings:insert_heading_respect_content(suffix) end function OrgMappings:insert_todo_heading_respect_content() - return self:insert_heading_respect_content(config:get_todo_keywords():first_by_type('TODO').value .. ' ') + local todo_keywords = self.files:get_current_file():get_todo_keywords() + return self:insert_heading_respect_content(todo_keywords:first_by_type('TODO').value .. ' ') end function OrgMappings:insert_todo_heading() local item = self.files:get_closest_headline_or_nil() - local first_todo_keyword = config:get_todo_keywords():first_by_type('TODO') + local todo_keywords = self.files:get_current_file():get_todo_keywords() + local first_todo_keyword = todo_keywords:first_by_type('TODO') if not item then self:_insert_heading_from_plain_line(first_todo_keyword.value .. ' ') return vim.cmd([[startinsert!]]) @@ -1048,7 +1050,8 @@ end function OrgMappings:_change_todo_state(direction, use_fast_access) local headline = self.files:get_closest_headline() local current_keyword = headline:get_todo() - local todo_state = TodoState:new({ current_state = current_keyword }) + local todos = headline.file:get_todo_keywords() + local todo_state = TodoState:new({ current_state = current_keyword, todos = todos }) local next_state = nil if use_fast_access and todo_state:has_fast_access() then next_state = todo_state:open_fast_access() diff --git a/tests/plenary/files/file_spec.lua b/tests/plenary/files/file_spec.lua index 61002edde..436eff168 100644 --- a/tests/plenary/files/file_spec.lua +++ b/tests/plenary/files/file_spec.lua @@ -830,4 +830,52 @@ describe('OrgFile', function() assert.are.same('somevalue', file:get_directive('somedirective')) end) end) + + describe('get_todos', function() + local has_correct_type = function(todos) + assert.are.same('TODO', todos.todo_keywords[1].type) + assert.are.same('TODO', todos.todo_keywords[2].type) + assert.are.same('DONE', todos.todo_keywords[3].type) + assert.are.same('DONE', todos.todo_keywords[4].type) + end + + local has_correct_values = function(todos) + assert.are.same('OPEN', todos.todo_keywords[1].value) + assert.are.same('DOING', todos.todo_keywords[2].value) + assert.are.same('FINISHED', todos.todo_keywords[3].value) + assert.are.same('ABORTED', todos.todo_keywords[4].value) + end + it('should get todo keywords from config by default', function() + config:extend({ + org_todo_keywords = { 'TODO', 'DOING', '|', 'DONE', 'CANCELED' }, + }) + local file = load_file_sync({ + '* TODO Headline 1', + }) + local todos = file:get_todo_keywords() + assert.are.same({ 'TODO', 'DOING', '|', 'DONE', 'CANCELED' }, todos.org_todo_keywords) + end) + + it('should parse custom todo keywords from file directive', function() + local file = load_file_sync({ + '#+TODO: OPEN DOING | FINISHED ABORTED', + '* OPEN Headline 1', + }) + local todos = file:get_todo_keywords() + has_correct_type(todos) + has_correct_values(todos) + assert.are.same({ 'OPEN', 'DOING', '|', 'FINISHED', 'ABORTED' }, todos.org_todo_keywords) + end) + + it('should handle todo keywords with shortcut keys', function() + local file = load_file_sync({ + '#+TODO: OPEN(o) DOING(d) | FINISHED(f) ABORTED(a)', + '* OPEN Headline 1', + }) + local todos = file:get_todo_keywords() + has_correct_type(todos) + has_correct_values(todos) + assert.are.same({ 'OPEN(o)', 'DOING(d)', '|', 'FINISHED(f)', 'ABORTED(a)' }, todos.org_todo_keywords) + end) + end) end) diff --git a/tests/plenary/ui/mappings/todo_spec.lua b/tests/plenary/ui/mappings/todo_spec.lua index 0e025d5c8..51450bca8 100644 --- a/tests/plenary/ui/mappings/todo_spec.lua +++ b/tests/plenary/ui/mappings/todo_spec.lua @@ -451,4 +451,142 @@ describe('Todo mappings', function() '** Non-todo item', }, vim.api.nvim_buf_get_lines(0, 0, 6, false)) end) + + it('should respect file-local todo keywords', function() + helpers.create_file({ + '#+TODO: OPEN DOING | FINISHED ABORTED', + '* OPEN Test with file-local todo keywords', + '** DOING Subtask', + }) + + vim.fn.cursor(2, 1) + vim.cmd([[norm cit]]) + assert.are.same({ + '#+TODO: OPEN DOING | FINISHED ABORTED', + '* DOING Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cit]]) + local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false) + assert.are.same('#+TODO: OPEN DOING | FINISHED ABORTED', lines[1]) + assert.are.same('* FINISHED Test with file-local todo keywords', lines[2]) + assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil) + assert.are.same('** DOING Subtask', lines[4]) + + vim.cmd([[norm cit]]) + lines = vim.api.nvim_buf_get_lines(0, 0, 4, false) + assert.are.same('#+TODO: OPEN DOING | FINISHED ABORTED', lines[1]) + assert.are.same('* ABORTED Test with file-local todo keywords', lines[2]) + assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil) + assert.are.same('** DOING Subtask', lines[4]) + + vim.cmd([[norm cit]]) + assert.are.same({ + '#+TODO: OPEN DOING | FINISHED ABORTED', + '* Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cit]]) + assert.are.same({ + '#+TODO: OPEN DOING | FINISHED ABORTED', + '* OPEN Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + end) + it('should consider locally defined permutation of globally defined todo keywords', function() + local local_todo_definition = '#+TODO: DONE OPEN | DOING' + config:extend({ + org_todo_keywords = { 'OPEN', 'DOING', '|', 'DONE' }, + org_log_into_drawer = 'LOGBOOK', + org_todo_repeat_to_state = 'MEET', + }) + helpers.create_file({ + local_todo_definition, + '* Test with file-local todo keywords', + '** DOING Subtask', + }) + + vim.fn.cursor(2, 1) + vim.cmd([[norm cit]]) + assert.are.same({ + local_todo_definition, + '* DONE Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cit]]) + assert.are.same({ + local_todo_definition, + '* OPEN Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cit]]) + local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false) + assert.are.same(local_todo_definition, lines[1]) + assert.are.same('* DOING Test with file-local todo keywords', lines[2]) + assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil) + assert.are.same('** DOING Subtask', lines[4]) + + vim.cmd([[norm cit]]) + assert.are.same({ + local_todo_definition, + '* Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cit]]) + assert.are.same({ + local_todo_definition, + '* DONE Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + end) + + local todos_with_shortcuts = '#+TODO: OPEN(o) DOING(d) | FINISHED(f) ABORTED(a)' + it('should respect file-local todo keywords with shortcut keys', function() + helpers.create_file({ + todos_with_shortcuts, + '* OPEN Test with file-local todo keywords', + '** DOING Subtask', + }) + + vim.fn.cursor(2, 1) + vim.cmd([[norm citd]]) + assert.are.same({ + todos_with_shortcuts, + '* DOING Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm citf]]) + local lines = vim.api.nvim_buf_get_lines(0, 0, 4, false) + assert.are.same(todos_with_shortcuts, lines[1]) + assert.are.same('* FINISHED Test with file-local todo keywords', lines[2]) + assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil) + assert.are.same('** DOING Subtask', lines[4]) + + vim.cmd([[norm cita]]) + lines = vim.api.nvim_buf_get_lines(0, 0, 4, false) + assert.are.same(todos_with_shortcuts, lines[1]) + assert.are.same('* ABORTED Test with file-local todo keywords', lines[2]) + assert.is_true(lines[3]:match('^%s+CLOSED: %[%d%d%d%d%-%d%d%-%d%d') ~= nil) + assert.are.same('** DOING Subtask', lines[4]) + + vim.cmd([[exe "norm cit\"]]) + assert.are.same({ + todos_with_shortcuts, + '* Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + + vim.cmd([[norm cito]]) + assert.are.same({ + todos_with_shortcuts, + '* OPEN Test with file-local todo keywords', + '** DOING Subtask', + }, vim.api.nvim_buf_get_lines(0, 0, 3, false)) + end) end)