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 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce
|
||||
---@type table<string, boolean>
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
214
lua/codetyper/preferences.lua
Normal file
214
lua/codetyper/preferences.lua
Normal file
@@ -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<string, CoderPreferences>
|
||||
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", "<Esc>", 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
|
||||
Reference in New Issue
Block a user