Skip to content

feat: allow to define multiple todo keyword sequences #974

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 45 additions & 17 deletions lua/orgmode/files/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -271,18 +271,28 @@ end

memoize('get_todo_keywords')
function OrgFile:get_todo_keywords()
local todo_directive = self:_get_directive('todo')
if not todo_directive then
local todo_directives = self:_get_directive('todo', true)

-- Fall back to config if no TODO directives were found
if not todo_directives 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,
-- Only one TODO directive defined in file
if type(todo_directives) ~= 'table' then
todo_directives = { todo_directives }
end

local keywords_data = {}
for _, directive in ipairs(todo_directives) do
local keywords = vim.split(vim.trim(directive), '%s+')
table.insert(keywords_data, keywords)
end

return require('orgmode.objects.todo_keywords'):new({
org_todo_keywords = keywords_data,
org_todo_keyword_faces = config.org_todo_keyword_faces,
})

return todo_keywords
end

---@return OrgHeadline[]
Expand Down Expand Up @@ -843,7 +853,7 @@ end

memoize('get_directive')
---@param directive_name string
---@return string | nil
---@return string[] | string | nil
function OrgFile:get_directive(directive_name)
return self:_get_directive(directive_name)
end
Expand All @@ -861,8 +871,10 @@ function OrgFile:id_get_or_create()
end

---@private
---@return string | nil
function OrgFile:_get_directive(directive_name)
---@param directive_name string
---@param all_matches? boolean If true, returns an array of all matching directive values
---@return string[] | string | nil
function OrgFile:_get_directive(directive_name, all_matches)
self:parse(true)
local directives_body = self.root:field('body')[1]
if not directives_body then
Expand All @@ -873,14 +885,30 @@ function OrgFile:_get_directive(directive_name)
return nil
end

for _, directive in ipairs(directives) do
local name = directive:field('name')[1]
local value = directive:field('value')[1]
if all_matches then
local results = {}
for _, directive in ipairs(directives) do
local name = directive:field('name')[1]
local value = directive:field('value')[1]

if name and value then
local name_text = self:get_node_text(name)
if name_text:lower() == directive_name:lower() then
return self:get_node_text(value)
if name and value then
local name_text = self:get_node_text(name)
if name_text:lower() == directive_name:lower() then
table.insert(results, self:get_node_text(value))
end
end
end
return #results > 0 and results or nil
else
for _, directive in ipairs(directives) do
local name = directive:field('name')[1]
local value = directive:field('value')[1]

if name and value then
local name_text = self:get_node_text(name)
if name_text:lower() == directive_name:lower() then
return self:get_node_text(value)
end
end
end
end
Expand Down
14 changes: 14 additions & 0 deletions lua/orgmode/files/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1200,4 +1200,18 @@ function Headline:_handle_promote_demote(recursive, modifier, dryRun)
return self:refresh()
end

---@param drawer_name string
---@param content string
---@return OrgHeadline
function Headline:add_to_drawer(drawer_name, content)
local append_line = self:get_drawer_append_line(drawer_name)
local bufnr = self.file:get_valid_bufnr()

-- Add the content indented appropriately
local indented_content = self:_apply_indent(content) --[[ @as string ]]
vim.api.nvim_buf_set_lines(bufnr, append_line, append_line, false, { indented_content })

return self:refresh()
end

return Headline
133 changes: 115 additions & 18 deletions lua/orgmode/objects/todo_keywords/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ local utils = require('orgmode.utils')
local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword')

---@class OrgTodoKeywords
---@field org_todo_keywords string[]
---@field org_todo_keywords string[][]|string[]
---@field org_todo_keyword_faces table<string, string>
---@field todo_keywords OrgTodoKeyword[]
---@field sequences OrgTodoKeyword[][] Array of todo keyword sequences
local TodoKeywords = {}
TodoKeywords.__index = TodoKeywords

---@param opts { org_todo_keywords: string[], org_todo_keyword_faces: table<string, string> }
---@param opts { org_todo_keywords: string[][]|string[], org_todo_keyword_faces: table<string, string> }
---@return OrgTodoKeywords
function TodoKeywords:new(opts)
local this = setmetatable({
org_todo_keywords = opts.org_todo_keywords,
org_todo_keyword_faces = opts.org_todo_keyword_faces,
sequences = {},
}, self)
this:_parse()
return this
Expand Down Expand Up @@ -44,6 +46,19 @@ function TodoKeywords:find(keyword)
end)
end

---@param keyword string
---@return number | nil sequence index this keyword belongs to
function TodoKeywords:find_sequence_index(keyword)
for seq_idx, seq in ipairs(self.sequences) do
for _, todo_keyword in ipairs(seq) do
if todo_keyword.value == keyword then
return seq_idx
end
end
end
return nil
end

---@param type OrgTodoKeywordType
---@return OrgTodoKeyword
function TodoKeywords:first_by_type(type)
Expand All @@ -60,6 +75,12 @@ function TodoKeywords:all()
return self.todo_keywords
end

---@param sequence_idx? number
---@return OrgTodoKeyword[]
function TodoKeywords:sequence(sequence_idx)
return self.sequences[sequence_idx or 1] or {}
end

---@return OrgTodoKeyword
function TodoKeywords:first()
return self.todo_keywords[1]
Expand All @@ -79,29 +100,105 @@ end

---@private
function TodoKeywords:_parse()
local todo, done = self:_split_todo_and_done()
local first_sequence = self.org_todo_keywords[1]

if type(first_sequence) ~= 'table' then
self:_parse_single_sequence(self.org_todo_keywords)
return
end

if #self.org_todo_keywords == 1 then
self:_parse_single_sequence(first_sequence)
return
end

self:_parse_multiple_sequences(self.org_todo_keywords)
end

---@private
---@param keyword string
---@param status_type string 'TODO' or 'DONE'
---@param index number
---@param seq_idx number
---@param used_shortcuts table<string, boolean>
---@return OrgTodoKeyword
function TodoKeywords:_create_keyword(keyword, status_type, index, seq_idx, used_shortcuts)
local todo_keyword = TodoKeyword:new({
type = status_type,
keyword = keyword,
index = index,
sequence_index = seq_idx,
})

-- Track used shortcuts to avoid conflicts
if todo_keyword.has_fast_access then
used_shortcuts[todo_keyword.shortcut] = true
elseif not used_shortcuts[todo_keyword.shortcut] then
-- Mark it as a fast access key when we have multiple sequences
if type(self.org_todo_keywords[1]) == 'table' and #self.org_todo_keywords > 1 then
todo_keyword.has_fast_access = true
used_shortcuts[todo_keyword.shortcut] = true
end
end

todo_keyword.hl = self:_get_hl(todo_keyword.value, status_type)
return todo_keyword
end

---@private
---@param keywords string[]
---@param seq_idx number
---@param used_shortcuts table<string, boolean>
---@param keyword_offset number
---@return OrgTodoKeyword[] keywords for the sequence
---@return OrgTodoKeyword[] seq_keywords keywords in this sequence
function TodoKeywords:_parse_sequence(keywords, seq_idx, used_shortcuts, keyword_offset)
keyword_offset = keyword_offset or 0
local todo, done = self:_split_todo_and_done(keywords)
local list = {}
local seq_keywords = {}

-- Process TODO keywords
for i, keyword in ipairs(todo) do
local todo_keyword = TodoKeyword:new({
type = 'TODO',
keyword = keyword,
index = i,
})
todo_keyword.hl = self:_get_hl(todo_keyword.value, 'TODO')
local todo_keyword = self:_create_keyword(keyword, 'TODO', keyword_offset + i, seq_idx, used_shortcuts)
table.insert(list, todo_keyword)
table.insert(seq_keywords, todo_keyword)
end

-- Process DONE keywords
for i, keyword in ipairs(done) do
local todo_keyword = TodoKeyword:new({
type = 'DONE',
keyword = keyword,
index = #todo + i,
})
todo_keyword.hl = self:_get_hl(todo_keyword.value, 'DONE')
local todo_keyword = self:_create_keyword(keyword, 'DONE', keyword_offset + #todo + i, seq_idx, used_shortcuts)
table.insert(list, todo_keyword)
table.insert(seq_keywords, todo_keyword)
end

self.todo_keywords = list
return list, seq_keywords
end

---@param todo_keywords string[]
function TodoKeywords:_parse_single_sequence(todo_keywords)
local keywords, seq_keywords = self:_parse_sequence(todo_keywords, 1, {}, 0)
self.todo_keywords = keywords
self.sequences = { seq_keywords }
end

---@param todo_keywords string[][]
---@private
function TodoKeywords:_parse_multiple_sequences(todo_keywords)
self.todo_keywords = {}
self.sequences = {}
local used_shortcuts = {}

for seq_idx, sequence in ipairs(todo_keywords) do
local keyword_offset = #self.todo_keywords
local keywords, seq_keywords = self:_parse_sequence(sequence, seq_idx, used_shortcuts, keyword_offset)

-- Add keywords to the main list and the sequence
for _, keyword in ipairs(keywords) do
table.insert(self.todo_keywords, keyword)
end
table.insert(self.sequences, seq_keywords)
end
end

---@private
Expand All @@ -116,9 +213,9 @@ function TodoKeywords:_get_hl(keyword, type)
end

---@private
---@param keywords string[]
---@return string[], string[]
function TodoKeywords:_split_todo_and_done()
local keywords = self.org_todo_keywords
function TodoKeywords:_split_todo_and_done(keywords)
local has_separator = vim.tbl_contains(keywords, '|')
if not has_separator then
return { unpack(keywords, 1, #keywords - 1) }, { keywords[#keywords] }
Expand Down
5 changes: 4 additions & 1 deletion lua/orgmode/objects/todo_keywords/todo_keyword.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
---@field shortcut string
---@field hl string
---@field has_fast_access boolean
---@field sequence_index number The sequence this keyword belongs to
local TodoKeyword = {}
TodoKeyword.__index = TodoKeyword

---@param opts { type: OrgTodoKeywordType, keyword: string, index: number }
---@param opts { type: OrgTodoKeywordType, keyword: string, index: number, sequence_index?: number }
---@return OrgTodoKeyword
function TodoKeyword:new(opts)
local this = setmetatable({
keyword = opts.keyword,
type = opts.type,
index = opts.index,
has_fast_access = false,
sequence_index = opts.sequence_index or 1,
}, self)
this:parse()
return this
Expand All @@ -32,6 +34,7 @@ function TodoKeyword:empty()
index = 1,
has_fast_access = false,
hl = '',
sequence_index = 1,
}, self)
end

Expand Down
Loading
Loading