diff options
| author | moxie <moxie@3kgcat.fi> | 2026-03-16 07:21:15 +0200 |
|---|---|---|
| committer | moxie <moxie@3kgcat.fi> | 2026-03-16 07:21:15 +0200 |
| commit | 98cbd80d5f56f3ce73e4819c3776ff5543accac3 (patch) | |
| tree | 2608134d92388b41fce667e1b4ab2827b11d0bc3 /lua/muwiki | |
| parent | 5ac2b3e08ca1cdaa3939e8f8446745925fb6a160 (diff) | |
feat: add todo list
Diffstat (limited to 'lua/muwiki')
| -rw-r--r-- | lua/muwiki/init.lua | 1 | ||||
| -rw-r--r-- | lua/muwiki/todo.lua | 136 |
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 |
