Skip to content

feat(todos): support file-specific todo definitions #956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lua/orgmode/files/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,21 @@ function OrgFile:find_headline_by_title(title)
end)
end

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(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
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/files/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lua/orgmode/objects/todo_state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lua/orgmode/org/mappings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,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()
Expand Down
48 changes: 48 additions & 0 deletions tests/plenary/files/file_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
88 changes: 88 additions & 0 deletions tests/plenary/ui/mappings/todo_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,92 @@ 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)
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\<Space>"]])
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)