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

Conversation

seflue
Copy link
Contributor

@seflue seflue commented May 4, 2025

They can be defined in the config or within an org file.

Summary

This PR adds the ability to define multiple todo keyword sets as described in Orgmode manual.

Related Issues

Relates to #250, #157, PR #956

Closes #250

Changes

  • additionally to a single keyword sequence org_todo_keywords allows to define a table of keyword sets
  • an org file can have multiple #+TODO: directives
  • if multiple keyword sets are defined, either in the config or in the current org file, org_todo keymap triggers fast access mode to select a keyword
  • org_todo_prev behaves like S-RIGHT in Emacs Orgmode

Falling back to fast access mode when multiple sets are defined is a bit of a shortcut to get a first version of this feature out of the door. Emacs Orgmode defines some additional keybindings to switch between keyword sets. This is a bit more elaborated and could be implemented in a further PR.

Checklist

I confirm that I have:

  • Followed the
    Conventional Commits
    specification
    (e.g., feat: add new feature, fix: correct bug,
    docs: update documentation).
  • My PR title also follows the conventional commits specification.
  • Updated relevant documentation, if necessary.
  • Thoroughly tested my changes.
  • Added tests (if applicable) and verified existing tests pass with
    make test.
  • Checked for breaking changes and documented them, if any.

@seflue seflue force-pushed the feature/support-multiple-todo-sequences branch from fc18036 to 2d74d78 Compare May 4, 2025 21:49
@seflue
Copy link
Contributor Author

seflue commented May 4, 2025

@kristijanhusak It seems, that the indentation test is a bit flaky (those are the both failing test). It also failed occasionally locally on my machine, but that is unrelated to my changes.

Copy link
Member

@kristijanhusak kristijanhusak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left few comments around the code, some around code styling changes (mostly around avoiding else when there's early return), and some comments around the logic.

I think we should approach this in a slightly different , and potentially simpler way:

  1. Always store the todo keywords as sequences. You already did that here more-less, but there's no need have a separate logic like parse_single_sequence or parse_multiple_sequence. When TodoKeywords get's org_todo_keywords value, just check if it's a string[] or string[][]. If it's former, just convert it to string[][] and handle it accordingly.
  2. Instead of keeping the sequence index on the TodoKeyword, and sequences on the TodoKeywords, we can just search things on the fly. These things are used only when mutating the document, so it's not that necessary to do these optimizations. Lua is fast enough to handle this, and I doubt users have a lot of todo sequences.

Another approach could be to keep the TodoKeywords as they are, which is basically as single sequence, and have a layer above that will know to figure out which of the sequence we need at the given point, and just return it and use it where necessary.
This might complicate some other things so it might not be the best idea, but I wanted to point it out.

Let me know what you think.

Comment on lines +902 to 914
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can end the above if statement here since it has a return

Suggested change
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
end
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

Comment on lines +106 to +107
self:_parse_single_sequence(self.org_todo_keywords)
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just return directly here, method returns void anyway. Same for the if below.

Suggested change
self:_parse_single_sequence(self.org_todo_keywords)
return
return self:_parse_single_sequence(self.org_todo_keywords)

Comment on lines +42 to +45
if #self.todos.sequences > 1 or self.todos:has_fast_access() then
return true
end
return false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if #self.todos.sequences > 1 or self.todos:has_fast_access() then
return true
end
return false
return #self.todos.sequences > 1 or self.todos:has_fast_access()

return keyword
-- When we're starting from an empty state and moving backward,
-- go to the last todo keyword of the last sequence
else
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for else block when if block returns. It removes one level of indentation.

-- Find the keyword by string value
if type(current_state) == 'string' then
opts.current_state = opts.todos:find(current_state) or TodoKeyword:empty()
-- Direct assignment of a TodoKeyword
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we create a TodoState with the direct assignment? We just provide the todo keyword string in all usages.

---@param headline OrgHeadline
---@param old_state string
---@param new_state string
function OrgMappings:_handle_repeating_task(headline, old_state, new_state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this part of code changed?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having multiple sequences doesn't necessarily mean we use fast access. When there is no fast access defined in the config of the todo keywords, Emacs just cycles through the sequence it figured out that is being used.

There's a catch though. Emacs somehow keeps in memory which was the last sequence used. For example, if you have these keywords:

(setq org-todo-keywords
      '((sequence "TODO" "|" "DONE")
        (sequence "REPORT" "BUG" "TESTING" "|" "FIXED")))

And you have a headline with * REPORT foo, cycling without the fast access will go REPORT -> BUG -> TESTING -> FIXED -> {EMPTY} -> REPORT -> etc.. So it keeps in memory that the last sequence is 2nd one. Now, if you switch to an empty headline without todo keyword, restart emacs, and try switching the todo state, it will use the first sequence. So it's only in memory while the emacs is open.

I'm not sure what's the best way to access that issue, but I wanted to bring it up in case you have some ideas.

They can be defined in the config or within an org file.
@seflue seflue force-pushed the feature/support-multiple-todo-sequences branch from 2d74d78 to 68597a8 Compare May 25, 2025 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support multiple todo keyword sequence definitions
2 participants