From 98cbd80d5f56f3ce73e4819c3776ff5543accac3 Mon Sep 17 00:00:00 2001 From: moxie Date: Mon, 16 Mar 2026 07:21:15 +0200 Subject: feat: add todo list --- README.md | 1 + doc/muwiki.txt | 21 ++++++++ doc/tags | 2 + lua/muwiki/init.lua | 1 + lua/muwiki/todo.lua | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 lua/muwiki/todo.lua diff --git a/README.md b/README.md index 6ec02b3..292529a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ See `:help muwiki-configuration` for all options. - `get_link()` - Get link info at cursor - `open_index(name)` - Open wiki index file - `open_with_menu(handlers)` - Open link with selectable handler +- `toggle_checkbox()` - Toggle checkbox and all children (useful for todo lists) See `:help muwiki-api` for detailed function documentation. diff --git a/doc/muwiki.txt b/doc/muwiki.txt index 26faa65..1b5fb24 100644 --- a/doc/muwiki.txt +++ b/doc/muwiki.txt @@ -13,6 +13,7 @@ CONTENTS *muwiki-contents* 7.1 Auto-create directories ............................. |muwiki-autocmd-mkdir| 7.2 Add template for new files .......................... |muwiki-autocmd-template| 7.3 Open with menu examples ............................. |muwiki-autocmd-open-with| + 7.4 Toggle checkbox ..................................... |muwiki-autocmd-toggle-checkbox| 8. Health Check ............................................ |muwiki-health| ============================================================================== @@ -173,6 +174,12 @@ muwiki.wiki_root({bufnr}) *muwiki.wiki_root()* Get the wiki root directory for a buffer. Returns the path or nil if buffer is not in a wiki. +muwiki.toggle_checkbox() *muwiki.toggle_checkbox()* + Toggle the checkbox at cursor position using treesitter. + Also toggles all nested child checkboxes to match the + parent's new state. Works when cursor is anywhere on + the line. + ============================================================================== 7. AUTOCOMMANDS & RECIPES *muwiki-autocmd* @@ -270,6 +277,20 @@ Custom open handler using get_link()~ *muwiki-a }) < +Toggle checkbox with Shift+T~ *muwiki-autocmd-toggle-checkbox* +> + vim.api.nvim_create_autocmd("FileType", { + pattern = "markdown", + callback = function(args) + if not muwiki.wiki_root(args.buf) then return end + vim.keymap.set('n', '', muwiki.toggle_checkbox, + { buffer = args.buf, desc = "Toggle checkbox" }) + end, + }) +< +This will toggle the checkbox at cursor position and all nested child +checkboxes to match the parent's new state. + ============================================================================== 8. HEALTH CHECK *muwiki-health* diff --git a/doc/tags b/doc/tags index 5f52c12..811c082 100644 --- a/doc/tags +++ b/doc/tags @@ -25,3 +25,5 @@ muwiki.open_with_menu() muwiki.txt /*muwiki.open_with_menu()* muwiki.prev_link() muwiki.txt /*muwiki.prev_link()* muwiki.setup() muwiki.txt /*muwiki.setup()* muwiki.wiki_root() muwiki.txt /*muwiki.wiki_root()* +muwiki.toggle_checkbox() muwiki.txt /*muwiki.toggle_checkbox()* +muwiki-autocmd-toggle-checkbox muwiki.txt /*muwiki-autocmd-toggle-checkbox* 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 -- cgit v1.2.3