aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--doc/muwiki.txt21
-rw-r--r--doc/tags2
-rw-r--r--lua/muwiki/init.lua1
-rw-r--r--lua/muwiki/todo.lua136
5 files changed, 161 insertions, 0 deletions
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', '<S-t>', 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