Skip to content

Commit bccd30d

Browse files
committed
fix: avoid orgmode temp windows for loaded agenda edits
Apply repeat-task state changes and schedule/deadline edits directly in already loaded org buffers instead of routing through orgmode's hidden temp window path. This prevents E36 errors, returns reloaded headline objects correctly, and keeps agenda actions stable when the source file is open in the same Neovim instance.
1 parent fb2b379 commit bccd30d

1 file changed

Lines changed: 134 additions & 66 deletions

File tree

lua/org-super-agenda/adapters/neovim/actions.lua

Lines changed: 134 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ end
424424
local function apply_heading_state_via_orgmode(hl, next_state)
425425
local todos = get_headline_todo_keywords(hl)
426426
local path = (hl.file and hl.file.filename) or hl.filename
427+
local start_line = hl.position and hl.position.start_line or 1
427428
local current_value = hl.todo_value or (hl._section and hl._section:get_todo()) or ''
428429
local current_kw = current_value ~= '' and todos and todos:find(current_value) or nil
429430
local target_kw = todos and todos:find(next_state) or nil
@@ -440,77 +441,95 @@ local function apply_heading_state_via_orgmode(hl, next_state)
440441
return false, { kind = 'swap_conflict', path = path }
441442
end
442443

443-
local ok, result = pcall(function()
444-
return hl:_do_action(function()
445-
local org = require('orgmode')
446-
local instance = org.instance()
447-
local mappings = instance and instance.org_mappings
448-
local current = instance and instance.files and instance.files:get_closest_headline()
449-
if not current then
450-
error('orgmode todo transition engine is unavailable', 0)
451-
end
444+
local function run_transition()
445+
local org = require('orgmode')
446+
local instance = org.instance()
447+
local mappings = instance and instance.org_mappings
448+
local current = instance and instance.files and instance.files:get_closest_headline()
449+
if not current then
450+
error('orgmode todo transition engine is unavailable', 0)
451+
end
452452

453-
local old_state = current:get_todo()
454-
local was_done = current:is_done()
455-
current:set_todo(next_state)
453+
local old_state = current:get_todo()
454+
local was_done = current:is_done()
455+
current:set_todo(next_state)
456456

457-
local item = instance.files:get_closest_headline()
458-
local is_done = item:is_done() and not was_done
459-
local is_undone = not item:is_done() and was_done
457+
local item = instance.files:get_closest_headline()
458+
local is_done = item:is_done() and not was_done
459+
local is_undone = not item:is_done() and was_done
460460

461-
if not is_done and not is_undone then
462-
return true
463-
end
461+
if not is_done and not is_undone then
462+
return true
463+
end
464464

465-
local org_config = require('orgmode.config')
466-
local Date = require('orgmode.objects.date')
467-
local TodoState = require('orgmode.objects.todo_state')
468-
local repeater_dates = item:get_repeater_dates()
469-
470-
if #repeater_dates == 0 then
471-
local log_closed_time = org_config.org_log_done == 'time'
472-
if log_closed_time then
473-
if is_done then
474-
item:set_closed_date()
475-
elseif is_undone then
476-
item:remove_closed_date()
477-
end
465+
local org_config = require('orgmode.config')
466+
local Date = require('orgmode.objects.date')
467+
local TodoState = require('orgmode.objects.todo_state')
468+
local repeater_dates = item:get_repeater_dates()
469+
470+
if #repeater_dates == 0 then
471+
local log_closed_time = org_config.org_log_done == 'time'
472+
if log_closed_time then
473+
if is_done then
474+
item:set_closed_date()
475+
elseif is_undone then
476+
item:remove_closed_date()
478477
end
479-
return true
480478
end
479+
return true
480+
end
481481

482-
if not (mappings and mappings._replace_date) then
483-
error('orgmode repeater engine is unavailable', 0)
484-
end
482+
if not (mappings and mappings._replace_date) then
483+
error('orgmode repeater engine is unavailable', 0)
484+
end
485485

486-
for _, date in ipairs(repeater_dates) do
487-
mappings:_replace_date(date:apply_repeater())
488-
end
486+
for _, date in ipairs(repeater_dates) do
487+
mappings:_replace_date(date:apply_repeater())
488+
end
489489

490-
item = instance.files:get_closest_headline()
491-
local new_todo = item:get_todo()
492-
local todo_state = TodoState:new({ current_state = new_todo, todos = item.file:get_todo_keywords() })
493-
local reset_keyword = todo_state:get_reset_todo(item, old_state)
494-
item:set_todo(reset_keyword.value)
495-
496-
local log_repeat_enabled = org_config.org_log_repeat ~= false
497-
local prompt_repeat_note = org_config.org_log_repeat == 'note'
498-
local prompt_done_note = org_config.org_log_done == 'note'
499-
if log_repeat_enabled then
500-
item:set_property('LAST_REPEAT', Date.now():to_wrapped_string(false))
501-
if not prompt_repeat_note and not prompt_done_note then
502-
local indent = item:get_indent()
503-
local repeat_note_template = ('%s- State %-12s from %-12s [%s]'):format(
504-
indent,
505-
[["]] .. (new_todo or '') .. [["]],
506-
[["]] .. (old_state or '') .. [["]],
507-
Date.now():to_string()
508-
)
509-
item:add_note({ repeat_note_template })
510-
end
490+
item = instance.files:get_closest_headline()
491+
local new_todo = item:get_todo()
492+
local todo_state = TodoState:new({ current_state = new_todo, todos = item.file:get_todo_keywords() })
493+
local reset_keyword = todo_state:get_reset_todo(item, old_state)
494+
item:set_todo(reset_keyword.value)
495+
496+
local log_repeat_enabled = org_config.org_log_repeat ~= false
497+
local prompt_repeat_note = org_config.org_log_repeat == 'note'
498+
local prompt_done_note = org_config.org_log_done == 'note'
499+
if log_repeat_enabled then
500+
item:set_property('LAST_REPEAT', Date.now():to_wrapped_string(false))
501+
if not prompt_repeat_note and not prompt_done_note then
502+
local indent = item:get_indent()
503+
local repeat_note_template = ('%s- State %-12s from %-12s [%s]'):format(
504+
indent,
505+
[["]] .. (new_todo or '') .. [["]],
506+
[["]] .. (old_state or '') .. [["]],
507+
Date.now():to_string()
508+
)
509+
item:add_note({ repeat_note_template })
511510
end
511+
end
512512

513-
return true
513+
return true
514+
end
515+
516+
local ok, result = pcall(function()
517+
if st.loaded then
518+
return vim.api.nvim_buf_call(st.bufnr, function()
519+
local view = vim.fn.winsaveview() or {}
520+
vim.fn.cursor({ start_line, 1 })
521+
run_transition()
522+
vim.cmd('silent noautocmd write')
523+
vim.fn.winrestview(view)
524+
local ok_reload, reloaded = pcall(function()
525+
return hl:reload()
526+
end)
527+
return ok_reload and reloaded or hl
528+
end)
529+
end
530+
531+
return hl:_do_action(function()
532+
return run_transition()
514533
end):wait(20000)
515534
end)
516535

@@ -753,6 +772,55 @@ local function find_stamp_near(path, start_line, keyword)
753772
return nil
754773
end
755774

775+
local function open_loaded_datepicker(hl, keyword, bufnr)
776+
local Calendar = require('orgmode.objects.calendar')
777+
local Date = require('orgmode.objects.date')
778+
779+
local get_date = keyword == 'SCHEDULED' and 'get_scheduled_date' or 'get_deadline_date'
780+
local set_date = keyword == 'SCHEDULED' and 'set_scheduled_date' or 'set_deadline_date'
781+
local remove_date = keyword == 'SCHEDULED' and 'remove_scheduled_date' or 'remove_deadline_date'
782+
local title = keyword == 'SCHEDULED' and 'Set schedule' or 'Set deadline'
783+
local initial = hl._section and hl._section[get_date] and hl._section[get_date](hl._section) or Date.today()
784+
785+
return Calendar.new({ date = initial, clearable = true, title = title }):open():next(function(new_date, cleared)
786+
if not new_date and not cleared then
787+
return
788+
end
789+
790+
return vim.api.nvim_buf_call(bufnr, function()
791+
local view = vim.fn.winsaveview() or {}
792+
vim.fn.cursor({ hl.position.start_line, 1 })
793+
794+
local org = require('orgmode')
795+
local item = org.instance().files:get_closest_headline()
796+
if not item then
797+
error('orgmode date change target is unavailable', 0)
798+
end
799+
800+
if cleared then
801+
item[remove_date](item)
802+
else
803+
item[set_date](item, new_date)
804+
end
805+
806+
vim.cmd('silent noautocmd write')
807+
vim.fn.winrestview(view)
808+
return true
809+
end)
810+
end)
811+
end
812+
813+
local function open_headline_datepicker(hl, keyword)
814+
local st = buf_status_for(hl.file.filename)
815+
if st.loaded then
816+
return open_loaded_datepicker(hl, keyword, st.bufnr)
817+
end
818+
if keyword == 'SCHEDULED' then
819+
return hl:set_scheduled()
820+
end
821+
return hl:set_deadline()
822+
end
823+
756824
-- Apply a date bulk-op: open datepicker on first_hl, then mirror result to all targets.
757825
-- Handles both "set new date" and "remove date" (r in datepicker).
758826
local function bulk_apply_date(targets, cur, first_hl, snap_first, keyword, open_picker)
@@ -1017,12 +1085,12 @@ local function bulk_action_menu(line_map)
10171085
elseif choice.key == 'r' then
10181086
-- bulk reschedule: prompt once via first item, apply result to all
10191087
bulk_apply_date(targets, cur, first_hl, snap_first, 'SCHEDULED', function(hl)
1020-
return hl:set_scheduled()
1088+
return open_headline_datepicker(hl, 'SCHEDULED')
10211089
end)
10221090
elseif choice.key == 'd' then
10231091
-- bulk deadline: same pattern
10241092
bulk_apply_date(targets, cur, first_hl, snap_first, 'DEADLINE', function(hl)
1025-
return hl:set_deadline()
1093+
return open_headline_datepicker(hl, 'DEADLINE')
10261094
end)
10271095
end
10281096
end)
@@ -1049,12 +1117,12 @@ local function bulk_action_menu(line_map)
10491117
elseif c == 'r' then
10501118
-- bulk reschedule: prompt once via first item, apply result to all
10511119
bulk_apply_date(targets, cur, first_hl, snap_first, 'SCHEDULED', function(hl)
1052-
return hl:set_scheduled()
1120+
return open_headline_datepicker(hl, 'SCHEDULED')
10531121
end)
10541122
elseif c == 'd' then
10551123
-- bulk deadline: same pattern
10561124
bulk_apply_date(targets, cur, first_hl, snap_first, 'DEADLINE', function(hl)
1057-
return hl:set_deadline()
1125+
return open_headline_datepicker(hl, 'DEADLINE')
10581126
end)
10591127
end
10601128
end
@@ -1167,7 +1235,7 @@ function A.set_keymaps(buf, win, line_map, reopen)
11671235
end
11681236
local snap = st.loaded and snapshot_heading_from_buf(hl) or snapshot_heading_from_disk(path, hl.position.start_line)
11691237
local ok_p, p = pcall(function()
1170-
return hl:set_scheduled()
1238+
return open_headline_datepicker(hl, 'SCHEDULED')
11711239
end)
11721240
if not ok_p then
11731241
if is_swap_error(p) then
@@ -1199,7 +1267,7 @@ function A.set_keymaps(buf, win, line_map, reopen)
11991267
end
12001268
local snap = st.loaded and snapshot_heading_from_buf(hl) or snapshot_heading_from_disk(path, hl.position.start_line)
12011269
local ok_p, p = pcall(function()
1202-
return hl:set_deadline()
1270+
return open_headline_datepicker(hl, 'DEADLINE')
12031271
end)
12041272
if not ok_p then
12051273
if is_swap_error(p) then

0 commit comments

Comments
 (0)