Refactor: Restructure project into core, features, adapters, and config modules

This commit is contained in:
2026-01-16 11:52:46 -05:00
parent 4fb52596e3
commit 10c1de8843
83 changed files with 337 additions and 337 deletions

View File

@@ -0,0 +1,681 @@
---@mod codetyper.credentials Secure credential storage for Codetyper.nvim
---@brief [[
--- Manages API keys and model preferences stored outside of config files.
--- Credentials are stored in ~/.local/share/nvim/codetyper/configuration.json
---@brief ]]
local M = {}
local utils = require("codetyper.support.utils")
--- Get the credentials file path
---@return string Path to credentials file
local function get_credentials_path()
local data_dir = vim.fn.stdpath("data")
return data_dir .. "/codetyper/configuration.json"
end
--- Ensure the credentials directory exists
---@return boolean Success
local function ensure_dir()
local data_dir = vim.fn.stdpath("data")
local codetyper_dir = data_dir .. "/codetyper"
return utils.ensure_dir(codetyper_dir)
end
--- Load credentials from file
---@return table Credentials data
function M.load()
local path = get_credentials_path()
local content = utils.read_file(path)
if not content or content == "" then
return {
version = 1,
providers = {},
}
end
local ok, data = pcall(vim.json.decode, content)
if not ok or not data then
return {
version = 1,
providers = {},
}
end
return data
end
--- Save credentials to file
---@param data table Credentials data
---@return boolean Success
function M.save(data)
if not ensure_dir() then
return false
end
local path = get_credentials_path()
local ok, json = pcall(vim.json.encode, data)
if not ok then
return false
end
return utils.write_file(path, json)
end
--- Get API key for a provider
---@param provider string Provider name (claude, openai, gemini, copilot, ollama)
---@return string|nil API key or nil if not found
function M.get_api_key(provider)
local data = M.load()
local provider_data = data.providers and data.providers[provider]
if provider_data and provider_data.api_key then
return provider_data.api_key
end
return nil
end
--- Get model for a provider
---@param provider string Provider name
---@return string|nil Model name or nil if not found
function M.get_model(provider)
local data = M.load()
local provider_data = data.providers and data.providers[provider]
if provider_data and provider_data.model then
return provider_data.model
end
return nil
end
--- Get endpoint for a provider (for custom OpenAI-compatible endpoints)
---@param provider string Provider name
---@return string|nil Endpoint URL or nil if not found
function M.get_endpoint(provider)
local data = M.load()
local provider_data = data.providers and data.providers[provider]
if provider_data and provider_data.endpoint then
return provider_data.endpoint
end
return nil
end
--- Get host for Ollama
---@return string|nil Host URL or nil if not found
function M.get_ollama_host()
local data = M.load()
local provider_data = data.providers and data.providers.ollama
if provider_data and provider_data.host then
return provider_data.host
end
return nil
end
--- Set credentials for a provider
---@param provider string Provider name
---@param credentials table Credentials (api_key, model, endpoint, host)
---@return boolean Success
function M.set_credentials(provider, credentials)
local data = M.load()
if not data.providers then
data.providers = {}
end
if not data.providers[provider] then
data.providers[provider] = {}
end
-- Merge credentials
for key, value in pairs(credentials) do
if value and value ~= "" then
data.providers[provider][key] = value
end
end
data.updated = os.time()
return M.save(data)
end
--- Remove credentials for a provider
---@param provider string Provider name
---@return boolean Success
function M.remove_credentials(provider)
local data = M.load()
if data.providers and data.providers[provider] then
data.providers[provider] = nil
data.updated = os.time()
return M.save(data)
end
return true
end
--- List all configured providers (checks both stored credentials AND config)
---@return table List of provider names with their config status
function M.list_providers()
local data = M.load()
local result = {}
local all_providers = { "claude", "openai", "gemini", "copilot", "ollama" }
for _, provider in ipairs(all_providers) do
local provider_data = data.providers and data.providers[provider]
local has_stored_key = provider_data and provider_data.api_key and provider_data.api_key ~= ""
local has_model = provider_data and provider_data.model and provider_data.model ~= ""
-- Check if configured from config or environment
local configured_from_config = false
local config_model = nil
local ok, codetyper = pcall(require, "codetyper")
if ok then
local config = codetyper.get_config()
if config and config.llm and config.llm[provider] then
local pc = config.llm[provider]
config_model = pc.model
if provider == "claude" then
configured_from_config = pc.api_key ~= nil or vim.env.ANTHROPIC_API_KEY ~= nil
elseif provider == "openai" then
configured_from_config = pc.api_key ~= nil or vim.env.OPENAI_API_KEY ~= nil
elseif provider == "gemini" then
configured_from_config = pc.api_key ~= nil or vim.env.GEMINI_API_KEY ~= nil
elseif provider == "copilot" then
configured_from_config = true -- Just needs copilot.lua
elseif provider == "ollama" then
configured_from_config = pc.host ~= nil
end
end
end
local is_configured = has_stored_key
or (provider == "ollama" and provider_data ~= nil)
or (provider == "copilot" and (provider_data ~= nil or configured_from_config))
or configured_from_config
table.insert(result, {
name = provider,
configured = is_configured,
has_api_key = has_stored_key,
has_model = has_model or config_model ~= nil,
model = (provider_data and provider_data.model) or config_model,
source = has_stored_key and "stored" or (configured_from_config and "config" or nil),
})
end
return result
end
--- Default models for each provider
M.default_models = {
claude = "claude-sonnet-4-20250514",
openai = "gpt-4o",
gemini = "gemini-2.0-flash",
copilot = "claude-sonnet-4",
ollama = "deepseek-coder:6.7b",
}
--- Available models for Copilot (GitHub Copilot Chat API)
--- Models with cost multipliers: 0x = free, 0.33x = discount, 1x = standard, 3x = premium
M.copilot_models = {
-- Free tier (0x)
{ name = "gpt-4.1", cost = "0x" },
{ name = "gpt-4o", cost = "0x" },
{ name = "gpt-5-mini", cost = "0x" },
{ name = "grok-code-fast-1", cost = "0x" },
{ name = "raptor-mini", cost = "0x" },
-- Discount tier (0.33x)
{ name = "claude-haiku-4.5", cost = "0.33x" },
{ name = "gemini-3-flash", cost = "0.33x" },
{ name = "gpt-5.1-codex-mini", cost = "0.33x" },
-- Standard tier (1x)
{ name = "claude-sonnet-4", cost = "1x" },
{ name = "claude-sonnet-4.5", cost = "1x" },
{ name = "gemini-2.5-pro", cost = "1x" },
{ name = "gemini-3-pro", cost = "1x" },
{ name = "gpt-5", cost = "1x" },
{ name = "gpt-5-codex", cost = "1x" },
{ name = "gpt-5.1", cost = "1x" },
{ name = "gpt-5.1-codex", cost = "1x" },
{ name = "gpt-5.1-codex-max", cost = "1x" },
{ name = "gpt-5.2", cost = "1x" },
{ name = "gpt-5.2-codex", cost = "1x" },
-- Premium tier (3x)
{ name = "claude-opus-4.5", cost = "3x" },
}
--- Get list of copilot model names (for completion)
---@return string[]
function M.get_copilot_model_names()
local names = {}
for _, model in ipairs(M.copilot_models) do
table.insert(names, model.name)
end
return names
end
--- Get cost for a copilot model
---@param model_name string
---@return string|nil
function M.get_copilot_model_cost(model_name)
for _, model in ipairs(M.copilot_models) do
if model.name == model_name then
return model.cost
end
end
return nil
end
--- Interactive command to add/update API key
function M.interactive_add()
local providers = { "claude", "openai", "gemini", "copilot", "ollama" }
-- Step 1: Select provider
vim.ui.select(providers, {
prompt = "Select LLM provider:",
format_item = function(item)
local display = item:sub(1, 1):upper() .. item:sub(2)
local creds = M.load()
local configured = creds.providers and creds.providers[item]
if configured and (configured.api_key or item == "ollama") then
return display .. " [configured]"
end
return display
end,
}, function(provider)
if not provider then
return
end
-- Step 2: Get API key (skip for Ollama)
if provider == "ollama" then
M.interactive_ollama_config()
else
M.interactive_api_key(provider)
end
end)
end
--- Interactive API key input
---@param provider string Provider name
function M.interactive_api_key(provider)
-- Copilot uses OAuth from copilot.lua, no API key needed
if provider == "copilot" then
M.interactive_copilot_config()
return
end
local prompt = string.format("Enter %s API key (leave empty to skip): ", provider:upper())
vim.ui.input({ prompt = prompt }, function(api_key)
if api_key == nil then
return -- Cancelled
end
-- Step 3: Get model
M.interactive_model(provider, api_key)
end)
end
--- Interactive Copilot configuration (no API key, uses OAuth)
---@param silent? boolean If true, don't show the OAuth info message
function M.interactive_copilot_config(silent)
if not silent then
utils.notify("Copilot uses OAuth from copilot.lua/copilot.vim - no API key needed", vim.log.levels.INFO)
end
-- Get current model if configured
local current_model = M.get_model("copilot") or M.default_models.copilot
local current_cost = M.get_copilot_model_cost(current_model) or "?"
-- Build model options with "Custom..." option
local model_options = vim.deepcopy(M.copilot_models)
table.insert(model_options, { name = "Custom...", cost = "" })
vim.ui.select(model_options, {
prompt = "Select Copilot model (current: " .. current_model .. "" .. current_cost .. "):",
format_item = function(item)
local display = item.name
if item.cost and item.cost ~= "" then
display = display .. "" .. item.cost
end
if item.name == current_model then
display = display .. " [current]"
end
return display
end,
}, function(choice)
if choice == nil then
return -- Cancelled
end
if choice.name == "Custom..." then
-- Allow custom model input
vim.ui.input({
prompt = "Enter custom model name: ",
default = current_model,
}, function(custom_model)
if custom_model and custom_model ~= "" then
M.save_and_notify("copilot", {
model = custom_model,
configured = true,
})
end
end)
else
M.save_and_notify("copilot", {
model = choice.name,
configured = true,
})
end
end)
end
--- Interactive model selection
---@param provider string Provider name
---@param api_key string|nil API key
function M.interactive_model(provider, api_key)
local default_model = M.default_models[provider] or ""
local prompt = string.format("Enter model (default: %s): ", default_model)
vim.ui.input({ prompt = prompt, default = default_model }, function(model)
if model == nil then
return -- Cancelled
end
-- Use default if empty
if model == "" then
model = default_model
end
-- Save credentials
local credentials = {
model = model,
}
if api_key and api_key ~= "" then
credentials.api_key = api_key
end
-- For OpenAI, also ask for custom endpoint
if provider == "openai" then
M.interactive_endpoint(provider, credentials)
else
M.save_and_notify(provider, credentials)
end
end)
end
--- Interactive endpoint input for OpenAI-compatible providers
---@param provider string Provider name
---@param credentials table Current credentials
function M.interactive_endpoint(provider, credentials)
vim.ui.input({
prompt = "Custom endpoint (leave empty for default OpenAI): ",
}, function(endpoint)
if endpoint == nil then
return -- Cancelled
end
if endpoint ~= "" then
credentials.endpoint = endpoint
end
M.save_and_notify(provider, credentials)
end)
end
--- Interactive Ollama configuration
function M.interactive_ollama_config()
vim.ui.input({
prompt = "Ollama host (default: http://localhost:11434): ",
default = "http://localhost:11434",
}, function(host)
if host == nil then
return -- Cancelled
end
if host == "" then
host = "http://localhost:11434"
end
-- Get model
local default_model = M.default_models.ollama
vim.ui.input({
prompt = string.format("Ollama model (default: %s): ", default_model),
default = default_model,
}, function(model)
if model == nil then
return -- Cancelled
end
if model == "" then
model = default_model
end
M.save_and_notify("ollama", {
host = host,
model = model,
})
end)
end)
end
--- Save credentials and notify user
---@param provider string Provider name
---@param credentials table Credentials to save
function M.save_and_notify(provider, credentials)
if M.set_credentials(provider, credentials) then
local msg = string.format("Saved %s configuration", provider:upper())
if credentials.model then
msg = msg .. " (model: " .. credentials.model .. ")"
end
utils.notify(msg, vim.log.levels.INFO)
else
utils.notify("Failed to save credentials", vim.log.levels.ERROR)
end
end
--- Show current credentials status
function M.show_status()
local providers = M.list_providers()
-- Get current active provider
local codetyper = require("codetyper")
local current = codetyper.get_config().llm.provider
local lines = {
"Codetyper Credentials Status",
"============================",
"",
"Storage: " .. get_credentials_path(),
"Active: " .. current:upper(),
"",
}
for _, p in ipairs(providers) do
local status_icon = p.configured and "" or ""
local active_marker = p.name == current and " [ACTIVE]" or ""
local source_info = ""
if p.configured then
source_info = p.source == "stored" and " (stored)" or " (config)"
end
local model_info = p.model and (" - " .. p.model) or ""
table.insert(lines, string.format(" %s %s%s%s%s",
status_icon,
p.name:upper(),
active_marker,
source_info,
model_info))
end
table.insert(lines, "")
table.insert(lines, "Commands:")
table.insert(lines, " :CoderAddApiKey - Add/update credentials")
table.insert(lines, " :CoderSwitchProvider - Switch active provider")
table.insert(lines, " :CoderRemoveApiKey - Remove stored credentials")
utils.notify(table.concat(lines, "\n"))
end
--- Interactive remove credentials
function M.interactive_remove()
local data = M.load()
local configured = {}
for provider, _ in pairs(data.providers or {}) do
table.insert(configured, provider)
end
if #configured == 0 then
utils.notify("No credentials configured", vim.log.levels.INFO)
return
end
vim.ui.select(configured, {
prompt = "Select provider to remove:",
}, function(provider)
if not provider then
return
end
vim.ui.select({ "Yes", "No" }, {
prompt = "Remove " .. provider:upper() .. " credentials?",
}, function(choice)
if choice == "Yes" then
if M.remove_credentials(provider) then
utils.notify("Removed " .. provider:upper() .. " credentials", vim.log.levels.INFO)
else
utils.notify("Failed to remove credentials", vim.log.levels.ERROR)
end
end
end)
end)
end
--- Set the active provider
---@param provider string Provider name
function M.set_active_provider(provider)
local data = M.load()
data.active_provider = provider
data.updated = os.time()
M.save(data)
-- Also update the runtime config
local codetyper = require("codetyper")
local config = codetyper.get_config()
config.llm.provider = provider
utils.notify("Active provider set to: " .. provider:upper(), vim.log.levels.INFO)
end
--- Get the active provider from stored config
---@return string|nil Active provider
function M.get_active_provider()
local data = M.load()
return data.active_provider
end
--- Check if a provider is configured (from stored credentials OR config)
---@param provider string Provider name
---@return boolean configured, string|nil source
local function is_provider_configured(provider)
-- Check stored credentials first
local data = M.load()
local stored = data.providers and data.providers[provider]
if stored then
if stored.configured or stored.api_key or provider == "ollama" or provider == "copilot" then
return true, "stored"
end
end
-- Check codetyper config
local ok, codetyper = pcall(require, "codetyper")
if not ok then
return false, nil
end
local config = codetyper.get_config()
if not config or not config.llm then
return false, nil
end
local provider_config = config.llm[provider]
if not provider_config then
return false, nil
end
-- Check for API key in config or environment
if provider == "claude" then
if provider_config.api_key or vim.env.ANTHROPIC_API_KEY then
return true, "config"
end
elseif provider == "openai" then
if provider_config.api_key or vim.env.OPENAI_API_KEY then
return true, "config"
end
elseif provider == "gemini" then
if provider_config.api_key or vim.env.GEMINI_API_KEY then
return true, "config"
end
elseif provider == "copilot" then
-- Copilot just needs copilot.lua installed
return true, "config"
elseif provider == "ollama" then
-- Ollama just needs host configured
if provider_config.host then
return true, "config"
end
end
return false, nil
end
--- Interactive switch provider
function M.interactive_switch_provider()
local all_providers = { "claude", "openai", "gemini", "copilot", "ollama" }
local available = {}
local sources = {}
for _, provider in ipairs(all_providers) do
local configured, source = is_provider_configured(provider)
if configured then
table.insert(available, provider)
sources[provider] = source
end
end
if #available == 0 then
utils.notify("No providers configured. Use :CoderAddApiKey or add to your config.", vim.log.levels.WARN)
return
end
local codetyper = require("codetyper")
local current = codetyper.get_config().llm.provider
vim.ui.select(available, {
prompt = "Select provider (current: " .. current .. "):",
format_item = function(item)
local marker = item == current and " [active]" or ""
local source_marker = sources[item] == "stored" and " (stored)" or " (config)"
return item:upper() .. marker .. source_marker
end,
}, function(provider)
if provider then
M.set_active_provider(provider)
end
end)
end
return M

View File

@@ -0,0 +1,160 @@
---@mod codetyper.config Configuration module for Codetyper.nvim
local M = {}
---@type CoderConfig
local defaults = {
llm = {
provider = "ollama", -- Options: "ollama", "openai", "gemini", "copilot"
ollama = {
host = "http://localhost:11434",
model = "deepseek-coder:6.7b",
},
openai = {
api_key = nil, -- Will use OPENAI_API_KEY env var if nil
model = "gpt-4o",
endpoint = nil, -- Custom endpoint (Azure, OpenRouter, etc.)
},
gemini = {
api_key = nil, -- Will use GEMINI_API_KEY env var if nil
model = "gemini-2.0-flash",
},
copilot = {
model = "claude-sonnet-4", -- Uses GitHub Copilot authentication
},
},
window = {
width = 25, -- 25% of screen width (1/4)
position = "left",
border = "rounded",
},
patterns = {
open_tag = "/@",
close_tag = "@/",
file_pattern = "*.coder.*",
},
auto_gitignore = true,
auto_open_ask = true, -- Auto-open Ask panel on startup
auto_index = false, -- Auto-create coder companion files on file open
scheduler = {
enabled = true, -- Enable event-driven scheduler
ollama_scout = true, -- Use Ollama as fast local scout for first attempt
escalation_threshold = 0.7, -- Below this confidence, escalate to remote LLM
max_concurrent = 2, -- Maximum concurrent workers
completion_delay_ms = 100, -- Wait after completion popup closes
apply_delay_ms = 5000, -- Wait before removing tags and applying code (ms)
},
indexer = {
enabled = true, -- Enable project indexing
auto_index = true, -- Index files on save
index_on_open = false, -- Index project when opening
max_file_size = 100000, -- Skip files larger than 100KB
excluded_dirs = { "node_modules", "dist", "build", ".git", ".coder", "__pycache__", "vendor", "target" },
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
memory = {
enabled = true, -- Enable memory persistence
max_memories = 1000, -- Maximum stored memories
prune_threshold = 0.1, -- Remove low-weight memories
},
},
brain = {
enabled = true, -- Enable brain learning system
auto_learn = true, -- Auto-learn from events
auto_commit = true, -- Auto-commit after threshold
commit_threshold = 10, -- Changes before auto-commit
max_nodes = 5000, -- Maximum nodes before pruning
max_deltas = 500, -- Maximum delta history
prune = {
enabled = true, -- Enable auto-pruning
threshold = 0.1, -- Remove nodes below this weight
unused_days = 90, -- Remove unused nodes after N days
},
output = {
max_tokens = 4000, -- Token budget for LLM context
format = "compact", -- "compact"|"json"|"natural"
},
},
suggestion = {
enabled = true, -- Enable ghost text suggestions (Copilot-style)
auto_trigger = true, -- Auto-trigger on typing
debounce = 150, -- Debounce in milliseconds
use_copilot = true, -- Use copilot.lua suggestions when available, fallback to codetyper
keymap = {
accept = "<Tab>", -- Accept suggestion
next = "<M-]>", -- Next suggestion (Alt+])
prev = "<M-[>", -- Previous suggestion (Alt+[)
dismiss = "<C-]>", -- Dismiss suggestion (Ctrl+])
},
},
}
--- Deep merge two tables
---@param t1 table Base table
---@param t2 table Table to merge into base
---@return table Merged table
local function deep_merge(t1, t2)
local result = vim.deepcopy(t1)
for k, v in pairs(t2) do
if type(v) == "table" and type(result[k]) == "table" then
result[k] = deep_merge(result[k], v)
else
result[k] = v
end
end
return result
end
--- Setup configuration with user options
---@param opts? CoderConfig User configuration options
---@return CoderConfig Final configuration
function M.setup(opts)
opts = opts or {}
return deep_merge(defaults, opts)
end
--- Get default configuration
---@return CoderConfig Default configuration
function M.get_defaults()
return vim.deepcopy(defaults)
end
--- Validate configuration
---@param config CoderConfig Configuration to validate
---@return boolean, string? Valid status and optional error message
function M.validate(config)
if not config.llm then
return false, "Missing LLM configuration"
end
local valid_providers = { "ollama", "openai", "gemini", "copilot" }
local is_valid_provider = false
for _, p in ipairs(valid_providers) do
if config.llm.provider == p then
is_valid_provider = true
break
end
end
if not is_valid_provider then
return false, "Invalid LLM provider. Must be one of: " .. table.concat(valid_providers, ", ")
end
-- Validate provider-specific configuration
if config.llm.provider == "openai" then
local api_key = config.llm.openai.api_key or vim.env.OPENAI_API_KEY
if not api_key or api_key == "" then
return false, "OpenAI API key not configured. Set llm.openai.api_key or OPENAI_API_KEY env var"
end
elseif config.llm.provider == "gemini" then
local api_key = config.llm.gemini.api_key or vim.env.GEMINI_API_KEY
if not api_key or api_key == "" then
return false, "Gemini API key not configured. Set llm.gemini.api_key or GEMINI_API_KEY env var"
end
end
-- Note: copilot uses OAuth from copilot.lua/copilot.vim, validated at runtime
-- Note: ollama doesn't require API key, just host configuration
return true
end
return M

View 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.support.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