Skip to content

Commit ba6132b

Browse files
committed
fix: refactored and fixed archive system (#187)
Converts public `archive` to use transaction. Complete refactor to internal `archive_todos`.
1 parent d8f4c59 commit ba6132b

File tree

5 files changed

+324
-235
lines changed

5 files changed

+324
-235
lines changed

lua/checkmate/api.lua

Lines changed: 152 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,18 +2244,21 @@ function M.count_child_todos(todo_item, todo_map, opts)
22442244
end
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
25032528
end
25042529

25052530
-- Helper function for handling cursor jumps after metadata operations

lua/checkmate/debug/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function M.at_cursor()
8282

8383
vim.notify(table.concat(msg, "\n"), vim.log.levels.DEBUG)
8484

85-
M.debug.highlight(item.range)
85+
M.highlight(item.range)
8686
end
8787

8888
--- Print todo map (in Snacks scratch buffer or vim.print)

0 commit comments

Comments
 (0)