diff --git a/lua/todoer/init.lua b/lua/todoer/init.lua index 7b16a19..92ef726 100644 --- a/lua/todoer/init.lua +++ b/lua/todoer/init.lua @@ -6,137 +6,193 @@ local defaults = { filetypes = { "markdown", "text", "norg" }, -- Filetypes to activate the plugin for } +-- 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 +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) + if not lines or #lines == 0 then return nil end -- Handle potential errors getting line + local line = lines[1] + + local indent = string.match(line, "^%s*") or "" + -- Pattern captures marker '-', '*', or '+' followed by at least one space + -- Returns: indent, marker, space_after_marker + local marker_pattern = "^(%s*)([%-%*%+])(%s+)" + -- Pattern captures marker, checkbox '[ ]' or '[x]', and optional space after checkbox + local todo_pattern = "^%s*[%-%*%+]%s+%[([ x])%]%s*" + + local _, marker_match, _ = string.match(line, marker_pattern) + local is_todo = (string.match(line, todo_pattern) ~= nil) + local content = "" + + if is_todo then + -- Content is everything after "[x] " or "[ ] " (including leading space) + content = string.match(line, todo_pattern .. "(.*)") or "" + elseif marker_match then + -- Content is everything after "- " / "* " / "+ " + content = string.match(line, marker_pattern .. "(.*)") or "" + else + -- No list marker, just the trimmed line content + content = vim.trim(line) + end + + return { + line = line, + indent = indent, + marker = marker_match, -- '-', '*', '+', or nil + content = content, + is_todo = is_todo, + } +end + +-- Helper: Gets details for the current line +local function get_current_line_details() + 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 +end + +-- ti: Convert line to TODO, cursor after "] " (Normal mode) +local function add_todo_insert_mode() + local details, lnum = get_current_line_details() + -- Don't convert if already a TODO or if getting details failed + if not details or details.is_todo then return end + + local use_marker = details.marker or "-" -- Default to '-' if no list marker found + local text_content = details.content -- Content already extracted correctly by get_line_details + + local new_line = details.indent .. use_marker .. " [ ] " .. text_content + -- Replace current line (lnum is 1-based, set_lines is 0-based) + vim.api.nvim_buf_set_lines(0, lnum - 1, lnum, false, { new_line }) + + -- Set cursor position: 1-based row, 0-based byte column + -- Position cursor right after the "] " + local cursor_col_bytes = vim.fn.strbytes(details.indent .. use_marker .. " [ ] ") + vim.api.nvim_win_set_cursor(0, { lnum, cursor_col_bytes }) +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() + if not details or details.is_todo then return end + + local use_marker = details.marker or "-" + local text_content = details.content + + local new_line = details.indent .. use_marker .. " [ ] " .. text_content + vim.api.nvim_buf_set_lines(0, lnum - 1, lnum, false, { new_line }) + + -- Set cursor position: 1-based row, 0-based byte column + -- Position cursor at the very end of the line content + local cursor_col_bytes = vim.fn.strbytes(new_line) + vim.api.nvim_win_set_cursor(0, { lnum, cursor_col_bytes }) +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() + 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 + local new_indent = details.indent + local new_marker = details.marker or "-" + + local new_line_content = new_indent .. new_marker .. " [ ] " + -- Insert after current line (lnum is 1-based, set_lines is 0-based for ranges) + vim.api.nvim_buf_set_lines(0, lnum, lnum, false, { new_line_content }) + + -- Set cursor position: 1-based row (lnum + 1), 0-based byte column + -- Position cursor at the end of the new line (after "] ") + local cursor_col_bytes = vim.fn.strbytes(new_line_content) + vim.api.nvim_win_set_cursor(0, { lnum + 1, cursor_col_bytes }) +end + -- Internal function to set up buffer-local keymaps local function setup_buffer_keymaps() - -- add new todo line when previous is already a todo + -- add new todo line when previous is already a todo (Will be refactored in Step 3) local function press_enter() local current_line = vim.api.nvim_get_current_line() - local index, spaces = string.find(current_line, "^%s*") - - -- Check if the current line matches the pattern - local pattern = "^%- %[[ x]%] .*$" - local match = string.match(current_line, pattern) - - if match then - if string.len(current_line) >= 7 then - vim.api.nvim_feedkeys("\n- [ ] ", "n", true) - else - -- go to normal mode before removing the content of the line - vim.api.nvim_feedkeys(vim.api.nvim_eval('"\\"'), "n", true) - vim.api.nvim_feedkeys("_", "n", true) - vim.api.nvim_feedkeys("C", "n", true) - vim.api.nvim_feedkeys("\n", "n", true) - -- vim.api.nvim_set_current_line("") - end - else - vim.api.nvim_feedkeys("\n", "n", true) + -- Check if the current line matches the pattern (Only checks '-' marker currently) + local pattern = "^%s*%- %[[ x]%]%s*$" -- Simplified pattern to check if it's an empty-ish TODO line + local content_pattern = "^%s*%- %[[ x]%]%s+(.+)" -- Pattern to check if there's content + + if string.match(current_line, content_pattern) then -- If it's a TODO with content + local indent = string.match(current_line, "^%s*") or "" + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(""..indent.."- [ ] ", true, false, true), "n", false) + elseif string.match(current_line, pattern) then -- If it's an empty TODO line + -- Current behavior: clear line and insert newline (will change in Step 3) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("S", true, false, true), "n", false) + else -- Not a TODO line + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false) end end - -- indent line if tab is pressed when line is a todo + -- indent line if tab is pressed when line is a todo (Will be refactored in Step 4) local function press_tab() local current_line = vim.api.nvim_get_current_line() - - -- Check if current line matches the patterns - local openpattern = "%- %[[ ]%]" - local closedpattern = "%- %[[x]%]" - - if string.match(current_line, openpattern) or string.match(current_line, closedpattern) then - print("tab pressed, allegedly") - vim.api.nvim_feedkeys("C-t", "i", true) - end + -- Check if current line matches the patterns (Only checks '-' marker currently) + local pattern = "^%s*%- %[[ x]%]" + if string.match(current_line, pattern) then + -- print("tab pressed, allegedly") -- Removed print + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "i", false) + else + -- Return to allow default behavior if expr=true is set on mapping + return vim.api.nvim_replace_termcodes("", true, false, true) + end end - -- indent line if shift tab is pressed when line is a todo + -- indent line if shift tab is pressed when line is a todo (Will be refactored in Step 4) local function press_shift_tab() local current_line = vim.api.nvim_get_current_line() - - -- Check if current line matches the patterns - local openpattern = "%- %[[ ]%]" - local closedpattern = "%- %[[x]%]" - - if string.match(current_line, openpattern) or string.match(current_line, closedpattern) then - print("shift tab pressed, allegedly") - vim.api.nvim_feedkeys("C-d", "i", true) - end + -- Check if current line matches the patterns (Only checks '-' marker currently) + local pattern = "^%s*%- %[[ x]%]" + if string.match(current_line, pattern) then + -- print("shift tab pressed, allegedly") -- Removed print + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "i", false) + else + -- Return to allow default behavior if expr=true is set on mapping + return vim.api.nvim_replace_termcodes("", true, false, true) + end end -- function that checks if the current line starts with the string "- [ ]" or "- [x]" and toggles the x + -- (Will be updated in Step 2 to handle *, +) local function toggle_todo() local openpattern = "%- %[[ ]%]" local closedpattern = "%- %[[x]%]" - local titleopenpattern = "# %[[ ]%]" + local titleopenpattern = "# %[[ ]%]" -- Keep title toggle for now? Or remove? Let's keep it. local titleclosedpattern = "# %[[x]%]" local line = vim.api.nvim_get_current_line() if string.match(line, openpattern) then - line = string.gsub(line, openpattern, "- [x]") + line = string.gsub(line, openpattern, "- [x]", 1) -- Replace only first instance vim.api.nvim_set_current_line(line) elseif string.match(line, closedpattern) then - line = string.gsub(line, closedpattern, "- [ ]") + line = string.gsub(line, closedpattern, "- [ ]", 1) -- Replace only first instance vim.api.nvim_set_current_line(line) elseif string.match(line, titleopenpattern) then - line = string.gsub(line, titleopenpattern, "# [x]") + line = string.gsub(line, titleopenpattern, "# [x]", 1) vim.api.nvim_set_current_line(line) elseif string.match(line, titleclosedpattern) then - line = string.gsub(line, titleclosedpattern, "# [ ]") - vim.api.nvim_set_current_line(line) - end - end - - -- function that checks if the current line doesn't start with the string "- [ ] " and, if it doesn't, adds the string at the beginning of the line - local function add_todo() - local openpattern = "^%s*%- %[[ ]%]" - local closedpattern = "^%s*%- %[[x]%]" - - local line = vim.api.nvim_get_current_line() - local index, spaces = string.find(line, "^%s*") - - if index == 1 and not string.match(line, openpattern) and not string.match(line, closedpattern) then - if index == 1 and spaces == 0 then - line = "- [ ] " .. line - vim.api.nvim_set_current_line(line) - vim.api.nvim_feedkeys("A", "n", true) - elseif index == 1 and spaces > 0 then - local trimedline = string.sub(line, spaces + 1) - local spacestring = "" - for x = 1, spaces, 1 do - spacestring = spacestring .. " " - end - line = spacestring .. "- [ ] " .. trimedline - vim.api.nvim_set_current_line(line) - end - end - end - - -- function that checks if the current line starts with the string "- [ ]" or "- [x]" and, if it does, removes that string from the line - local function remove_todo() - local openpattern = "%- %[[ ]%]" - local closedpattern = "%- %[[x]%]" - local line = vim.api.nvim_get_current_line() - -- local index, spaces = string.find(line, "^%s*") - - if string.match(line, openpattern) or string.match(line, closedpattern) then - print("encontrado") - line = string.gsub(line, openpattern, "") - line = string.gsub(line, closedpattern, "") + line = string.gsub(line, titleclosedpattern, "# [ ]", 1) vim.api.nvim_set_current_line(line) end end -- Set up buffer-local keymaps -- Use buffer = 0 to target the current buffer - local map_opts = { noremap = true, silent = true, buffer = 0 } - local expr_map_opts = { noremap = true, expr = true, silent = true, buffer = 0 } - - vim.keymap.set("n", "t", function() end, { desc = "+TODOs", buffer = 0 }) -- Placeholder, keep silent=false if needed - vim.keymap.set("i", "", press_enter, { desc = "Todoer: Handle Enter", expr = true, buffer = 0 }) -- Keep expr=true, noremap=true is default - vim.keymap.set("i", "", press_tab, { desc = "Todoer: Handle Tab", expr = true, buffer = 0 }) -- Keep expr=true - vim.keymap.set("i", "", press_shift_tab, { desc = "Todoer: Handle Shift-Tab", expr = true, buffer = 0 }) -- Keep expr=true + -- Note: Setting expr = true for Tab/S-Tab allows returning keys for default behavior + vim.keymap.set("n", "t", function() end, { desc = "+TODOs", buffer = 0 }) -- Placeholder + vim.keymap.set("i", "", press_enter, { desc = "Todoer: Handle Enter", expr = true, buffer = 0 }) + vim.keymap.set("i", "", press_tab, { desc = "Todoer: Handle Tab", expr = true, buffer = 0 }) + vim.keymap.set("i", "", press_shift_tab, { desc = "Todoer: Handle Shift-Tab", expr = true, buffer = 0 }) vim.keymap.set("n", "tt", toggle_todo, { desc = "Todoer: Toggle TODO", buffer = 0 }) - vim.keymap.set("n", "ta", add_todo, { desc = "Todoer: Add TODO", buffer = 0 }) - vim.keymap.set("n", "td", remove_todo, { desc = "Todoer: Remove TODO", buffer = 0 }) + -- New mappings for adding TODOs + 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 }) -- Optional: Notify that keymaps are set for this buffer -- vim.notify("Todoer keymaps activated for this buffer", vim.log.levels.INFO)