Skip to content

Commit b26b1f2

Browse files
feat: Add support for org log repeat (#738)
1 parent 069eda8 commit b26b1f2

File tree

7 files changed

+131
-66
lines changed

7 files changed

+131
-66
lines changed

DOCS.md

+10
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,16 @@ Possible values:
283283
- `note` - adds `CLOSED` date as above, and prompts for closing note via capture window. Confirm note with `org_note_finalize` (Default `<C-c>`), or ignore providing note via `org_note_kill` (Default `<Leader>ok`)
284284
- `false` - Disable any logging
285285

286+
#### **org_log_repeat**
287+
288+
_type_: `string|false`<br />
289+
_default value_: `time`<br />
290+
Possible values:
291+
292+
- `time` - adds `LAST_REPEAT` date to properties when marking headline with a repeater date as done
293+
- `note` - adds `LAST_REPEAT` date as above, and prompts for closing note via capture window. Confirm note with `org_note_finalize` (Default `<C-c>`), or ignore providing note via `org_note_kill` (Default `<Leader>ok`)
294+
- `false` - Disable logging the `LAST_REPEAT` date
295+
286296
#### **org_log_into_drawer**
287297

288298
_type_: `string|nil`<br />

lua/orgmode/capture/window.lua

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ function CaptureWindow:new(opts)
2525
return setmetatable(data, CaptureWindow)
2626
end
2727

28-
---
2928
function CaptureWindow:open()
3029
if self._window then
3130
return self:focus()

lua/orgmode/config/defaults.lua

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---@class OrgDefaultConfig
22
---@field org_id_method 'uuid' | 'ts' | 'org'
33
---@field org_agenda_span 'day' | 'week' | 'month' | 'year' | number
4+
---@field org_log_repeat 'time' | 'note' | false
45
---@field calendar { round_min_with_hours: boolean, min_big_step: number, min_small_step: number? }
56
local DefaultConfig = {
67
org_agenda_files = '',
@@ -39,6 +40,7 @@ local DefaultConfig = {
3940
org_hide_emphasis_markers = false,
4041
org_ellipsis = '...',
4142
org_log_done = 'time',
43+
org_log_repeat = 'time',
4244
org_log_into_drawer = nil,
4345
org_highlight_latex_and_related = nil,
4446
org_custom_exports = {},

lua/orgmode/config/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function Config:new(opts)
2222
end
2323

2424
function Config:__index(key)
25-
if self.opts[key] then
25+
if self.opts[key] ~= nil then
2626
return self.opts[key]
2727
end
2828
return rawget(getmetatable(self), key)

lua/orgmode/files/headline.lua

+17
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,23 @@ function Headline:set_property(name, value)
424424
return self:refresh()
425425
end
426426

427+
---@param note string[] | nil
428+
---@return OrgHeadline
429+
function Headline:add_note(note)
430+
if not note then
431+
return self
432+
end
433+
local drawer = config.org_log_into_drawer
434+
local append_line
435+
if drawer ~= nil then
436+
append_line = self:get_drawer_append_line(drawer)
437+
else
438+
append_line = self:get_append_line()
439+
end
440+
vim.api.nvim_buf_set_lines(self.file:get_valid_bufnr(), append_line, append_line, false, note)
441+
return self:refresh()
442+
end
443+
427444
---@param property_name string
428445
---@param search_parents? boolean
429446
---@return string | nil, TSNode | nil

lua/orgmode/org/mappings.lua

+68-64
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ local utils = require('orgmode.utils')
1212
local fs = require('orgmode.utils.fs')
1313
local Table = require('orgmode.files.elements.table')
1414
local EventManager = require('orgmode.events')
15-
local Promise = require('orgmode.utils.promise')
1615
local events = EventManager.event
17-
local Link = require('orgmode.org.hyperlinks.link')
1816
local Babel = require('orgmode.babel')
19-
local OrgApi = require('orgmode.api')
2017

2118
---@class OrgMappings
2219
---@field capture OrgCapture
@@ -384,96 +381,103 @@ function OrgMappings:_todo_change_state(direction)
384381
local old_state = headline:get_todo()
385382
local was_done = headline:is_done()
386383
local changed = self:_change_todo_state(direction, true)
384+
387385
if not changed then
388386
return
389387
end
388+
390389
local item = self.files:get_closest_headline()
390+
EventManager.dispatch(events.TodoChanged:new(item, old_state, was_done))
391391

392-
local dispatchEvent = function()
393-
EventManager.dispatch(events.TodoChanged:new(item, old_state, was_done))
394-
return item
395-
end
392+
local is_done = item:is_done() and not was_done
393+
local is_undone = not item:is_done() and was_done
396394

397-
if not item:is_done() and not was_done then
398-
return dispatchEvent()
395+
-- State was changed in the same group (TODO NEXT | DONE)
396+
-- For example: Changed from TODO to NEXT
397+
if not is_done and not is_undone then
398+
return item
399399
end
400400

401-
local log_note = config.org_log_done == 'note'
402-
local log_time = config.org_log_done == 'time'
403-
local should_log_time = log_note or log_time
401+
local prompt_done_note = config.org_log_done == 'note'
402+
local log_closed_time = config.org_log_done == 'time'
404403
local indent = headline:get_indent()
405404

406-
local get_note = function(note)
407-
if note == nil then
408-
return
409-
end
405+
local closing_note_text = ('%s- CLOSING NOTE %s \\\\'):format(indent, Date.now():to_wrapped_string(false))
410406

411-
for i, line in ipairs(note) do
412-
note[i] = indent .. ' ' .. line
413-
end
407+
local get_note = function(template)
408+
return self.capture.closing_note:open():next(function(closing_note)
409+
if closing_note == nil then
410+
return
411+
end
414412

415-
table.insert(note, 1, ('%s- CLOSING NOTE %s \\\\'):format(indent, Date.now():to_wrapped_string(false)))
416-
return note
413+
for i, line in ipairs(closing_note) do
414+
closing_note[i] = indent .. ' ' .. line
415+
end
416+
417+
return vim.list_extend({ template }, closing_note)
418+
end)
417419
end
418420

419421
local repeater_dates = item:get_repeater_dates()
420-
if #repeater_dates == 0 then
421-
if should_log_time and item:is_done() and not was_done then
422-
headline:set_closed_date()
423-
item = self.files:get_closest_headline()
424422

425-
if log_note then
426-
dispatchEvent()
427-
return self.capture.closing_note:open():next(function(note)
428-
local valid_note = get_note(note)
429-
if valid_note then
430-
local append_line = headline:get_append_line()
431-
vim.api.nvim_buf_set_lines(0, append_line, append_line, false, valid_note)
432-
end
433-
end)
423+
-- No dates with a repeater. Add closed date and note if enabled.
424+
if #repeater_dates == 0 then
425+
local set_closed_date = prompt_done_note or log_closed_time
426+
if set_closed_date then
427+
if is_done then
428+
headline:set_closed_date()
429+
elseif is_undone then
430+
headline:remove_closed_date()
434431
end
432+
item = self.files:get_closest_headline()
435433
end
436-
if should_log_time and not item:is_done() and was_done then
437-
headline:remove_closed_date()
434+
435+
if is_undone or not prompt_done_note then
436+
return item
438437
end
439-
return dispatchEvent()
438+
439+
return get_note(closing_note_text):next(function(closing_note)
440+
return item:add_note(closing_note)
441+
end)
440442
end
441443

442444
for _, date in ipairs(repeater_dates) do
443445
self:_replace_date(date:apply_repeater())
444446
end
445-
446447
local new_todo = item:get_todo()
447448
self:_change_todo_state('reset')
448-
local state_change = {
449-
string.format('%s- State "%s" from "%s" [%s]', indent, new_todo, old_state, Date.now():to_string()),
450-
}
451449

452-
dispatchEvent()
453-
return Promise.resolve()
454-
:next(function()
455-
if not log_note then
456-
return state_change
457-
end
450+
local prompt_repeat_note = config.org_log_repeat == 'note'
451+
local log_repeat_enabled = config.org_log_repeat ~= false
452+
local repeat_note_template = ('%s- State "%s" from "%s" [%s]'):format(
453+
indent,
454+
new_todo,
455+
old_state,
456+
Date.now():to_string()
457+
)
458458

459-
return self.capture.closing_note:open():next(function(closing_note)
460-
return get_note(closing_note)
461-
end)
462-
end)
463-
:next(function(note)
464-
headline:set_property('LAST_REPEAT', Date.now():to_wrapped_string(false))
465-
if not note then
466-
return
467-
end
468-
local drawer = config.org_log_into_drawer
469-
local append_line
470-
if drawer ~= nil then
471-
append_line = headline:get_drawer_append_line(drawer)
472-
else
473-
append_line = headline:get_append_line()
474-
end
475-
vim.api.nvim_buf_set_lines(0, append_line, append_line, false, note)
459+
if log_repeat_enabled then
460+
headline:set_property('LAST_REPEAT', Date.now():to_wrapped_string(false))
461+
end
462+
463+
if not prompt_repeat_note and not prompt_done_note then
464+
-- If user is not prompted for a note, use a default repeat note
465+
if log_repeat_enabled then
466+
return item:add_note({ repeat_note_template })
467+
end
468+
return item
469+
end
470+
471+
-- Done note has precedence over repeat note
472+
if prompt_done_note then
473+
return get_note(closing_note_text):next(function(closing_note)
474+
return item:add_note(closing_note)
476475
end)
476+
end
477+
478+
return get_note(repeat_note_template .. ' \\\\'):next(function(closing_note)
479+
return item:add_note(closing_note)
480+
end)
477481
end
478482

479483
function OrgMappings:do_promote(whole_subtree)

tests/plenary/ui/mappings/todo_spec.lua

+33
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,38 @@ describe('Todo mappings', function()
7272
'* TODO Another task',
7373
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))
7474
end)
75+
76+
it('should change todo state of repeatable task and not log last repeat date if disabled', function()
77+
helpers.create_agenda_file({
78+
'#TITLE: Test',
79+
'',
80+
'* TODO Test orgmode',
81+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
82+
'',
83+
'* TODO Another task',
84+
}, {
85+
org_log_repeat = false,
86+
})
87+
88+
assert.are.same({
89+
'* TODO Test orgmode',
90+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
91+
'',
92+
'* TODO Another task',
93+
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
94+
vim.fn.cursor(3, 1)
95+
vim.cmd([[norm cit]])
96+
vim.wait(50)
97+
assert.are.same({
98+
'* TODO Test orgmode',
99+
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
100+
'',
101+
'* TODO Another task',
102+
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))
103+
104+
config.org_log_repeat = 'time'
105+
end)
106+
75107
it('should add last repeat property and state change to drawer (org_log_into_drawer)', function()
76108
config:extend({
77109
org_log_into_drawer = 'LOGBOOK',
@@ -125,6 +157,7 @@ describe('Todo mappings', function()
125157
'* TODO Another task',
126158
}, vim.api.nvim_buf_get_lines(0, 2, 13, false))
127159
end)
160+
128161
it('should change todo state of a headline backward (org_todo_prev)', function()
129162
helpers.create_agenda_file({
130163
'#TITLE: Test',

0 commit comments

Comments
 (0)