You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
11 KiB
Lua

-- Create the module table
local M = {}
-- Default configuration
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
-- 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 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
-- <leader>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 = #(details.indent .. use_marker .. " [ ] ") -- Use Lua # operator
vim.api.nvim_win_set_cursor(0, { lnum, cursor_col_bytes })
end
-- <leader>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 = #new_line -- Use Lua # operator
vim.api.nvim_win_set_cursor(0, { lnum, cursor_col_bytes })
end
-- <leader>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 = #new_line_content -- Use Lua # operator
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()
-- Handles Enter key press in Insert mode
local function press_enter()
local details, lnum = get_current_line_details()
if not details then
-- Fallback if getting details fails for some reason
return vim.api.nvim_replace_termcodes("<CR>", true, false, true)
end
-- Check if the line is a TODO item and if it has content after the marker
-- Use string.match with %S to check for any non-whitespace character
local has_content = details.content and string.match(details.content, "%S")
if details.is_todo and has_content then
-- Case 1: Non-empty TODO line
-- Create a new TODO line below with same indent/marker
local new_marker = details.marker or "-" -- Should always have marker if is_todo, but safety check
local new_line_content = details.indent .. new_marker .. " [ ] "
-- Insert the new line below the current one
vim.api.nvim_buf_set_lines(0, lnum, lnum, false, { new_line_content })
-- Move cursor to the new line, after the "] "
local cursor_col_bytes = #new_line_content
vim.api.nvim_win_set_cursor(0, { lnum + 1, cursor_col_bytes })
-- Action handled, return empty string because mapping is expr=true
return ""
elseif details.is_todo and not has_content then
-- Case 2: Empty TODO line (e.g., "- [ ]")
-- TODO: Implement Step 3 logic (outdenting) here later.
-- For now, keep the old (potentially problematic) feedkeys behavior
-- or simply clear the line and let Neovim handle Enter.
-- Let's try clearing the line and returning <CR> for now.
vim.api.nvim_buf_set_lines(0, lnum - 1, lnum, false, { details.indent }) -- Replace with just indent
return vim.api.nvim_replace_termcodes("<CR>", true, false, true) -- Let Neovim handle newline
-- Old feedkeys behavior (kept for reference, but commented out):
-- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>S<CR>", true, false, true), "n", false)
-- return "" -- Feedkeys handles it
else
-- Case 3: Not a TODO line
-- Let Neovim handle the Enter key press normally
return vim.api.nvim_replace_termcodes("<CR>", true, false, true)
end
end
-- 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 (Only checks '-' marker currently)
-- TODO: Update pattern to match *, + as well
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("<C-t>", true, false, true), "i", false)
return "" -- Action handled by feedkeys
else
-- Return <Tab> to allow default behavior
return vim.api.nvim_replace_termcodes("<Tab>", true, false, true)
end
end
-- 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 (Only checks '-' marker currently)
-- TODO: Update pattern to match *, + as well
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("<C-d>", true, false, true), "i", false)
return "" -- Action handled by feedkeys
else
-- Return <S-Tab> to allow default behavior
return vim.api.nvim_replace_termcodes("<S-Tab>", true, false, true)
end
end
-- Toggles the checkbox [ ] <-> [x] for lines starting with -, *, +, or #
local function toggle_todo()
local line = vim.api.nvim_get_current_line()
-- Pattern captures:
-- 1: Prefix (indentation, marker '-', '*', '+', or '#', and whitespace)
-- 2: State (' ' or 'x')
-- 3: Rest of the line (after ']')
local prefix, state, rest = string.match(line, "^([%s]*[%-#%*%+]%s*)%[([ x])%](.*)")
if prefix then -- Check if the pattern matched (i.e., it's a toggleable line)
-- Determine the new state
local new_state = (state == ' ') and 'x' or ' '
-- Reconstruct the line with the new state
local new_line = prefix .. "[" .. new_state .. "]" .. rest
-- Update the current line
vim.api.nvim_set_current_line(new_line)
end
-- If the pattern didn't match, do nothing.
end
-- Set up buffer-local keymaps
-- Use buffer = 0 to target the current buffer
-- Note: Setting expr = true for Tab/S-Tab allows returning keys for default behavior
vim.keymap.set("n", "<leader>t", function() end, { desc = "+TODOs", buffer = 0 }) -- Placeholder
vim.keymap.set("i", "<CR>", press_enter, { desc = "Todoer: Handle Enter", expr = true, buffer = 0 })
vim.keymap.set("i", "<TAB>", press_tab, { desc = "Todoer: Handle Tab", expr = true, buffer = 0 })
vim.keymap.set("i", "<S-Tab>", press_shift_tab, { desc = "Todoer: Handle Shift-Tab", expr = true, buffer = 0 })
vim.keymap.set("n", "<leader>tt", toggle_todo, { desc = "Todoer: Toggle TODO", buffer = 0 })
-- New mappings for adding TODOs
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>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)
end
-- Setup function: Called by the user in their config
function M.setup(opts)
-- Merge user options with defaults
local config = vim.tbl_deep_extend("force", {}, defaults, opts or {})
-- Create an autocommand group to ensure we can clear it later if needed
local group = vim.api.nvim_create_augroup("TodoerUserSetup", { clear = true })
-- Create the autocommand
vim.api.nvim_create_autocmd("FileType", {
group = group,
pattern = config.filetypes, -- Use filetypes from config
desc = "Setup Todoer keymaps for specific filetypes",
callback = function()
-- Call the function that sets up buffer-local keymaps
setup_buffer_keymaps()
end,
})
end
-- Return the module table
return M