Skip to content

Commit e49da78

Browse files
Add support for fast access to TODO states. Closes #63.
1 parent e4deea1 commit e49da78

File tree

7 files changed

+162
-48
lines changed

7 files changed

+162
-48
lines changed

DOCS.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,19 @@ Example: `~/Dropbox/org/notes.org`
4444
List of "unfinished" and "finished" states.<br />
4545
`|` is used as a separator between "unfinished" and "finished".<br />
4646
If `|` is omitted, only last entry in array is considered a "finished" state.<br />
47-
Examples:
47+
To use [Fast access to TODO States](https://orgmode.org/manual/Fast-access-to-TODO-states.html#Fast-access-to-TODO-states)
48+
set a fast access key to at least one of the entries.<br />
49+
50+
Examples (without the fast access):
4851
* `{'TODO', 'NEXT', '|', 'DONE'}`
4952
* `{'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}`
5053

54+
Examples (With fast access):
55+
* `{'TODO(t)', 'NEXT(n)', '|', 'DONE(d)'}`
56+
* `{'TODO(t)', 'NEXT', '|', 'DONE'}` - will work same as above. Only one todo keyword needs to have fast access key, others will be parsed from first char.
57+
58+
NOTE: Make sure fast access keys do not overlap. If that happens, first entry in list gets it.
59+
5160
#### **org_todo_keyword_faces**
5261
*type*: `table<string, string>`<br />
5362
*default value*: `{}`<br />
@@ -480,10 +489,10 @@ Decrease date under cursor by 1 day
480489
Change date under cursor. Opens calendar to select new date
481490
#### **org_todo**
482491
*mapped to*: `cit`<br />
483-
Cycle todo keyword forward on current headline ()
492+
Cycle todo keyword forward on current headline or open fast access to TODO states prompt (see [org_todo_keywords](#org_todo_keywords)) if it's enabled.
484493
#### **org_todo_prev**
485494
*mapped to*: `ciT`<br />
486-
Cycle todo keyword forward on current headline ()
495+
Cycle todo keyword backward on current headline.
487496
#### **org_toggle_checkbox**
488497
*mapped to*: `<C-Space>`<br />
489498
Toggle current line checkbox state

doc/orgmode.txt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,19 @@ default value: `{'TODO', '|', 'DONE'}`
147147
List of "unfinished" and "finished" states.
148148
`|` is used as a separator between "unfinished" and "finished".
149149
If `|` is omitted, only last entry in array is considered a "finished" state.
150-
Examples:
150+
To use Fast access to TODO States (https://orgmode.org/manual/Fast-access-to-TODO-states.html#Fast-access-to-TODO-states)
151+
set a fast access key to at least one of the entries.
152+
153+
Examples (without the fast access):
151154
* `{'TODO', 'NEXT', '|', 'DONE'}`
152155
* `{'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}`
153156

157+
Examples (With fast access):
158+
* `{'TODO(t)', 'NEXT(n)', '|', 'DONE(d)'}`
159+
* `{'TODO(t)', 'NEXT', '|', 'DONE'}` - will work same as above. Only one todo keyword needs to have fast access key, others will be parsed from first char.
160+
161+
NOTE: Make sure fast access keys do not overlap. If that happens, first entry in list gets it.
162+
154163
ORG_TODO_KEYWORD_FACES *orgmode-org_todo_keyword_faces*
155164

156165
type: `table<string, string>`
@@ -646,12 +655,12 @@ Change date under cursor. Opens calendar to select new date
646655
ORG_TODO *orgmode-org_todo*
647656

648657
mapped to: `cit`
649-
Cycle todo keyword forward on current headline ()
658+
Cycle todo keyword forward on current headline or open fast access to TODO states prompt (see org_todo_keywords (#org_todo_keywords)) if it's enabled.
650659

651660
ORG_TODO_PREV *orgmode-org_todo_prev*
652661

653662
mapped to: `ciT`
654-
Cycle todo keyword forward on current headline ()
663+
Cycle todo keyword backward on current headline.
655664

656665
ORG_TOGGLE_CHECKBOX *orgmode-org_toggle_checkbox*
657666

lua/orgmode/config/init.lua

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ local defaults = require('orgmode.config.defaults')
44
local mappings = require('orgmode.config.mappings')
55

66
---@class Config
7+
---@field opts table
8+
---@field todo_keywords table
79
local Config = {}
810

911
---@param opts? table
1012
function Config:new(opts)
1113
local data = {
1214
opts = vim.tbl_deep_extend('force', defaults, opts or {}),
15+
todo_keywords = nil,
1316
}
1417
setmetatable(data, self)
1518
return data
@@ -25,6 +28,7 @@ end
2528
---@param opts table
2629
---@return Config
2730
function Config:extend(opts)
31+
self.todo_keywords = nil
2832
self.opts = vim.tbl_deep_extend('force', self.opts, opts or {})
2933
return self
3034
end
@@ -93,19 +97,39 @@ function Config:get_agenda_span()
9397
end
9498

9599
function Config:get_todo_keywords()
96-
local types = { TODO = {}, DONE = {}, ALL = {} }
100+
if self.todo_keywords then
101+
return vim.deepcopy(self.todo_keywords)
102+
end
103+
local parse_todo = function(val)
104+
local value, shortcut = val:match('(.*)%((.)[^%)]*%)$')
105+
if value and shortcut then
106+
return { value = value, shortcut = shortcut, custom_shortcut = true }
107+
end
108+
return { value = val, shortcut = val:sub(1, 1):lower(), custom_shortcut = false }
109+
end
110+
local types = { TODO = {}, DONE = {}, ALL = {}, FAST_ACCESS = {}, has_fast_access = false }
97111
local type = 'TODO'
98112
for _, word in ipairs(self.opts.org_todo_keywords) do
99113
if word == '|' then
100114
type = 'DONE'
101115
else
102-
table.insert(types[type], word)
103-
table.insert(types.ALL, word)
116+
local data = parse_todo(word)
117+
if not types.has_fast_access and data.custom_shortcut then
118+
types.has_fast_access = true
119+
end
120+
table.insert(types[type], data.value)
121+
table.insert(types.ALL, data.value)
122+
table.insert(types.FAST_ACCESS, {
123+
value = data.value,
124+
type = type,
125+
shortcut = data.shortcut,
126+
})
104127
end
105128
end
106129
if #types.DONE == 0 then
107130
types.DONE = { table.remove(types.TODO, #types.TODO) }
108131
end
132+
self.todo_keywords = types
109133
return types
110134
end
111135

lua/orgmode/objects/todo_state.lua

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
local config = require('orgmode.config')
2+
local highlights = require('orgmode.colors.highlights')
23

34
---@class TodoState
45
---@field current_state string
6+
---@field hl_map table
57
---@field todos table
68
local TodoState = {}
79

@@ -11,20 +13,54 @@ function TodoState:new(data)
1113
opts.current_state = data.current_state or ''
1214
local todo_keywords = config:get_todo_keywords()
1315
opts.todos = {
14-
TODO = vim.tbl_add_reverse_lookup(todo_keywords.TODO),
15-
DONE = vim.tbl_add_reverse_lookup(todo_keywords.DONE),
16-
ALL = vim.tbl_add_reverse_lookup(todo_keywords.ALL),
16+
TODO = vim.tbl_add_reverse_lookup({ unpack(todo_keywords.TODO) }),
17+
DONE = vim.tbl_add_reverse_lookup({ unpack(todo_keywords.DONE) }),
18+
ALL = vim.tbl_add_reverse_lookup({ unpack(todo_keywords.ALL) }),
19+
FAST_ACCESS = todo_keywords.FAST_ACCESS,
20+
has_fast_access = todo_keywords.has_fast_access,
1721
}
22+
opts.hl_map = highlights.get_agenda_hl_map()
1823
setmetatable(opts, self)
1924
self.__index = self
2025
return opts
2126
end
2227

28+
---@return boolean
29+
function TodoState:has_fast_access()
30+
return self.todos.has_fast_access
31+
end
32+
33+
function TodoState:open_fast_access()
34+
local output = {}
35+
36+
for _, todo in ipairs(self.todos.FAST_ACCESS) do
37+
table.insert(output, { string.format('[%s] ', todo.shortcut) })
38+
table.insert(output, { todo.value, self.hl_map[todo.value] or self.hl_map[todo.type] })
39+
table.insert(output, { ' ' })
40+
end
41+
42+
table.insert(output, { '\n' })
43+
vim.api.nvim_echo(output, true, {})
44+
local char = vim.fn.nr2char(vim.fn.getchar())
45+
vim.cmd([[redraw!]])
46+
if char == ' ' then
47+
self.current_state = ''
48+
return { value = '', type = '' }
49+
end
50+
for _, todo in ipairs(self.todos.FAST_ACCESS) do
51+
if char == todo.shortcut then
52+
self.current_state = todo.value
53+
return { value = todo.value, type = todo.type, hl = self.hl_map[todo.value] or self.hl_map[todo.type] }
54+
end
55+
end
56+
end
57+
2358
---@return table
2459
function TodoState:get_next()
2560
if self.current_state == '' then
2661
self.current_state = self.todos.ALL[1]
27-
return { value = self.todos.ALL[1], type = 'TODO' }
62+
local val = self.todos.ALL[1]
63+
return { value = val, type = 'TODO', hl = self.hl_map[val] or self.hl_map.TODO }
2864
end
2965
local current_item_index = self.todos.ALL[self.current_state]
3066
local next_state = self.todos.ALL[current_item_index + 1]
@@ -35,15 +71,15 @@ function TodoState:get_next()
3571
self.current_state = next_state
3672
local type = self.todos.TODO[next_state] and 'TODO' or 'DONE'
3773

38-
return { value = next_state, type = type }
74+
return { value = next_state, type = type, hl = self.hl_map[next_state] or self.hl_map[type] }
3975
end
4076

4177
---@return table
4278
function TodoState:get_prev()
4379
if self.current_state == '' then
4480
local last_item = self.todos.ALL[#self.todos.ALL]
4581
self.current_state = last_item
46-
return { value = last_item, type = 'DONE' }
82+
return { value = last_item, type = 'DONE', hl = self.hl_map[last_item] or self.hl_map.DONE }
4783
end
4884
local current_item_index = self.todos.ALL[self.current_state]
4985
local prev_state = self.todos.ALL[current_item_index - 1]
@@ -54,11 +90,12 @@ function TodoState:get_prev()
5490
self.current_state = prev_state
5591
local type = self.todos.TODO[prev_state] and 'TODO' or 'DONE'
5692

57-
return { value = prev_state, type = type }
93+
return { value = prev_state, type = type, hl = self.hl_map[prev_state] or self.hl_map[type] }
5894
end
5995

6096
function TodoState:get_todo()
61-
return { value = self.todos.TODO[1], type = 'TODO' }
97+
local first = self.todos.TODO[1]
98+
return { value = first, type = 'TODO', hl = self.hl_map[first] or self.hl_map.TODO }
6299
end
63100

64101
return TodoState

lua/orgmode/org/mappings.lua

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ function OrgMappings:todo_next_state()
146146
local item = Files.get_current_file():get_closest_headline()
147147
local was_done = item:is_done()
148148
local old_state = item.todo_keyword.value
149-
self:_change_todo_state('next')
149+
local changed = self:_change_todo_state('next', true)
150+
if not changed then
151+
return
152+
end
150153
item = Files.get_current_file():get_closest_headline()
151154
if not item:is_done() and not was_done then
152155
return item
@@ -168,7 +171,7 @@ function OrgMappings:todo_next_state()
168171
self:_replace_date(date:apply_repeater())
169172
end
170173

171-
self:_change_todo_state('reset')
174+
self:_change_todo_state('reset', true)
172175
local state_change = string.format(
173176
'- State "%s" from "%s" [%s]',
174177
item.todo_keyword.value,
@@ -387,17 +390,34 @@ function OrgMappings:outline_up_heading()
387390
end
388391

389392
---@param direction string
390-
function OrgMappings:_change_todo_state(direction)
393+
---@param skip_fast_access boolean
394+
---@return string
395+
function OrgMappings:_change_todo_state(direction, use_fast_access)
391396
local item = Files.get_current_file():get_closest_headline()
392397
local todo = item.todo_keyword
393398
local todo_state = TodoState:new({ current_state = todo.value })
394399
local next_state = nil
395-
if direction == 'next' then
396-
next_state = todo_state:get_next()
397-
elseif direction == 'prev' then
398-
next_state = todo_state:get_prev()
399-
elseif direction == 'reset' then
400-
next_state = todo_state:get_todo()
400+
if use_fast_access and todo_state:has_fast_access() then
401+
next_state = todo_state:open_fast_access()
402+
else
403+
if direction == 'next' then
404+
next_state = todo_state:get_next()
405+
elseif direction == 'prev' then
406+
next_state = todo_state:get_prev()
407+
elseif direction == 'reset' then
408+
next_state = todo_state:get_todo()
409+
end
410+
end
411+
412+
if not next_state then
413+
return false
414+
end
415+
416+
if next_state.value == todo.value then
417+
if todo.value ~= '' then
418+
utils.echo_info('TODO state was already ', { { next_state.value, next_state.hl } })
419+
end
420+
return false
401421
end
402422

403423
local linenr = item.range.start_line
@@ -412,6 +432,7 @@ function OrgMappings:_change_todo_state(direction)
412432
end
413433
local new_line = vim.fn.getline(linenr):gsub('^' .. stars .. '%s+' .. old_state, stars .. ' ' .. new_state)
414434
vim.fn.setline(linenr, new_line)
435+
return true
415436
end
416437

417438
---@param date Date

lua/orgmode/utils.lua

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,35 @@ function utils.open(target)
4444
end
4545

4646
---@param msg string
47-
function utils.echo_warning(msg)
48-
vim.cmd([[redraw!]])
49-
return vim.api.nvim_echo({ { string.format('[orgmode] %s', msg), 'WarningMsg' } }, true, {})
47+
---@param additional_msg table
48+
function utils.echo_warning(msg, additional_msg)
49+
return utils._echo(msg, 'WarningMsg', additional_msg)
5050
end
5151

5252
---@param msg string
53-
function utils.echo_error(msg)
54-
vim.cmd([[redraw!]])
55-
return vim.api.nvim_echo({ { string.format('[orgmode] %s', msg), 'ErrorMsg' } }, true, {})
53+
---@param additional_msg table
54+
function utils.echo_error(msg, additional_msg)
55+
return utils._echo(msg, 'ErrorMsg', additional_msg)
5656
end
5757

5858
---@param msg string
59-
function utils.echo_info(msg)
59+
---@param additional_msg table
60+
function utils.echo_info(msg, additional_msg)
61+
return utils._echo(msg, nil, additional_msg)
62+
end
63+
64+
---@private
65+
function utils._echo(msg, hl, additional_msg)
6066
vim.cmd([[redraw!]])
61-
return vim.api.nvim_echo({ { string.format('[orgmode] %s', msg) } }, true, {})
67+
local msg_item = { string.format('[orgmode] %s', msg) }
68+
if hl then
69+
table.insert(msg_item, hl)
70+
end
71+
local msg_list = { msg_item }
72+
if additional_msg then
73+
msg_list = utils.concat(msg_list, additional_msg)
74+
end
75+
return vim.api.nvim_echo(msg_list, true, {})
6276
end
6377

6478
---@param word string

0 commit comments

Comments
 (0)