@@ -269,19 +269,18 @@ function M.highlight_todo_item(bufnr, todo_item, todo_map, opts)
269269 ctx .depth = ctx .depth + 1
270270
271271 local success , err = pcall (function ()
272- -- 1. Highlight the todo marker
273- M .highlight_todo_marker (bufnr , todo_item )
272+ -- row lookup for todo items for O(1) access
273+ local todo_rows = {}
274+ for _ , todo in pairs (todo_map ) do
275+ todo_rows [todo .range .start .row ] = true
276+ end
274277
275- -- 2. Highlight the list marker of the todo item
276- M .highlight_list_marker (bufnr , todo_item )
278+ M .highlight_todo_marker (bufnr , todo_item )
277279
278- -- 3. Highlight the child list markers within this todo item
279- M .highlight_child_list_markers (bufnr , todo_item )
280+ M .highlight_list_markers_in_range (bufnr , todo_item , todo_rows )
280281
281- -- 4. Highlight content directly in this todo item
282282 M .highlight_content (bufnr , todo_item , todo_map )
283283
284- -- 5. Show child count indicator
285284 M .show_todo_count_indicator (bufnr , todo_item , todo_map )
286285 end )
287286
@@ -370,20 +369,16 @@ function M.highlight_list_marker(bufnr, todo_item)
370369
371370 local start_row , start_col , _ , _ = list_marker .node :range ()
372371
373- local line = M . get_buffer_line ( bufnr , start_row )
374- if not line then
372+ local marker_text = list_marker . text
373+ if not marker_text then
375374 return
376375 end
377376
378- -- get actual marker text (-, *, +, or digits followed by . or ))
379- local marker_text = todo_item .list_marker .text
380- local marker_end_col = start_col + # marker_text
381-
382377 local hl_group = list_marker .type == " ordered" and " CheckmateListMarkerOrdered" or " CheckmateListMarkerUnordered"
383378
384379 vim .api .nvim_buf_set_extmark (bufnr , config .ns , start_row , start_col , {
385380 end_row = start_row , -- don't let it ever span multiple lines
386- end_col = marker_end_col , -- end exclusive (end col doesn't actually include the list marker)
381+ end_col = start_col + # marker_text , -- end exclusive (end col doesn't actually include the list marker)
387382 hl_group = hl_group ,
388383 priority = M .PRIORITY .LIST_MARKER ,
389384 right_gravity = false ,
@@ -392,7 +387,7 @@ function M.highlight_list_marker(bufnr, todo_item)
392387 })
393388end
394389
395- --- Finds and highlights all markdown list_markers within the todo item, excluding the
390+ --- Highlight all child list markers (non-todo list items) within this todo item's range
396391--- list_marker for the todo item itself (i.e. the first list_marker in the todo item's list_item node)
397392--- @param bufnr integer Buffer number
398393--- @param todo_item checkmate.TodoItem
@@ -404,28 +399,29 @@ function M.highlight_child_list_markers(bufnr, todo_item)
404399 return
405400 end
406401
407- local list_marker_query = parser .get_list_marker_query ( )
402+ local list_items = parser .get_all_list_items ( bufnr )
408403
409- for id , marker_node , _ in list_marker_query :iter_captures (todo_item .node , bufnr , 0 , - 1 ) do
410- local name = list_marker_query .captures [id ]
411- local marker_type = parser .get_marker_type_from_capture_name (name )
404+ for _ , list_item in ipairs (list_items ) do
405+ local item_start_row = list_item .range .start .row
412406
413- -- only process if it's not the todo item's own list marker
414- if not (todo_item .list_marker and todo_item .list_marker .node == marker_node ) then
415- local marker_start_row , marker_start_col , marker_end_row , _ = marker_node :range ()
416-
417- -- safety check: only highlight list markers within the parent todo's range
418- if marker_start_row >= todo_item .range .start .row and marker_end_row <= todo_item .range [" end" ].row then
419- local line = M .get_buffer_line (bufnr , marker_start_row )
420- if line then
421- local marker_text = todo_item .list_marker .text
407+ -- Skip if:
408+ -- 1. outside todo item's range
409+ -- 2. it's the todo item itself (same start row)
410+ -- 3. it's another todo item (has a todo state)
411+ if item_start_row > todo_item .range .start .row and item_start_row <= todo_item .range [" end" ].row then
412+ local line = M .get_buffer_line (bufnr , item_start_row )
413+ if line and not parser .get_todo_item_state (line ) then
414+ local marker = list_item .list_marker
415+ if marker and marker .node then
416+ local marker_text = list_item .text :match (" ^%s*([%-%*%+])" ) or list_item .text :match (" ^%s*(%d+[%.%)])" )
422417
423418 if marker_text then
424- local hl_group = marker_type == " ordered" and " CheckmateListMarkerOrdered" or " CheckmateListMarkerUnordered"
419+ local _ , marker_col = marker .node :range ()
420+ local hl_group = marker .type == " ordered" and " CheckmateListMarkerOrdered" or " CheckmateListMarkerUnordered"
425421
426- vim .api .nvim_buf_set_extmark (bufnr , config .ns , marker_start_row , marker_start_col , {
427- end_row = marker_start_row , -- never span lines
428- end_col = marker_start_col + # marker_text , -- end exclusive
422+ vim .api .nvim_buf_set_extmark (bufnr , config .ns , item_start_row , marker_col , {
423+ end_row = item_start_row ,
424+ end_col = marker_col + # marker_text ,
429425 hl_group = hl_group ,
430426 priority = M .PRIORITY .LIST_MARKER ,
431427 right_gravity = false ,
@@ -439,6 +435,64 @@ function M.highlight_child_list_markers(bufnr, todo_item)
439435 end
440436end
441437
438+ --- Highlight list markers for todo items and their child list items
439+ --- @param bufnr integer Buffer number
440+ --- @param todo_item checkmate.TodoItem
441+ --- @param todo_rows table<integer , checkmate.TodoItem> row to todo lookup table
442+ function M .highlight_list_markers_in_range (bufnr , todo_item , todo_rows )
443+ local config = require (" checkmate.config" )
444+ local parser = require (" checkmate.parser" )
445+
446+ -- highlight the todo item's own marker
447+ if todo_item .list_marker and todo_item .list_marker .node then
448+ local start_row , start_col = todo_item .list_marker .node :range ()
449+ local marker_text = todo_item .list_marker .text
450+
451+ if marker_text then
452+ local hl_group = todo_item .list_marker .type == " ordered" and " CheckmateListMarkerOrdered"
453+ or " CheckmateListMarkerUnordered"
454+
455+ vim .api .nvim_buf_set_extmark (bufnr , config .ns , start_row , start_col , {
456+ end_row = start_row ,
457+ end_col = start_col + # marker_text ,
458+ hl_group = hl_group ,
459+ priority = M .PRIORITY .LIST_MARKER ,
460+ right_gravity = false ,
461+ end_right_gravity = false ,
462+ hl_eol = false ,
463+ })
464+ end
465+ end
466+
467+ local query = parser .FULL_TODO_QUERY
468+
469+ for id , node in query :iter_captures (todo_item .node , bufnr ) do
470+ local capture_name = query .captures [id ]
471+
472+ if capture_name == " list_marker" or capture_name == " list_marker_ordered" then
473+ local marker_start_row , marker_start_col , _ , marker_end_col = node :range ()
474+
475+ -- skip if:
476+ -- - the todo item's own marker (same row)
477+ -- - not another todo item
478+ if marker_start_row > todo_item .range .start .row and not todo_rows [marker_start_row ] then
479+ local marker_type = parser .get_marker_type_from_capture_name (capture_name )
480+ local hl_group = marker_type == " ordered" and " CheckmateListMarkerOrdered" or " CheckmateListMarkerUnordered"
481+
482+ vim .api .nvim_buf_set_extmark (bufnr , config .ns , marker_start_row , marker_start_col , {
483+ end_row = marker_start_row ,
484+ end_col = marker_end_col - 1 , -- ts includes trailing space after list marker, we exclude it here
485+ hl_group = hl_group ,
486+ priority = M .PRIORITY .LIST_MARKER ,
487+ right_gravity = false ,
488+ end_right_gravity = false ,
489+ hl_eol = false ,
490+ })
491+ end
492+ end
493+ end
494+ end
495+
442496--- Applies highlight groups to metadata entries
443497--- @param bufnr integer Buffer number
444498--- @param config checkmate.Config.mod Configuration module
@@ -577,18 +631,32 @@ function M.highlight_content(bufnr, todo_item, todo_map)
577631 -- skip empty lines
578632 if row_line and not row_line :match (" ^%s*$" ) then
579633 local content_start = nil
580- for i = 0 , # row_line - 1 do
581- local char = row_line :sub (i + 1 , i + 1 )
582- if char ~= " " and char ~= " \t " then
583- content_start = i
584- break
634+
635+ local list_marker_pattern = " ^(%s*)[%-%*%+]%s+" -- unordered
636+ local ordered_pattern = " ^(%s*)%d+[%.%)]%s+" -- ordered
637+
638+ local _ , marker_end = row_line :find (list_marker_pattern )
639+ if not marker_end then
640+ _ , marker_end = row_line :find (ordered_pattern )
641+ end
642+
643+ if marker_end then
644+ content_start = marker_end -- already 0-based since find returns 1-based
645+ else
646+ -- no list marker, find first non-whitespace
647+ for i = 0 , # row_line - 1 do
648+ local char = row_line :sub (i + 1 , i + 1 )
649+ if char ~= " " and char ~= " \t " then
650+ content_start = i
651+ break
652+ end
585653 end
586654 end
587655
588- if content_start then
656+ if content_start and content_start < # row_line then
589657 vim .api .nvim_buf_set_extmark (bufnr , config .ns , row , content_start , {
590658 end_row = row ,
591- end_col = # row_line , -- byte length
659+ end_col = # row_line ,
592660 hl_group = additional_content_hl ,
593661 priority = M .PRIORITY .CONTENT ,
594662 hl_eol = false ,
0 commit comments