|  |  | @ -7,7 +7,7 @@ local defaults = { | 
			
		
	
		
		
			
				
					
					|  |  |  | } |  |  |  | } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | -- Helper: Parses line details for a given line number (1-based) |  |  |  | -- 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) |  |  |  | local function get_line_details(lnum) | 
			
		
	
		
		
			
				
					
					|  |  |  |   -- Use nvim_buf_get_lines which is 0-indexed for ranges |  |  |  |   -- Use nvim_buf_get_lines which is 0-indexed for ranges | 
			
		
	
		
		
			
				
					
					|  |  |  |   local lines = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false) |  |  |  |   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" |  |  |  |   -- Matches lines like: "- [ ] task", "* [x] task", "+ [ ] task" | 
			
		
	
		
		
			
				
					
					|  |  |  |   local todo_pattern = "^(%s*[%-%*%+]%s+)%[([ x])%]%s*" -- Capture prefix before checkbox |  |  |  |   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 is_todo = (string.match(line, todo_pattern) ~= nil) | 
			
		
	
		
		
			
				
					
					|  |  |  |   local content = "" |  |  |  |   local content = "" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   local prefix_len = 0 -- Length of the string before the content starts | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   if is_todo then |  |  |  |   if is_todo then | 
			
		
	
		
		
			
				
					
					|  |  |  |     -- Content is everything after "[x] " or "[ ] " (including leading space) |  |  |  |     -- 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 "" |  |  |  |     content = string.match(line, todo_pattern .. "(.*)") or "" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     prefix_len = #todo_prefix | 
			
		
	
		
		
			
				
					
					|  |  |  |   elseif marker_match then |  |  |  |   elseif marker_match then | 
			
		
	
		
		
			
				
					
					|  |  |  |     -- Content is everything after "- " / "* " / "+ " |  |  |  |     -- Content is everything after "- " / "* " / "+ " | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     local list_prefix = prefix_match .. marker_match .. space_match -- The part like "- " | 
			
		
	
		
		
			
				
					
					|  |  |  |     content = string.match(line, marker_pattern .. "(.*)") or "" |  |  |  |     content = string.match(line, marker_pattern .. "(.*)") or "" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     prefix_len = #list_prefix | 
			
		
	
		
		
			
				
					
					|  |  |  |   else |  |  |  |   else | 
			
		
	
		
		
			
				
					
					|  |  |  |     -- No list marker, just the trimmed line content |  |  |  |     -- No list marker, just the trimmed line content | 
			
		
	
		
		
			
				
					
					|  |  |  |     content = vim.trim(line) |  |  |  |     content = vim.trim(line) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     prefix_len = #indent -- Length before the trimmed content | 
			
		
	
		
		
			
				
					
					|  |  |  |   end |  |  |  |   end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   return { |  |  |  |   return { | 
			
		
	
	
		
		
			
				
					|  |  | @ -43,19 +49,21 @@ local function get_line_details(lnum) | 
			
		
	
		
		
			
				
					
					|  |  |  |     marker = marker_match, -- '-', '*', '+', or nil |  |  |  |     marker = marker_match, -- '-', '*', '+', or nil | 
			
		
	
		
		
			
				
					
					|  |  |  |     content = content, |  |  |  |     content = content, | 
			
		
	
		
		
			
				
					
					|  |  |  |     is_todo = is_todo, |  |  |  |     is_todo = is_todo, | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     prefix_len = prefix_len, -- Store the calculated prefix length | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | end |  |  |  | end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | -- Helper: Gets details for the current line |  |  |  | -- Helper: Gets details for the current line including cursor position | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | local function get_current_line_details() |  |  |  | local function get_current_line_details_with_cursor() | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     local cursor_pos = vim.api.nvim_win_get_cursor(0) |  |  |  |     local cursor_pos = vim.api.nvim_win_get_cursor(0) | 
			
		
	
		
		
			
				
					
					|  |  |  |     local lnum = cursor_pos[1] -- 1-based line number |  |  |  |     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 |  |  |  | end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | -- <leader>ti: Convert line to TODO, cursor after "] " (Normal mode) |  |  |  | -- <leader>ti: Convert line to TODO, cursor after "] " (Normal mode) | 
			
		
	
		
		
			
				
					
					|  |  |  | local function add_todo_insert_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 |  |  |  |     -- Don't convert if already a TODO or if getting details failed | 
			
		
	
		
		
			
				
					
					|  |  |  |     if not details or details.is_todo then return end |  |  |  |     if not details or details.is_todo then return end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -74,7 +82,7 @@ end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | -- <leader>ta: Convert line to TODO, cursor at end of line (Normal mode) |  |  |  | -- <leader>ta: Convert line to TODO, cursor at end of line (Normal mode) | 
			
		
	
		
		
			
				
					
					|  |  |  | local function add_todo_append_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 |  |  |  |     if not details or details.is_todo then return end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     local use_marker = details.marker or "-" |  |  |  |     local use_marker = details.marker or "-" | 
			
		
	
	
		
		
			
				
					|  |  | @ -92,7 +100,7 @@ end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | -- <leader>to: Insert new TODO line below, cursor after "] " (Normal mode) |  |  |  | -- <leader>to: Insert new TODO line below, cursor after "] " (Normal mode) | 
			
		
	
		
		
			
				
					
					|  |  |  | local function add_todo_new_line_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 |  |  |  |     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 |  |  |  |     -- 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 }) |  |  |  |     vim.api.nvim_win_set_cursor(0, { lnum + 1, cursor_col_bytes }) | 
			
		
	
		
		
			
				
					
					|  |  |  | end |  |  |  | end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | -- <leader>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 |  |  |  | -- Internal function to set up buffer-local keymaps | 
			
		
	
		
		
			
				
					
					|  |  |  | local function setup_buffer_keymaps() |  |  |  | local function setup_buffer_keymaps() | 
			
		
	
		
		
			
				
					
					|  |  |  | 	-- Handles Enter key press in Insert mode |  |  |  | 	-- Handles Enter key press in Insert mode | 
			
		
	
		
		
			
				
					
					|  |  |  | 	local function press_enter() |  |  |  | 	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 |  |  |  |         if not details then | 
			
		
	
		
		
			
				
					
					|  |  |  |             -- Getting details failed, do nothing and let Neovim handle Enter. |  |  |  |             -- Getting details failed, do nothing and let Neovim handle Enter. | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
	
		
		
			
				
					|  |  | @ -259,6 +306,9 @@ local function setup_buffer_keymaps() | 
			
		
	
		
		
			
				
					
					|  |  |  | 	vim.keymap.set("n", "<leader>ti", add_todo_insert_mode, { desc = "Todoer: Add TODO (cursor after marker)", buffer = 0, silent = true }) |  |  |  | 	vim.keymap.set("n", "<leader>ti", add_todo_insert_mode, { desc = "Todoer: Add TODO (cursor after marker)", buffer = 0, silent = true }) | 
			
		
	
		
		
			
				
					
					|  |  |  | 	vim.keymap.set("n", "<leader>ta", add_todo_append_mode, { desc = "Todoer: Add TODO (cursor at end)", buffer = 0, silent = true }) |  |  |  | 	vim.keymap.set("n", "<leader>ta", add_todo_append_mode, { desc = "Todoer: Add TODO (cursor at end)", buffer = 0, silent = true }) | 
			
		
	
		
		
			
				
					
					|  |  |  | 	vim.keymap.set("n", "<leader>to", add_todo_new_line_mode, { desc = "Todoer: Add new TODO line below", buffer = 0, silent = true }) |  |  |  | 	vim.keymap.set("n", "<leader>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", "<leader>td", remove_todo_markup, { desc = "Todoer: Remove TODO markup", buffer = 0, silent = true }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 	-- Optional: Notify that keymaps are set for this buffer |  |  |  | 	-- Optional: Notify that keymaps are set for this buffer | 
			
		
	
		
		
			
				
					
					|  |  |  | 	-- vim.notify("Todoer keymaps activated for this buffer", vim.log.levels.INFO) |  |  |  | 	-- vim.notify("Todoer keymaps activated for this buffer", vim.log.levels.INFO) | 
			
		
	
	
		
		
			
				
					|  |  | 
 |