1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
local M = {}
---Toggle checkbox at cursor position and all children
-- Uses treesitter to find markdown list items with checkboxes
function M.toggle_checkbox()
local bufnr = vim.api.nvim_get_current_buf()
local cursor = vim.api.nvim_win_get_cursor(0)
local cursor_row = cursor[1] - 1 -- 0-indexed
local cursor_col = cursor[2]
-- Get markdown parser
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'markdown')
if not ok or not parser then
vim.notify('Markdown parser not available', vim.log.levels.WARN)
return
end
-- Parse and get tree
local trees = parser:parse()
if not trees or #trees == 0 then
return
end
local tree = trees[1]
local root = tree:root()
-- Find list_item node at cursor position
local target_node = nil
for node in root:iter_children() do
if node:range() then
local start_row, start_col, end_row, end_col = node:range()
-- end_row is exclusive, so use < not <=
if cursor_row >= start_row and cursor_row < end_row then
-- Check if this is a list_item or contains one
target_node = M.find_list_item_at(node, cursor_row, cursor_col)
if target_node then
break
end
end
end
end
if not target_node then
return
end
-- Toggle the target and its children
local new_state = M.toggle_list_item(bufnr, target_node)
if new_state then
M.toggle_children(bufnr, target_node, new_state)
end
end
---Find list_item node containing cursor position
-- Searches children first to return the deepest matching node
function M.find_list_item_at(node, cursor_row, cursor_col)
-- Check children first to find the deepest match
for child in node:iter_children() do
local result = M.find_list_item_at(child, cursor_row, cursor_col)
if result then
return result
end
end
-- Then check this node
if node:type() == 'list_item' then
local start_row, start_col, end_row, end_col = node:range()
-- end_row is exclusive, so use < not <=
if cursor_row >= start_row and cursor_row < end_row then
return node
end
end
return nil
end
---Toggle a single list item's checkbox
-- Returns the new state ('x' or ' ') or nil if not a task
function M.toggle_list_item(bufnr, node)
-- Find task_list_marker
for child in node:iter_children() do
local child_type = child:type()
if child_type == 'task_list_marker_unchecked' then
-- Replace [ ] with [X]
local start_row, start_col, end_row, end_col = child:range()
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[X]' })
return 'X'
elseif child_type == 'task_list_marker_checked' then
-- Replace [X] with [ ]
local start_row, start_col, end_row, end_col = child:range()
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[ ]' })
return ' '
end
end
return nil
end
---Toggle all child list items to match parent state
function M.toggle_children(bufnr, parent_node, target_state)
-- Find nested list
for child in parent_node:iter_children() do
if child:type() == 'list' then
-- Iterate through list_items in the nested list
for list_item in child:iter_children() do
if list_item:type() == 'list_item' then
-- Force this child to target state
M.force_list_item_state(bufnr, list_item, target_state)
-- Recursively handle grandchildren
M.toggle_children(bufnr, list_item, target_state)
end
end
break
end
end
end
---Force a list item to specific state without toggling
function M.force_list_item_state(bufnr, node, target_state)
for child in node:iter_children() do
local child_type = child:type()
if child_type == 'task_list_marker_unchecked' and target_state == 'X' then
-- Need to check it
local start_row, start_col, end_row, end_col = child:range()
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[X]' })
return
elseif child_type == 'task_list_marker_checked' and target_state == ' ' then
-- Need to uncheck it
local start_row, start_col, end_row, end_col = child:range()
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[ ]' })
return
end
end
end
return M
|