@@ -2244,18 +2244,21 @@ function M.count_child_todos(todo_item, todo_map, opts)
22442244end
22452245
22462246--- Archives completed todo items to a designated section
2247+ --- @param ctx checkmate.TransactionContext
22472248--- @param opts ? { heading ?: { title ?: string , level ?: integer }, include_children ?: boolean , newest_first ?: boolean } Archive options
2248- --- @return boolean success Whether any items were archived
2249- function M .archive_todos (opts )
2249+ --- @return checkmate.TextDiffHunk[] hunks
2250+ function M .archive_todos (ctx , opts )
22502251 opts = opts or {}
22512252
2253+ local bufnr = ctx .get_buf ()
2254+
22522255 -- create the Markdown heading that the user has defined, e.g. ## Archived
22532256 local archive_heading_string = util .get_heading_string (
22542257 opts .heading and opts .heading .title or config .options .archive .heading .title or " Archived" ,
22552258 opts .heading and opts .heading .level or config .options .archive .heading .level or 2
22562259 )
2257- local include_children = opts .include_children ~= false -- default: true
2258- local newest_first = opts .newest_first or config .options .archive .newest_first ~= false -- default: true
2260+ local include_children = opts .include_children ~= false
2261+ local newest_first = opts .newest_first or config .options .archive .newest_first ~= false
22592262 local parent_spacing = math.max (config .options .archive .parent_spacing or 0 , 0 )
22602263
22612264 -- helpers
@@ -2273,18 +2276,15 @@ function M.archive_todos(opts)
22732276 end
22742277 end
22752278
2276- -- discover todos and current archive block boundaries
2277-
2278- local bufnr = vim .api .nvim_get_current_buf ()
2279- local todo_map = parser .get_todo_map (bufnr )
2279+ local todo_map = ctx .get_todo_map ()
22802280 local sorted_todos = util .get_sorted_todo_list (todo_map )
22812281 local current_buf_lines = vim .api .nvim_buf_get_lines (bufnr , 0 , - 1 , false )
22822282
22832283 local archive_start_row , archive_end_row
22842284 do
22852285 local heading_level = archive_heading_string :match (" ^(#+)" )
22862286 local heading_level_len = heading_level and # heading_level or 0
2287- -- The 'next' heading level is a heading at the same or higher level
2287+ -- the 'next' heading level is a heading at the same or higher level
22882288 -- which represents a new section
22892289 local next_heading_pat = heading_level
22902290 and (" ^%s*" .. string.rep (" #" , heading_level_len , heading_level_len ) .. " +%s" )
@@ -2346,160 +2346,185 @@ function M.archive_todos(opts)
23462346
23472347 if archived_root_cnt == 0 then
23482348 util .notify (" No completed todo items to archive" , vim .log .levels .INFO )
2349- return false
2349+ return {}
23502350 end
23512351
23522352 table.sort (archived_ranges , function (a , b )
23532353 return a .start_row < b .start_row
23542354 end )
23552355
2356- -- rebuild buffer content
2357- -- start with re-creating the buffer's 'active' section (non-archived)
2358-
2359- local new_main_content = {} -- lines that will remain in the main document
2360- local new_archive_content = {} -- lines that will live under the Archive heading
2361- local newly_archived_lines = {} -- temp storage for newly added archived todos
2362-
2363- -- Walk every line of the current buffer.
2364- -- we copy it into `new_content` unless it is:
2365- -- a) part of the existing archive section, or
2366- -- b) part of a todo block we’re about to archive, or
2367- -- c) the single blank line that immediately follows such a block
2368- -- (to avoid stray empty rows after removal).
2369- local i = 1
2370- while i <= # current_buf_lines do
2371- local idx = i - 1 -- 0-indexed (current row)
2372-
2373- -- skip over existing archive section in one jump
2374- if archive_start_row and idx >= archive_start_row and idx <= archive_end_row then
2375- i = archive_end_row + 2
2376- else
2377- -- skip lines inside newly-archived ranges (plus trailing blank directly after each)
2378- local skip = false
2379- for _ , r in ipairs (archived_ranges ) do
2380- if idx >= r .start_row and idx <= r .end_row then
2381- skip = true -- inside a soon-to-be-archived todo
2382- break
2383- end
2384- end
2385- -- handle spacing preservation
2386- if not skip and current_buf_lines [i ] == " " then
2387- for _ , r in ipairs (archived_ranges ) do
2388- if idx == r .end_row + 1 then
2389- -- this blank line is immediately after an archived todo
2390- -- ...so check if there was a blank line before the todo
2391- local has_blank_before = r .start_row > 0 and current_buf_lines [r .start_row ] == " "
2392-
2393- -- check if we already added content (which means there's something before)
2394- local has_content_before = # new_main_content > 0
2395-
2396- -- Skip this blank line if:
2397- -- - already have blank line before todo
2398- -- - the last line we added to new_main_content is blank
2399- if has_blank_before or (has_content_before and new_main_content [# new_main_content ] == " " ) then
2400- skip = true
2401- end
2402- break
2403- end
2404- end
2405- end
2406-
2407- if not skip then
2408- new_main_content [# new_main_content + 1 ] = current_buf_lines [i ]
2409- end
2410- i = i + 1
2411- end
2412- end
2413-
2414- -- If an archive section already exists, copy everything below its heading
2415-
2416- if archive_start_row and archive_end_row and archive_end_row >= archive_start_row + 1 then
2417- local start = archive_start_row + 2
2418- while start <= archive_end_row + 1 and current_buf_lines [start ] == " " do
2419- start = start + 1
2420- end
2421- for j = start , archive_end_row + 1 do
2422- new_archive_content [# new_archive_content + 1 ] = current_buf_lines [j ]
2423- end
2424- trim_trailing_blank (new_archive_content )
2425- end
2356+ -- collect archived lines (to insert into archive section)
24262357
2427- -- collect newly archived todo items
2358+ local newly_archived_lines = {}
2359+ local to_delete = {} --- @type table<integer , boolean>
24282360
24292361 if # archived_ranges > 0 then
24302362 for idx , r in ipairs (archived_ranges ) do
2363+ -- collect lines for archive payload
24312364 for row = r .start_row , r .end_row do
24322365 newly_archived_lines [# newly_archived_lines + 1 ] = current_buf_lines [row + 1 ]
24332366 end
2434-
24352367 -- spacing after each root todo except the last
24362368 if idx < # archived_ranges and parent_spacing > 0 then
24372369 add_spacing (newly_archived_lines )
24382370 end
2371+
2372+ -- mark all lines in the range for deletion
2373+ for row = r .start_row , r .end_row do
2374+ to_delete [row ] = true
2375+ end
24392376 end
24402377 end
24412378
2442- -- combine existing and new archive content based on newest_first option
2379+ -- blank-line cleanup: avoid leaving stray empty lines after deletions
2380+ -- For each archived block, if the immediately following line is blank,
2381+ -- delete it when it would create double-blank or preserve an existing blank above
2382+ for _ , r in ipairs (archived_ranges ) do
2383+ local after = r .end_row + 1
2384+ local before = r .start_row - 1
24432385
2444- if newest_first then
2445- -- newest items go at the top of the archive section
2446- local combined_lines = {}
2386+ if after <= # current_buf_lines - 1 and current_buf_lines [after + 1 ] == " " then
2387+ -- find the nearest surviving line above
2388+ while before >= 0 and to_delete [before ] do
2389+ before = before - 1
2390+ end
2391+ local has_blank_before = (before >= 0 ) and (current_buf_lines [before + 1 ] == " " )
24472392
2448- -- add new items first
2449- for _ , line in ipairs (newly_archived_lines ) do
2450- combined_lines [# combined_lines + 1 ] = line
2393+ -- if we already have a blank above (or the top is blank), remove the trailing blank
2394+ if has_blank_before then
2395+ to_delete [after ] = true
2396+ end
24512397 end
2398+ end
24522399
2453- -- add spacing between new and existing content if both exist
2454- if # newly_archived_lines > 0 and # new_archive_content > 0 and parent_spacing > 0 then
2455- add_spacing (combined_lines )
2400+ -- make contiguous delete hunks rows marked in `to_delete`
2401+ -- Idea: scan the buffer once, whenever we see a run of deletable lines, start it...
2402+ -- when the run ends, emit one delete hunk covering [run_start, last_row]
2403+ local delete_hunks = {}
2404+ do
2405+ local run_start --- @type integer | nil
2406+ local function flush_run (last_row )
2407+ if run_start ~= nil then
2408+ table.insert (delete_hunks , diff .make_line_delete ({ run_start , last_row }))
2409+ run_start = nil
2410+ end
24562411 end
24572412
2458- -- add existing archive content
2459- for _ , line in ipairs (new_archive_content ) do
2460- combined_lines [# combined_lines + 1 ] = line
2413+ for row = 0 , # current_buf_lines - 1 do
2414+ if to_delete [row ] then
2415+ if run_start == nil then
2416+ run_start = row
2417+ end
2418+ else
2419+ if run_start ~= nil then
2420+ flush_run (row - 1 )
2421+ end
2422+ end
24612423 end
2424+ flush_run (# current_buf_lines - 1 )
2425+ end
24622426
2463- new_archive_content = combined_lines
2464- else
2465- -- newest items go at the bottom (default behavior)
2466- if # new_archive_content > 0 and # newly_archived_lines > 0 and parent_spacing > 0 then
2467- add_spacing (new_archive_content ) -- gap between old and new archive content
2468- end
2427+ -- prepare archive section insertion/merge hunks
24692428
2470- for _ , line in ipairs (newly_archived_lines ) do
2471- new_archive_content [# new_archive_content + 1 ] = line
2472- end
2473- end
2429+ trim_trailing_blank (newly_archived_lines )
24742430
2475- -- make sure we don't leave more than `parent_spacing`
2476- -- blank lines at the very end of the archive section.
2477- trim_trailing_blank (new_archive_content )
2431+ local archive_hunks = {}
24782432
2479- -- inject archive section into document
2433+ if # newly_archived_lines > 0 then
2434+ if not archive_start_row then
2435+ -- no archive section exists yet: append at end of buffer
2436+ local line_count = # current_buf_lines
24802437
2481- if # new_archive_content > 0 then
2482- -- blank line before archive heading if needed
2483- if # new_main_content > 0 and new_main_content [# new_main_content ] ~= " " then
2484- new_main_content [# new_main_content + 1 ] = " "
2485- end
2486- new_main_content [# new_main_content + 1 ] = archive_heading_string
2487- new_main_content [# new_main_content + 1 ] = " " -- blank after heading
2488- for _ , line in ipairs (new_archive_content ) do
2489- new_main_content [# new_main_content + 1 ] = line
2438+ local insertion = {} --- heading + required blank + content
2439+ -- ensure single blank line before heading if buffer isn't empty & last line non-blank
2440+ local need_pre_blank = line_count > 0 and current_buf_lines [# current_buf_lines ] ~= " "
2441+ if need_pre_blank then
2442+ insertion [# insertion + 1 ] = " "
2443+ end
2444+ insertion [# insertion + 1 ] = archive_heading_string
2445+ insertion [# insertion + 1 ] = " "
2446+ for _ , l in ipairs (newly_archived_lines ) do
2447+ insertion [# insertion + 1 ] = l
2448+ end
2449+
2450+ archive_hunks [# archive_hunks + 1 ] = diff .make_line_insert (line_count , insertion )
2451+ else
2452+ -- Archive section exists: normalize a single blank after heading,
2453+ -- then insert at top (newest_first) or append at bottom (!newest_first)
2454+
2455+ -- 1) normalize the run of blanks after the heading to exactly one
2456+ local first_nonblank_after = archive_start_row + 1
2457+ while first_nonblank_after <= archive_end_row and current_buf_lines [first_nonblank_after + 1 ] == " " do
2458+ first_nonblank_after = first_nonblank_after + 1
2459+ end
2460+ if first_nonblank_after == archive_start_row + 1 then
2461+ -- no blank line existed; create one right after heading
2462+ archive_hunks [# archive_hunks + 1 ] = diff .make_line_insert (archive_start_row + 1 , { " " })
2463+ else
2464+ -- there is at least one blank; compress to a single blank
2465+ archive_hunks [# archive_hunks + 1 ] = diff .make_line_replace (
2466+ { archive_start_row + 1 , first_nonblank_after - 1 },
2467+ { " " }
2468+ )
2469+ end
2470+
2471+ if newest_first then
2472+ -- insert just after the single blank line under the heading
2473+ local insert_row = archive_start_row + 2
2474+ local payload = {}
2475+ for _ , l in ipairs (newly_archived_lines ) do
2476+ payload [# payload + 1 ] = l
2477+ end
2478+
2479+ -- if archive content already exists and spacing requested, add spacer between new and existing
2480+ local has_existing = (archive_end_row and (archive_end_row >= archive_start_row + 1 ))
2481+ and (archive_end_row >= insert_row )
2482+ if has_existing and parent_spacing > 0 then
2483+ add_spacing (payload )
2484+ end
2485+
2486+ archive_hunks [# archive_hunks + 1 ] = diff .make_line_insert (insert_row , payload )
2487+ else
2488+ -- Append to bottom of existing archive content
2489+ -- Find the last non-blank line inside the archive section to avoid
2490+ -- inserting after a stray trailing blank
2491+ local tail = archive_end_row or (archive_start_row + 1 )
2492+ while tail > archive_start_row and current_buf_lines [tail + 1 ] == " " do
2493+ tail = tail - 1
2494+ end
2495+
2496+ local append_at = tail + 1
2497+ local payload = {}
2498+
2499+ -- if there is existing content and spacing requested, add spacer first
2500+ local has_existing = tail > archive_start_row
2501+ if has_existing and parent_spacing > 0 then
2502+ add_spacing (payload )
2503+ end
2504+
2505+ for _ , l in ipairs (newly_archived_lines ) do
2506+ payload [# payload + 1 ] = l
2507+ end
2508+
2509+ archive_hunks [# archive_hunks + 1 ] = diff .make_line_insert (append_at , payload )
2510+ end
24902511 end
24912512 end
24922513
2493- -- write buffer
2494- util .with_preserved_view (function ()
2495- vim .api .nvim_buf_set_lines (bufnr , 0 , - 1 , false , new_main_content )
2496- end )
2514+ local hunks = {}
2515+ vim .list_extend (hunks , delete_hunks )
2516+ vim .list_extend (hunks , archive_hunks )
24972517
2498- util .notify (
2499- (" Archived %d todo item%s" ):format (archived_root_cnt , archived_root_cnt > 1 and " s" or " " ),
2500- vim .log .levels .INFO
2501- )
2502- return true
2518+ if archived_root_cnt > 0 then
2519+ ctx .add_cb (function ()
2520+ util .notify (
2521+ (" Archived %d todo item%s" ):format (archived_root_cnt , archived_root_cnt > 1 and " s" or " " ),
2522+ vim .log .levels .INFO
2523+ )
2524+ end )
2525+ end
2526+
2527+ return hunks
25032528end
25042529
25052530-- Helper function for handling cursor jumps after metadata operations
0 commit comments