424424local 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
754773end
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).
758826local 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