aboutsummaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authormoxie <moxie@3kgcat.fi>2026-03-16 07:21:15 +0200
committermoxie <moxie@3kgcat.fi>2026-03-16 07:21:15 +0200
commit98cbd80d5f56f3ce73e4819c3776ff5543accac3 (patch)
tree2608134d92388b41fce667e1b4ab2827b11d0bc3 /lua
parent5ac2b3e08ca1cdaa3939e8f8446745925fb6a160 (diff)
feat: add todo list
Diffstat (limited to 'lua')
-rw-r--r--lua/muwiki/init.lua1
-rw-r--r--lua/muwiki/todo.lua136
2 files changed, 137 insertions, 0 deletions
diff --git a/lua/muwiki/init.lua b/lua/muwiki/init.lua
index f9d8f23..f9f7187 100644
--- a/lua/muwiki/init.lua
+++ b/lua/muwiki/init.lua
@@ -12,5 +12,6 @@ M.get_link = function() return require('muwiki.links').get_link() end
M.open_with_menu = function(handlers, link) require('muwiki.links').open_with_menu(handlers, link) end
M.open_index = function(name) require('muwiki.utils').open_index(name) end
M.wiki_root = function(bufnr) return require('muwiki.utils').wiki_root(bufnr) end
+M.toggle_checkbox = function() require('muwiki.todo').toggle_checkbox() end
return M
diff --git a/lua/muwiki/todo.lua b/lua/muwiki/todo.lua
new file mode 100644
index 0000000..6978c54
--- /dev/null
+++ b/lua/muwiki/todo.lua
@@ -0,0 +1,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