diff --git a/lua/todoer/init.lua b/lua/todoer/init.lua index 2d0815a..42473ef 100644 --- a/lua/todoer/init.lua +++ b/lua/todoer/init.lua @@ -7,7 +7,7 @@ local defaults = { } -- Helper: Parses line details for a given line number (1-based) --- Returns table: { line = str, indent = str, marker = str|nil, content = str, is_todo = bool } or nil +-- Returns table: { line = str, indent = str, marker = str|nil, content = str, is_todo = bool, prefix_len = number } or nil local function get_line_details(lnum) -- Use nvim_buf_get_lines which is 0-indexed for ranges local lines = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false) @@ -22,19 +22,25 @@ local function get_line_details(lnum) -- Matches lines like: "- [ ] task", "* [x] task", "+ [ ] task" local todo_pattern = "^(%s*[%-%*%+]%s+)%[([ x])%]%s*" -- Capture prefix before checkbox - local _, marker_match, _ = string.match(line, marker_pattern) + local prefix_match, marker_match, space_match = string.match(line, marker_pattern) local is_todo = (string.match(line, todo_pattern) ~= nil) local content = "" + local prefix_len = 0 -- Length of the string before the content starts if is_todo then -- Content is everything after "[x] " or "[ ] " (including leading space) + local todo_prefix = string.match(line, todo_pattern) -- The part like "- [ ] " content = string.match(line, todo_pattern .. "(.*)") or "" + prefix_len = #todo_prefix elseif marker_match then -- Content is everything after "- " / "* " / "+ " + local list_prefix = prefix_match .. marker_match .. space_match -- The part like "- " content = string.match(line, marker_pattern .. "(.*)") or "" + prefix_len = #list_prefix else -- No list marker, just the trimmed line content content = vim.trim(line) + prefix_len = #indent -- Length before the trimmed content end return { @@ -43,19 +49,21 @@ local function get_line_details(lnum) marker = marker_match, -- '-', '*', '+', or nil content = content, is_todo = is_todo, + prefix_len = prefix_len, -- Store the calculated prefix length } end --- Helper: Gets details for the current line -local function get_current_line_details() +-- Helper: Gets details for the current line including cursor position +local function get_current_line_details_with_cursor() local cursor_pos = vim.api.nvim_win_get_cursor(0) local lnum = cursor_pos[1] -- 1-based line number - return get_line_details(lnum), lnum + local col = cursor_pos[2] -- 0-based byte column + return get_line_details(lnum), lnum, col end -- ti: Convert line to TODO, cursor after "] " (Normal mode) local function add_todo_insert_mode() - local details, lnum = get_current_line_details() + local details, lnum = get_current_line_details_with_cursor() -- Use basic helper is fine -- Don't convert if already a TODO or if getting details failed if not details or details.is_todo then return end @@ -74,7 +82,7 @@ end -- ta: Convert line to TODO, cursor at end of line (Normal mode) local function add_todo_append_mode() - local details, lnum = get_current_line_details() + local details, lnum = get_current_line_details_with_cursor() -- Use basic helper is fine if not details or details.is_todo then return end local use_marker = details.marker or "-" @@ -92,7 +100,7 @@ end -- to: Insert new TODO line below, cursor after "] " (Normal mode) local function add_todo_new_line_mode() - local details, lnum = get_current_line_details() + local details, lnum = get_current_line_details_with_cursor() -- Use basic helper is fine if not details then return end -- Should not happen usually, but check anyway -- Determine indent and marker based on current line, default to '-' if no marker @@ -109,11 +117,50 @@ local function add_todo_new_line_mode() vim.api.nvim_win_set_cursor(0, { lnum + 1, cursor_col_bytes }) end +-- td: Remove TODO markup from the current line (Normal mode) +local function remove_todo_markup() + local details, lnum, original_col = get_current_line_details_with_cursor() + if not details or not details.is_todo then return end -- Only act on TODO lines + + -- Construct the new line without the TODO markup "[ ] " or "[x] " + local new_line = details.indent .. details.marker .. " " .. details.content + + -- Calculate the position where the markup started + -- This is the length of the indent + marker + one space + local markup_start_col = #(details.indent .. details.marker .. " ") + + -- The markup itself is 4 characters: '[', ' ' or 'x', ']', ' ' + local markup_len = 4 + + -- Update the line content + vim.api.nvim_buf_set_lines(0, lnum - 1, lnum, false, { new_line }) + + -- Calculate the new cursor column + local new_col + if original_col < markup_start_col then + -- Cursor was before the markup, keep its position + new_col = original_col + elseif original_col >= markup_start_col and original_col < markup_start_col + markup_len then + -- Cursor was inside the markup, move it to the start of where the markup was + new_col = markup_start_col + else + -- Cursor was after the markup, shift it left by the length of the removed markup + new_col = original_col - markup_len + end + + -- Ensure the column is not negative (shouldn't happen here, but good practice) + new_col = math.max(0, new_col) + + -- Set the new cursor position + vim.api.nvim_win_set_cursor(0, { lnum, new_col }) +end + + -- Internal function to set up buffer-local keymaps local function setup_buffer_keymaps() -- Handles Enter key press in Insert mode local function press_enter() - local details, lnum = get_current_line_details() + local details, lnum = get_current_line_details_with_cursor() -- Use basic helper is fine if not details then -- Getting details failed, do nothing and let Neovim handle Enter. return @@ -259,6 +306,9 @@ local function setup_buffer_keymaps() vim.keymap.set("n", "ti", add_todo_insert_mode, { desc = "Todoer: Add TODO (cursor after marker)", buffer = 0, silent = true }) vim.keymap.set("n", "ta", add_todo_append_mode, { desc = "Todoer: Add TODO (cursor at end)", buffer = 0, silent = true }) vim.keymap.set("n", "to", add_todo_new_line_mode, { desc = "Todoer: Add new TODO line below", buffer = 0, silent = true }) + -- Mapping for removing TODO markup + vim.keymap.set("n", "td", remove_todo_markup, { desc = "Todoer: Remove TODO markup", buffer = 0, silent = true }) + -- Optional: Notify that keymaps are set for this buffer -- vim.notify("Todoer keymaps activated for this buffer", vim.log.levels.INFO)