From c3da2901c9a5e75898118a24bfc6209091b17409 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Tue, 13 Jan 2026 23:51:17 -0500 Subject: [PATCH] feat: add user preference system for auto/manual tag processing - Add preferences.lua module for managing per-project preferences - Stores preferences in .coder/preferences.json - Shows floating dialog to ask user on first /@ @/ tag - Supports toggle between auto/manual modes - Update autocmds.lua with preference-aware wrapper functions - check_for_closed_prompt_with_preference() - check_all_prompts_with_preference() - Only auto-process when user chose automatic mode - Add CoderAutoToggle and CoderAutoSet commands - Toggle between automatic and manual modes - Set mode directly with :CoderAutoSet auto|manual - Fix completion.lua to work in directories outside project - Use current file's directory as base when editing files outside cwd (e.g., ~/.config/* files) - Search in both current dir and cwd for completions Co-Authored-By: Claude Opus 4.5 --- lua/codetyper/autocmds.lua | 106 ++++++++++++++++- lua/codetyper/commands.lua | 57 +++++++++ lua/codetyper/completion.lua | 40 ++++++- lua/codetyper/preferences.lua | 214 ++++++++++++++++++++++++++++++++++ 4 files changed, 408 insertions(+), 9 deletions(-) create mode 100644 lua/codetyper/preferences.lua diff --git a/lua/codetyper/autocmds.lua b/lua/codetyper/autocmds.lua index 3967c29..cf5d06b 100644 --- a/lua/codetyper/autocmds.lua +++ b/lua/codetyper/autocmds.lua @@ -15,6 +15,9 @@ local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce ---@type table local processed_prompts = {} +--- Track if we're currently asking for preferences +local asking_preference = false + --- Generate a unique key for a prompt ---@param bufnr number Buffer number ---@param prompt table Prompt object @@ -55,8 +58,8 @@ function M.setup() if utils.is_coder_file(filepath) and vim.bo.modified then vim.cmd("silent! write") end - -- Check for closed prompts and auto-process - M.check_for_closed_prompt() + -- Check for closed prompts and auto-process (respects preferences) + M.check_for_closed_prompt_with_preference() end, desc = "Check for closed prompt tags on InsertLeave", }) @@ -73,7 +76,7 @@ function M.setup() end -- Slight delay to let buffer settle vim.defer_fn(function() - M.check_all_prompts() + M.check_all_prompts_with_preference() end, 50) end, desc = "Auto-process closed prompts when entering normal mode", @@ -91,7 +94,7 @@ function M.setup() end local mode = vim.api.nvim_get_mode().mode if mode == "n" then - M.check_all_prompts() + M.check_all_prompts_with_preference() end end, desc = "Auto-process closed prompts when idle in normal mode", @@ -566,6 +569,101 @@ function M.check_all_prompts() end end +--- Check for closed prompt with preference check +--- If user hasn't chosen auto/manual mode, ask them first +function M.check_for_closed_prompt_with_preference() + local preferences = require("codetyper.preferences") + local parser = require("codetyper.parser") + + -- First check if there are any prompts to process + local bufnr = vim.api.nvim_get_current_buf() + local prompts = parser.find_prompts_in_buffer(bufnr) + if #prompts == 0 then + return + end + + -- Check user preference + local auto_process = preferences.is_auto_process_enabled() + + if auto_process == nil then + -- Not yet decided - ask the user (but only once per session) + if not asking_preference then + asking_preference = true + preferences.ask_auto_process_preference(function(enabled) + asking_preference = false + if enabled then + -- User chose automatic - process now + M.check_for_closed_prompt() + else + -- User chose manual - show hint + utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO) + end + end) + end + return + end + + if auto_process then + -- Automatic mode - process prompts + M.check_for_closed_prompt() + end + -- Manual mode - do nothing, user will run :CoderProcess +end + +--- Check all prompts with preference check +function M.check_all_prompts_with_preference() + local preferences = require("codetyper.preferences") + local parser = require("codetyper.parser") + + -- First check if there are any prompts to process + local bufnr = vim.api.nvim_get_current_buf() + local prompts = parser.find_prompts_in_buffer(bufnr) + if #prompts == 0 then + return + end + + -- Check if any prompts are unprocessed + local has_unprocessed = false + for _, prompt in ipairs(prompts) do + local prompt_key = get_prompt_key(bufnr, prompt) + if not processed_prompts[prompt_key] then + has_unprocessed = true + break + end + end + + if not has_unprocessed then + return + end + + -- Check user preference + local auto_process = preferences.is_auto_process_enabled() + + if auto_process == nil then + -- Not yet decided - ask the user (but only once per session) + if not asking_preference then + asking_preference = true + preferences.ask_auto_process_preference(function(enabled) + asking_preference = false + if enabled then + -- User chose automatic - process now + M.check_all_prompts() + else + -- User chose manual - show hint + utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO) + end + end) + end + return + end + + if auto_process then + -- Automatic mode - process prompts + M.check_all_prompts() + end + -- Manual mode - do nothing, user will run :CoderProcess +end + --- Reset processed prompts for a buffer (useful for re-processing) ---@param bufnr? number Buffer number (default: current) function M.reset_processed(bufnr) diff --git a/lua/codetyper/commands.lua b/lua/codetyper/commands.lua index c0a8956..1471489 100644 --- a/lua/codetyper/commands.lua +++ b/lua/codetyper/commands.lua @@ -741,6 +741,29 @@ local function coder_cmd(args) ["logs-toggle"] = cmd_logs_toggle, ["queue-status"] = cmd_queue_status, ["queue-process"] = cmd_queue_process, + ["auto-toggle"] = function() + local preferences = require("codetyper.preferences") + preferences.toggle_auto_process() + end, + ["auto-set"] = function(args) + local preferences = require("codetyper.preferences") + local arg = (args[1] or ""):lower() + if arg == "auto" or arg == "automatic" or arg == "on" then + preferences.set_auto_process(true) + utils.notify("Set to automatic mode", vim.log.levels.INFO) + elseif arg == "manual" or arg == "off" then + preferences.set_auto_process(false) + utils.notify("Set to manual mode", vim.log.levels.INFO) + else + local auto = preferences.is_auto_process_enabled() + if auto == nil then + utils.notify("Mode not set yet (will ask on first prompt)", vim.log.levels.INFO) + else + local mode = auto and "automatic" or "manual" + utils.notify("Currently in " .. mode .. " mode", vim.log.levels.INFO) + end + end + end, } local cmd_fn = commands[subcommand] @@ -764,6 +787,7 @@ function M.setup() "agent", "agent-close", "agent-toggle", "agent-stop", "type-toggle", "logs-toggle", "queue-status", "queue-process", + "auto-toggle", "auto-set", } end, desc = "Codetyper.nvim commands", @@ -860,6 +884,39 @@ function M.setup() cmd_queue_process() end, { desc = "Manually trigger queue processing" }) + -- Preferences commands + vim.api.nvim_create_user_command("CoderAutoToggle", function() + local preferences = require("codetyper.preferences") + preferences.toggle_auto_process() + end, { desc = "Toggle automatic/manual prompt processing" }) + + vim.api.nvim_create_user_command("CoderAutoSet", function(opts) + local preferences = require("codetyper.preferences") + local arg = opts.args:lower() + if arg == "auto" or arg == "automatic" or arg == "on" then + preferences.set_auto_process(true) + vim.notify("Codetyper: Set to automatic mode", vim.log.levels.INFO) + elseif arg == "manual" or arg == "off" then + preferences.set_auto_process(false) + vim.notify("Codetyper: Set to manual mode", vim.log.levels.INFO) + else + -- Show current mode + local auto = preferences.is_auto_process_enabled() + if auto == nil then + vim.notify("Codetyper: Mode not set yet (will ask on first prompt)", vim.log.levels.INFO) + else + local mode = auto and "automatic" or "manual" + vim.notify("Codetyper: Currently in " .. mode .. " mode", vim.log.levels.INFO) + end + end + end, { + desc = "Set prompt processing mode (auto/manual)", + nargs = "?", + complete = function() + return { "auto", "manual" } + end, + }) + -- Setup default keymaps M.setup_keymaps() end diff --git a/lua/codetyper/completion.lua b/lua/codetyper/completion.lua index 20de177..c66f39b 100644 --- a/lua/codetyper/completion.lua +++ b/lua/codetyper/completion.lua @@ -12,25 +12,46 @@ local utils = require("codetyper.utils") ---@return table[] List of completion items local function get_file_completions(prefix) local cwd = vim.fn.getcwd() + local current_file = vim.fn.expand("%:p") + local current_dir = vim.fn.fnamemodify(current_file, ":h") local files = {} -- Use vim.fn.glob to find files matching the prefix local pattern = prefix .. "*" - -- Search in current directory - local matches = vim.fn.glob(cwd .. "/" .. pattern, false, true) + -- Determine base directory - use current file's directory if outside cwd + local base_dir = cwd + if current_dir ~= "" and not current_dir:find(cwd, 1, true) then + -- File is outside project, use its directory as base + base_dir = current_dir + end + + -- Search in base directory + local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true) -- Search with ** for all subdirectories - local deep_matches = vim.fn.glob(cwd .. "/**/" .. pattern, false, true) + local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true) for _, m in ipairs(deep_matches) do table.insert(matches, m) end + -- Also search in cwd if different from base_dir + if base_dir ~= cwd then + local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true) + for _, m in ipairs(cwd_matches) do + table.insert(matches, m) + end + local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true) + for _, m in ipairs(cwd_deep) do + table.insert(matches, m) + end + end + -- Also search specific directories if prefix doesn't have path if not prefix:find("/") then local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" } for _, dir in ipairs(search_dirs) do - local dir_path = cwd .. "/" .. dir + local dir_path = base_dir .. "/" .. dir if vim.fn.isdirectory(dir_path) == 1 then local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true) for _, m in ipairs(dir_matches) do @@ -43,7 +64,16 @@ local function get_file_completions(prefix) -- Convert to relative paths and deduplicate local seen = {} for _, match in ipairs(matches) do - local rel_path = match:sub(#cwd + 2) -- Remove cwd/ prefix + -- Convert to relative path based on which base it came from + local rel_path + if match:find(base_dir, 1, true) == 1 then + rel_path = match:sub(#base_dir + 2) + elseif match:find(cwd, 1, true) == 1 then + rel_path = match:sub(#cwd + 2) + else + rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative + end + -- Skip directories, coder files, and hidden/generated files if vim.fn.isdirectory(match) == 0 and not utils.is_coder_file(match) diff --git a/lua/codetyper/preferences.lua b/lua/codetyper/preferences.lua new file mode 100644 index 0000000..ec28f4f --- /dev/null +++ b/lua/codetyper/preferences.lua @@ -0,0 +1,214 @@ +---@mod codetyper.preferences User preferences management +---@brief [[ +--- Manages user preferences stored in .coder/preferences.json +--- Allows per-project configuration of plugin behavior. +---@brief ]] + +local M = {} + +local utils = require("codetyper.utils") + +---@class CoderPreferences +---@field auto_process boolean Whether to auto-process /@ @/ tags (default: nil = ask) +---@field asked_auto_process boolean Whether we've asked the user about auto_process + +--- Default preferences +local defaults = { + auto_process = nil, -- nil means "not yet decided" + asked_auto_process = false, +} + +--- Cached preferences per project +---@type table +local cache = {} + +--- Get the preferences file path for current project +---@return string +local function get_preferences_path() + local cwd = vim.fn.getcwd() + return cwd .. "/.coder/preferences.json" +end + +--- Ensure .coder directory exists +local function ensure_coder_dir() + local cwd = vim.fn.getcwd() + local coder_dir = cwd .. "/.coder" + if vim.fn.isdirectory(coder_dir) == 0 then + vim.fn.mkdir(coder_dir, "p") + end +end + +--- Load preferences from file +---@return CoderPreferences +function M.load() + local cwd = vim.fn.getcwd() + + -- Check cache first + if cache[cwd] then + return cache[cwd] + end + + local path = get_preferences_path() + local prefs = vim.deepcopy(defaults) + + if utils.file_exists(path) then + local content = utils.read_file(path) + if content then + local ok, decoded = pcall(vim.json.decode, content) + if ok and decoded then + -- Merge with defaults + for k, v in pairs(decoded) do + prefs[k] = v + end + end + end + end + + -- Cache it + cache[cwd] = prefs + return prefs +end + +--- Save preferences to file +---@param prefs CoderPreferences +function M.save(prefs) + local cwd = vim.fn.getcwd() + ensure_coder_dir() + + local path = get_preferences_path() + local ok, encoded = pcall(vim.json.encode, prefs) + if ok then + utils.write_file(path, encoded) + -- Update cache + cache[cwd] = prefs + end +end + +--- Get a specific preference +---@param key string +---@return any +function M.get(key) + local prefs = M.load() + return prefs[key] +end + +--- Set a specific preference +---@param key string +---@param value any +function M.set(key, value) + local prefs = M.load() + prefs[key] = value + M.save(prefs) +end + +--- Check if auto-process is enabled +---@return boolean|nil Returns true/false if set, nil if not yet decided +function M.is_auto_process_enabled() + return M.get("auto_process") +end + +--- Set auto-process preference +---@param enabled boolean +function M.set_auto_process(enabled) + M.set("auto_process", enabled) + M.set("asked_auto_process", true) +end + +--- Check if we've already asked the user about auto-process +---@return boolean +function M.has_asked_auto_process() + return M.get("asked_auto_process") == true +end + +--- Ask user about auto-process preference (shows floating window) +---@param callback function(enabled: boolean) Called with user's choice +function M.ask_auto_process_preference(callback) + -- Check if already asked + if M.has_asked_auto_process() then + local enabled = M.is_auto_process_enabled() + if enabled ~= nil then + callback(enabled) + return + end + end + + -- Create floating window to ask + local width = 60 + local height = 7 + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + + local buf = vim.api.nvim_create_buf(false, true) + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "wipe" + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + row = row, + col = col, + width = width, + height = height, + style = "minimal", + border = "rounded", + title = " Codetyper Preferences ", + title_pos = "center", + }) + + local lines = { + "", + " How would you like to process /@ @/ prompt tags?", + "", + " [a] Automatic - Process when you close the tag", + " [m] Manual - Only process with :CoderProcess", + "", + " Press 'a' or 'm' to choose (Esc to cancel)", + } + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.bo[buf].modifiable = false + + -- Highlight + local ns = vim.api.nvim_create_namespace("codetyper_prefs") + vim.api.nvim_buf_add_highlight(buf, ns, "Title", 1, 0, -1) + vim.api.nvim_buf_add_highlight(buf, ns, "String", 3, 2, 5) + vim.api.nvim_buf_add_highlight(buf, ns, "String", 4, 2, 5) + + local function close_and_callback(enabled) + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + if enabled ~= nil then + M.set_auto_process(enabled) + local mode = enabled and "automatic" or "manual" + vim.notify("Codetyper: Set to " .. mode .. " mode (saved to .coder/preferences.json)", vim.log.levels.INFO) + end + if callback then + callback(enabled) + end + end + + -- Keymaps + local opts = { buffer = buf, noremap = true, silent = true } + vim.keymap.set("n", "a", function() close_and_callback(true) end, opts) + vim.keymap.set("n", "A", function() close_and_callback(true) end, opts) + vim.keymap.set("n", "m", function() close_and_callback(false) end, opts) + vim.keymap.set("n", "M", function() close_and_callback(false) end, opts) + vim.keymap.set("n", "", function() close_and_callback(nil) end, opts) + vim.keymap.set("n", "q", function() close_and_callback(nil) end, opts) +end + +--- Clear cached preferences (useful when changing projects) +function M.clear_cache() + cache = {} +end + +--- Toggle auto-process mode +function M.toggle_auto_process() + local current = M.is_auto_process_enabled() + local new_value = not current + M.set_auto_process(new_value) + local mode = new_value and "automatic" or "manual" + vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO) +end + +return M