migrating autocmds

This commit is contained in:
2026-03-24 22:56:38 -04:00
parent 75de3198cd
commit 5c20f57eb4
40 changed files with 1787 additions and 1854 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
local utils = require("codetyper.support.utils")
local autocmds_state = require("codetyper.adapters.nvim.autocmds.state")
local is_supported_extension = require("codetyper.adapters.nvim.autocmds.is_supported_extension")
local should_ignore_for_coder = require("codetyper.adapters.nvim.autocmds.should_ignore_for_coder")
--- Auto-index a file by creating/opening its coder companion
---@param bufnr number Buffer number
local function auto_index_file(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
if autocmds_state.auto_indexed_buffers[bufnr] then
return
end
local filepath = vim.api.nvim_buf_get_name(bufnr)
if not filepath or filepath == "" then
return
end
if utils.is_coder_file(filepath) then
return
end
local buftype = vim.bo[bufnr].buftype
if buftype ~= "" then
return
end
local ext = vim.fn.fnamemodify(filepath, ":e")
if ext == "" or not is_supported_extension(ext) then
return
end
if should_ignore_for_coder(filepath) then
return
end
local codetyper = require("codetyper")
local config = codetyper.get_config()
if config and config.auto_index == false then
return
end
autocmds_state.auto_indexed_buffers[bufnr] = true
local coder_path = utils.get_coder_path(filepath)
local coder_exists = utils.file_exists(coder_path)
if not coder_exists then
local filename = vim.fn.fnamemodify(filepath, ":t")
local file_ext = vim.fn.fnamemodify(filepath, ":e")
local comment_prefix = "--"
local comment_block_start = "--[["
local comment_block_end = "]]"
if
file_ext == "ts"
or file_ext == "tsx"
or file_ext == "js"
or file_ext == "jsx"
or file_ext == "java"
or file_ext == "c"
or file_ext == "cpp"
or file_ext == "cs"
or file_ext == "go"
or file_ext == "rs"
then
comment_prefix = "//"
comment_block_start = "/*"
comment_block_end = "*/"
elseif file_ext == "py" or file_ext == "rb" or file_ext == "yaml" or file_ext == "yml" then
comment_prefix = "#"
comment_block_start = '"""'
comment_block_end = '"""'
end
local content = ""
pcall(function()
local lines = vim.fn.readfile(filepath)
if lines then
content = table.concat(lines, "\n")
end
end)
local functions = extract_functions(content, file_ext)
local classes = extract_classes(content, file_ext)
local imports = extract_imports(content, file_ext)
local pseudo_code = {}
table.insert(
pseudo_code,
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(pseudo_code, comment_prefix .. " CODER COMPANION: " .. filename)
table.insert(
pseudo_code,
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(pseudo_code, comment_prefix .. " This file describes the business logic and behavior of " .. filename)
table.insert(pseudo_code, comment_prefix .. " Edit this pseudo-code to guide code generation.")
table.insert(pseudo_code, comment_prefix .. "")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " MODULE PURPOSE:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " TODO: Describe what this module/file is responsible for")
table.insert(pseudo_code, comment_prefix .. ' Example: "Handles user authentication and session management"')
table.insert(pseudo_code, comment_prefix .. "")
if #imports > 0 then
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " DEPENDENCIES:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
for _, imp in ipairs(imports) do
table.insert(pseudo_code, comment_prefix .. "" .. imp)
end
table.insert(pseudo_code, comment_prefix .. "")
end
if #classes > 0 then
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " CLASSES:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
for _, class in ipairs(classes) do
table.insert(pseudo_code, comment_prefix .. "")
table.insert(pseudo_code, comment_prefix .. " class " .. class.name .. ":")
table.insert(pseudo_code, comment_prefix .. " PURPOSE: TODO - describe what this class represents")
table.insert(pseudo_code, comment_prefix .. " RESPONSIBILITIES:")
table.insert(pseudo_code, comment_prefix .. " - TODO: list main responsibilities")
end
table.insert(pseudo_code, comment_prefix .. "")
end
if #functions > 0 then
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " FUNCTIONS:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
for _, func in ipairs(functions) do
table.insert(pseudo_code, comment_prefix .. "")
table.insert(pseudo_code, comment_prefix .. " " .. func.name .. "():")
table.insert(pseudo_code, comment_prefix .. " PURPOSE: TODO - what does this function do?")
table.insert(pseudo_code, comment_prefix .. " INPUTS: TODO - describe parameters")
table.insert(pseudo_code, comment_prefix .. " OUTPUTS: TODO - describe return value")
table.insert(pseudo_code, comment_prefix .. " BEHAVIOR:")
table.insert(pseudo_code, comment_prefix .. " - TODO: describe step-by-step logic")
end
table.insert(pseudo_code, comment_prefix .. "")
end
if #functions == 0 and #classes == 0 then
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " PLANNED STRUCTURE:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " TODO: Describe what you want to build in this file")
table.insert(pseudo_code, comment_prefix .. "")
table.insert(pseudo_code, comment_prefix .. " Example pseudo-code:")
table.insert(pseudo_code, comment_prefix .. " Create a module that:")
table.insert(pseudo_code, comment_prefix .. " 1. Exports a main function")
table.insert(pseudo_code, comment_prefix .. " 2. Handles errors gracefully")
table.insert(pseudo_code, comment_prefix .. " 3. Returns structured data")
table.insert(pseudo_code, comment_prefix .. "")
end
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " BUSINESS RULES:")
table.insert(
pseudo_code,
comment_prefix
.. " ─────────────────────────────────────────────────────────────"
)
table.insert(pseudo_code, comment_prefix .. " TODO: Document any business rules, constraints, or requirements")
table.insert(pseudo_code, comment_prefix .. " Example:")
table.insert(pseudo_code, comment_prefix .. " - Users must be authenticated before accessing this feature")
table.insert(pseudo_code, comment_prefix .. " - Data must be validated before saving")
table.insert(pseudo_code, comment_prefix .. " - Errors should be logged but not exposed to users")
table.insert(pseudo_code, comment_prefix .. "")
table.insert(
pseudo_code,
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(
pseudo_code,
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(pseudo_code, "")
utils.write_file(coder_path, table.concat(pseudo_code, "\n"))
end
local coder_filename = vim.fn.fnamemodify(coder_path, ":t")
if coder_exists then
utils.notify("Coder companion available: " .. coder_filename, vim.log.levels.DEBUG)
else
utils.notify("Created coder companion: " .. coder_filename, vim.log.levels.INFO)
end
end
return auto_index_file

View File

@@ -0,0 +1,32 @@
local process_single_prompt = require("codetyper.adapters.nvim.autocmds.process_single_prompt")
--- Check and process all closed prompts in the buffer
local function check_all_prompts()
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
local bufnr = vim.api.nvim_get_current_buf()
local current_file = vim.fn.expand("%:p")
if current_file == "" then
return
end
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
end
local codetyper = require("codetyper")
local ct_config = codetyper.get_config()
local scheduler_enabled = ct_config and ct_config.scheduler and ct_config.scheduler.enabled
if not scheduler_enabled then
return
end
for _, prompt in ipairs(prompts) do
process_single_prompt(bufnr, prompt, current_file)
end
end
return check_all_prompts

View File

@@ -0,0 +1,35 @@
local processed_prompts = require("codetyper.constants.constants").processed_prompts
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
local check_all_prompts = require("codetyper.adapters.nvim.autocmds.check_all_prompts")
--- Check all prompts with preference check
--- Only processes if there are unprocessed prompts and auto_process is enabled
local function check_all_prompts_with_preference()
local preferences = require("codetyper.config.preferences")
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
local bufnr = vim.api.nvim_get_current_buf()
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
end
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
if auto_process then
check_all_prompts()
end
end
return check_all_prompts_with_preference

View File

@@ -0,0 +1,187 @@
local utils = require("codetyper.support.utils")
local processed_prompts = require("codetyper.constants.constants").processed_prompts
local is_processing = require("codetyper.constants.constants").is_processing
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
local read_attached_files = require("codetyper.adapters.nvim.autocmds.read_attached_files")
local create_injection_marks = require("codetyper.adapters.nvim.autocmds.create_injection_marks")
--- Check if the buffer has a newly closed prompt and auto-process
function check_for_closed_prompt()
if is_processing then
return
end
is_processing = true
local has_closing_tag = require("codetyper.parser.has_closing_tag")
local get_last_prompt = require("codetyper.parser.get_last_prompt")
local clean_prompt = require("codetyper.parser.clean_prompt")
local strip_file_references = require("codetyper.parser.strip_file_references")
local bufnr = vim.api.nvim_get_current_buf()
local current_file = vim.fn.expand("%:p")
if current_file == "" then
is_processing = false
return
end
local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1]
local lines = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)
if #lines == 0 then
is_processing = false
return
end
local current_line = lines[1]
if has_closing_tag(current_line, config.patterns.close_tag) then
local prompt = get_last_prompt(bufnr)
if prompt and prompt.content and prompt.content ~= "" then
local prompt_key = get_prompt_key(bufnr, prompt)
if processed_prompts[prompt_key] then
is_processing = false
return
end
processed_prompts[prompt_key] = true
local codetyper = require("codetyper")
local ct_config = codetyper.get_config()
local scheduler_enabled = ct_config and ct_config.scheduler and ct_config.scheduler.enabled
if scheduler_enabled then
vim.schedule(function()
local queue = require("codetyper.core.events.queue")
local patch_mod = require("codetyper.core.diff.patch")
local intent_mod = require("codetyper.core.intent")
local scope_mod = require("codetyper.core.scope")
local snapshot = patch_mod.snapshot_buffer(bufnr, {
start_line = prompt.start_line,
end_line = prompt.end_line,
})
local target_path
if utils.is_coder_file(current_file) then
target_path = utils.get_target_path(current_file)
else
target_path = current_file
end
local attached_files = read_attached_files(prompt.content, current_file)
local cleaned = clean_prompt(strip_file_references(prompt.content))
local is_from_coder_file = utils.is_coder_file(current_file)
local target_bufnr = vim.fn.bufnr(target_path)
local scope = nil
local scope_text = nil
local scope_range = nil
if not is_from_coder_file then
if target_bufnr == -1 then
target_bufnr = bufnr
end
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
if scope and scope.type ~= "file" then
scope_text = scope.text
scope_range = {
start_line = scope.range.start_row,
end_line = scope.range.end_row,
}
end
else
if target_bufnr == -1 then
target_bufnr = vim.fn.bufadd(target_path)
if target_bufnr ~= 0 then
vim.fn.bufload(target_bufnr)
end
end
end
local intent = intent_mod.detect(cleaned)
if not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
intent = {
type = "complete",
scope_hint = "function",
confidence = intent.confidence,
action = "replace",
keywords = intent.keywords,
}
end
end
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
intent = {
type = intent.type == "complete" and "add" or intent.type,
confidence = intent.confidence,
action = "append",
keywords = intent.keywords,
}
end
local priority = 2
if intent.type == "fix" or intent.type == "complete" then
priority = 1
elseif intent.type == "test" or intent.type == "document" then
priority = 3
end
local raw_start = (prompt.injection_range and prompt.injection_range.start_line) or prompt.start_line or 1
local raw_end = (prompt.injection_range and prompt.injection_range.end_line) or prompt.end_line or 1
local target_line_count = vim.api.nvim_buf_line_count(target_bufnr)
target_line_count = math.max(1, target_line_count)
local range_start = math.max(1, math.min(raw_start, target_line_count))
local range_end = math.max(1, math.min(raw_end, target_line_count))
if range_end < range_start then
range_end = range_start
end
local event_range = { start_line = range_start, end_line = range_end }
local range_for_marks = scope_range or event_range
local injection_marks = create_injection_marks(target_bufnr, range_for_marks)
queue.enqueue({
id = queue.generate_id(),
bufnr = bufnr,
range = event_range,
timestamp = os.clock(),
changedtick = snapshot.changedtick,
content_hash = snapshot.content_hash,
prompt_content = cleaned,
target_path = target_path,
priority = priority,
status = "pending",
attempt_count = 0,
intent = intent,
scope = scope,
scope_text = scope_text,
scope_range = scope_range,
attached_files = attached_files,
injection_marks = injection_marks,
})
local scope_info = scope
and scope.type ~= "file"
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
or ""
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
end)
else
utils.notify("Processing prompt...", vim.log.levels.INFO)
vim.schedule(function()
vim.cmd("CoderProcess")
end)
end
end
end
is_processing = false
end
return check_for_closed_prompt

View File

@@ -0,0 +1,19 @@
local check_for_closed_prompt = require("codetyper.adapters.nvim.autocmds.check_for_closed_prompt")
--- Check for closed prompt with preference check
--- If auto_process is enabled, process; otherwise do nothing (manual mode)
local function check_for_closed_prompt_with_preference()
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
local bufnr = vim.api.nvim_get_current_buf()
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
end
if auto_process then
check_for_closed_prompt()
end
end
return check_for_closed_prompt_with_preference

View File

@@ -0,0 +1,9 @@
local autocmds_state = require("codetyper.adapters.nvim.autocmds.state")
--- Clear auto-opened tracking for a buffer
---@param bufnr number Buffer number
local function clear_auto_opened(bufnr)
autocmds_state.auto_opened_buffers[bufnr] = nil
end
return clear_auto_opened

View File

@@ -0,0 +1,31 @@
--- Create extmarks for injection range so position survives user edits
---@param target_bufnr number Target buffer (where code will be injected)
---@param range { start_line: number, end_line: number } Range to mark (1-based)
---@return table|nil injection_marks { start_mark, end_mark } or nil if buffer invalid
local function create_injection_marks(target_bufnr, range)
if not range or target_bufnr == -1 or not vim.api.nvim_buf_is_valid(target_bufnr) then
return nil
end
local line_count = vim.api.nvim_buf_line_count(target_bufnr)
if line_count == 0 then
return nil
end
local start_line = math.max(1, math.min(range.start_line, line_count))
local end_line = math.max(1, math.min(range.end_line, line_count))
if start_line > end_line then
end_line = start_line
end
local marks = require("codetyper.core.marks")
local end_line_content = vim.api.nvim_buf_get_lines(target_bufnr, end_line - 1, end_line, false)
local end_col_0 = 0
if end_line_content and end_line_content[1] then
end_col_0 = #end_line_content[1]
end
local start_mark, end_mark = marks.mark_range(target_bufnr, start_line, end_line, end_col_0)
if not start_mark.id or not end_mark.id then
return nil
end
return { start_mark = start_mark, end_mark = end_mark }
end
return create_injection_marks

View File

@@ -0,0 +1,9 @@
--- Generate a unique key for a prompt
---@param bufnr number Buffer number
---@param prompt table Prompt object
---@return string Unique key
local function get_prompt_key(bufnr, prompt)
return string.format("%d:%d:%d:%s", bufnr, prompt.start_line, prompt.end_line, prompt.content:sub(1, 50))
end
return get_prompt_key

View File

@@ -0,0 +1,25 @@
local ignored_directories = {
".git",
".codetyper",
".claude",
".vscode",
".idea",
"node_modules",
"vendor",
"dist",
"build",
"target",
"__pycache__",
".cache",
".npm",
".yarn",
"coverage",
".next",
".nuxt",
".svelte-kit",
"out",
"bin",
"obj",
}
return ignored_directories

View File

@@ -0,0 +1,48 @@
local ignored_files = {
".gitignore",
".gitattributes",
".gitmodules",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Cargo.lock",
"Gemfile.lock",
"poetry.lock",
"composer.lock",
".env",
".env.local",
".env.development",
".env.production",
".eslintrc",
".eslintrc.json",
".prettierrc",
".prettierrc.json",
".editorconfig",
".dockerignore",
"Dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
".npmrc",
".yarnrc",
".nvmrc",
"tsconfig.json",
"jsconfig.json",
"babel.config.js",
"webpack.config.js",
"vite.config.js",
"rollup.config.js",
"jest.config.js",
"vitest.config.js",
".stylelintrc",
"tailwind.config.js",
"postcss.config.js",
"README.md",
"CHANGELOG.md",
"LICENSE",
"LICENSE.md",
"CONTRIBUTING.md",
"Makefile",
"CMakeLists.txt",
}
return ignored_files

View File

@@ -0,0 +1,18 @@
local ignored_directories = require("codetyper.adapters.nvim.autocmds.ignored_directories")
--- Check if a file path contains an ignored directory
---@param filepath string Full file path
---@return boolean
local function is_in_ignored_directory(filepath)
for _, dir in ipairs(ignored_directories) do
if filepath:match("/" .. dir .. "/") or filepath:match("/" .. dir .. "$") then
return true
end
if filepath:match("^" .. dir .. "/") then
return true
end
end
return false
end
return is_in_ignored_directory

View File

@@ -0,0 +1,15 @@
local supported_extensions = require("codetyper.adapters.nvim.autocmds.supported_extensions")
--- Check if extension is supported for auto-indexing
---@param ext string File extension
---@return boolean
local function is_supported_extension(ext)
for _, supported in ipairs(supported_extensions) do
if ext == supported then
return true
end
end
return false
end
return is_supported_extension

View File

@@ -0,0 +1,178 @@
local utils = require("codetyper.support.utils")
local processed_prompts = require("codetyper.constants.constants").processed_prompts
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
local read_attached_files = require("codetyper.adapters.nvim.autocmds.read_attached_files")
local create_injection_marks = require("codetyper.adapters.nvim.autocmds.create_injection_marks")
--- Process a single prompt through the scheduler
---@param bufnr number Buffer number
---@param prompt table Prompt object with start_line, end_line, content
---@param current_file string Current file path
---@param skip_processed_check? boolean Skip the processed check (for manual mode)
local function process_single_prompt(bufnr, prompt, current_file, skip_processed_check)
local clean_prompt = require("codetyper.parser.clean_prompt")
local strip_file_references = require("codetyper.parser.strip_file_references")
local scheduler = require("codetyper.core.scheduler.scheduler")
if not prompt.content or prompt.content == "" then
return
end
if not scheduler.status().running then
scheduler.start()
end
local prompt_key = get_prompt_key(bufnr, prompt)
if not skip_processed_check and processed_prompts[prompt_key] then
return
end
processed_prompts[prompt_key] = true
vim.schedule(function()
local queue = require("codetyper.core.events.queue")
local patch_mod = require("codetyper.core.diff.patch")
local intent_mod = require("codetyper.core.intent")
local scope_mod = require("codetyper.core.scope")
local snapshot = patch_mod.snapshot_buffer(bufnr, {
start_line = prompt.start_line,
end_line = prompt.end_line,
})
local target_path
local is_from_coder_file = utils.is_coder_file(current_file)
if is_from_coder_file then
target_path = utils.get_target_path(current_file)
else
target_path = current_file
end
local attached_files = read_attached_files(prompt.content, current_file)
local cleaned = clean_prompt(strip_file_references(prompt.content))
local target_bufnr = vim.fn.bufnr(target_path)
local scope = nil
local scope_text = nil
local scope_range = nil
if not is_from_coder_file then
if target_bufnr == -1 then
target_bufnr = bufnr
end
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
if scope and scope.type ~= "file" then
scope_text = scope.text
scope_range = {
start_line = scope.range.start_row,
end_line = scope.range.end_row,
}
end
else
if target_bufnr == -1 then
target_bufnr = vim.fn.bufadd(target_path)
if target_bufnr ~= 0 then
vim.fn.bufload(target_bufnr)
end
end
end
local intent = intent_mod.detect(cleaned)
if prompt.intent_override then
intent.action = prompt.intent_override.action or intent.action
if prompt.intent_override.type then
intent.type = prompt.intent_override.type
end
elseif not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
intent = {
type = "complete",
scope_hint = "function",
confidence = intent.confidence,
action = "replace",
keywords = intent.keywords,
}
end
end
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
intent = {
type = intent.type == "complete" and "add" or intent.type,
confidence = intent.confidence,
action = "append",
keywords = intent.keywords,
}
end
local project_context = nil
if prompt.is_whole_file then
pcall(function()
local tree = require("codetyper.support.tree")
local tree_log = tree.get_tree_log_path()
if tree_log and vim.fn.filereadable(tree_log) == 1 then
local tree_lines = vim.fn.readfile(tree_log)
if tree_lines and #tree_lines > 0 then
local tree_content = table.concat(tree_lines, "\n")
project_context = tree_content:sub(1, 4000)
end
end
end)
end
local priority = 2
if intent.type == "fix" or intent.type == "complete" then
priority = 1
elseif intent.type == "test" or intent.type == "document" then
priority = 3
end
local raw_start = (prompt.injection_range and prompt.injection_range.start_line) or prompt.start_line or 1
local raw_end = (prompt.injection_range and prompt.injection_range.end_line) or prompt.end_line or 1
local target_line_count = vim.api.nvim_buf_line_count(target_bufnr)
target_line_count = math.max(1, target_line_count)
local range_start = math.max(1, math.min(raw_start, target_line_count))
local range_end = math.max(1, math.min(raw_end, target_line_count))
if range_end < range_start then
range_end = range_start
end
local event_range = { start_line = range_start, end_line = range_end }
local range_for_marks = scope_range or event_range
local injection_marks = create_injection_marks(target_bufnr, range_for_marks)
queue.enqueue({
id = queue.generate_id(),
bufnr = bufnr,
range = event_range,
timestamp = os.clock(),
changedtick = snapshot.changedtick,
content_hash = snapshot.content_hash,
prompt_content = cleaned,
target_path = target_path,
priority = priority,
status = "pending",
attempt_count = 0,
intent = intent,
intent_override = prompt.intent_override,
scope = scope,
scope_text = scope_text,
scope_range = scope_range,
attached_files = attached_files,
injection_marks = injection_marks,
injection_range = prompt.injection_range,
is_whole_file = prompt.is_whole_file,
project_context = project_context,
})
local scope_info = scope
and scope.type ~= "file"
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
or ""
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
end)
end
return process_single_prompt

View File

@@ -0,0 +1,42 @@
local utils = require("codetyper.support.utils")
--- Read attached files from prompt content
---@param prompt_content string Prompt content
---@param base_path string Base path to resolve relative file paths
---@return table[] attached_files List of {path, content} tables
local function read_attached_files(prompt_content, base_path)
local extract_file_references = require("codetyper.parser.extract_file_references")
local file_refs = extract_file_references(prompt_content)
local attached = {}
local cwd = vim.fn.getcwd()
local base_dir = vim.fn.fnamemodify(base_path, ":h")
for _, ref in ipairs(file_refs) do
local file_path = nil
local cwd_path = cwd .. "/" .. ref
if utils.file_exists(cwd_path) then
file_path = cwd_path
else
local rel_path = base_dir .. "/" .. ref
if utils.file_exists(rel_path) then
file_path = rel_path
end
end
if file_path then
local content = utils.read_file(file_path)
if content then
table.insert(attached, {
path = ref,
full_path = file_path,
content = content,
})
end
end
end
return attached
end
return read_attached_files

View File

@@ -0,0 +1,19 @@
local utils = require("codetyper.support.utils")
local processed_prompts = require("codetyper.constants.constants").processed_prompts
--- Reset processed prompts for a buffer (useful for re-processing)
---@param bufnr? number Buffer number (default: current)
---@param silent? boolean Suppress notification (default: false)
local function reset_processed(bufnr, silent)
bufnr = bufnr or vim.api.nvim_get_current_buf()
for key, _ in pairs(processed_prompts) do
if key:match("^" .. bufnr .. ":") then
processed_prompts[key] = nil
end
end
if not silent then
utils.notify("Prompt history cleared - prompts can be re-processed")
end
end
return reset_processed

View File

@@ -0,0 +1,17 @@
local tree_update_timer = require("codetyper.constants.constants").tree_update_timer
local TREE_UPDATE_DEBOUNCE_MS = require("codetyper.constants.constants").TREE_UPDATE_DEBOUNCE_MS
--- Schedule tree update with debounce
local function schedule_tree_update()
if tree_update_timer then
tree_update_timer:stop()
end
tree_update_timer = vim.defer_fn(function()
local tree = require("codetyper.support.tree")
tree.update_tree_log()
tree_update_timer = nil
end, TREE_UPDATE_DEBOUNCE_MS)
end
return schedule_tree_update

View File

@@ -0,0 +1,38 @@
--- Set appropriate filetype for coder files based on extension
local function set_coder_filetype()
local filepath = vim.fn.expand("%:p")
local ext = filepath:match("%.codetyper%.(%w+)$")
if ext then
local ft_map = {
ts = "typescript",
tsx = "typescriptreact",
js = "javascript",
jsx = "javascriptreact",
py = "python",
lua = "lua",
go = "go",
rs = "rust",
rb = "ruby",
java = "java",
c = "c",
cpp = "cpp",
cs = "cs",
json = "json",
yaml = "yaml",
yml = "yaml",
md = "markdown",
html = "html",
css = "css",
scss = "scss",
vue = "vue",
svelte = "svelte",
}
local filetype = ft_map[ext] or ext
vim.bo.filetype = filetype
end
end
return set_coder_filetype

View File

@@ -0,0 +1,203 @@
local utils = require("codetyper.support.utils")
local AUGROUP = require("codetyper.constants.constants").AUGROUP
local processed_prompts = require("codetyper.constants.constants").processed_prompts
local is_processing = require("codetyper.constants.constants").is_processing
local previous_mode = require("codetyper.constants.constants").previous_mode
local prompt_process_timer = require("codetyper.constants.constants").prompt_process_timer
local PROMPT_PROCESS_DEBOUNCE_MS = require("codetyper.constants.constants").PROMPT_PROCESS_DEBOUNCE_MS
local schedule_tree_update = require("codetyper.adapters.nvim.autocmds.schedule_tree_update")
local check_for_closed_prompt_with_preference = require("codetyper.adapters.nvim.autocmds.check_for_closed_prompt_with_preference")
local check_all_prompts_with_preference = require("codetyper.adapters.nvim.autocmds.check_all_prompts_with_preference")
local set_coder_filetype = require("codetyper.adapters.nvim.autocmds.set_coder_filetype")
local clear_auto_opened = require("codetyper.adapters.nvim.autocmds.clear_auto_opened")
local auto_index_file = require("codetyper.adapters.nvim.autocmds.auto_index_file")
local update_brain_from_file = require("codetyper.adapters.nvim.autocmds.update_brain_from_file")
--- Setup autocommands
local function setup()
local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true })
vim.api.nvim_create_autocmd("InsertLeave", {
group = group,
pattern = "*",
callback = function()
local buftype = vim.bo.buftype
if buftype ~= "" then
return
end
local filepath = vim.fn.expand("%:p")
if utils.is_coder_file(filepath) and vim.bo.modified then
vim.cmd("silent! write")
end
check_for_closed_prompt_with_preference()
end,
desc = "Check for closed prompt tags on InsertLeave",
})
vim.api.nvim_create_autocmd("ModeChanged", {
group = group,
pattern = "*",
callback = function(ev)
local old_mode = ev.match:match("^(.-):")
if old_mode then
previous_mode = old_mode
end
end,
desc = "Track previous mode for visual mode detection",
})
vim.api.nvim_create_autocmd("ModeChanged", {
group = group,
pattern = "*:n",
callback = function()
local buftype = vim.bo.buftype
if buftype ~= "" then
return
end
if is_processing then
return
end
if previous_mode == "v" or previous_mode == "V" or previous_mode == "\22" then
return
end
if prompt_process_timer then
prompt_process_timer:stop()
prompt_process_timer = nil
end
prompt_process_timer = vim.defer_fn(function()
prompt_process_timer = nil
local mode = vim.api.nvim_get_mode().mode
if mode ~= "n" then
return
end
check_all_prompts_with_preference()
end, PROMPT_PROCESS_DEBOUNCE_MS)
end,
desc = "Auto-process closed prompts when entering normal mode",
})
vim.api.nvim_create_autocmd("CursorHold", {
group = group,
pattern = "*",
callback = function()
local buftype = vim.bo.buftype
if buftype ~= "" then
return
end
if is_processing then
return
end
local mode = vim.api.nvim_get_mode().mode
if mode == "n" then
check_all_prompts_with_preference()
end
end,
desc = "Auto-process closed prompts when idle in normal mode",
})
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
group = group,
pattern = "*.codetyper/*",
callback = function()
set_coder_filetype()
end,
desc = "Set filetype for coder files",
})
vim.api.nvim_create_autocmd("BufWipeout", {
group = group,
pattern = "*.codetyper/*",
callback = function(ev)
local bufnr = ev.buf
for key, _ in pairs(processed_prompts) do
if key:match("^" .. bufnr .. ":") then
processed_prompts[key] = nil
end
end
clear_auto_opened(bufnr)
end,
desc = "Cleanup on coder buffer close",
})
vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, {
group = group,
pattern = "*",
callback = function(ev)
local filepath = ev.file or vim.fn.expand("%:p")
if filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.codetyper/") then
return
end
schedule_tree_update()
local indexer_loaded, indexer = pcall(require, "codetyper.indexer")
if indexer_loaded then
indexer.schedule_index_file(filepath)
end
local brain_loaded, brain = pcall(require, "codetyper.brain")
if brain_loaded and brain.is_initialized and brain.is_initialized() then
vim.defer_fn(function()
update_brain_from_file(filepath)
end, 500)
end
end,
desc = "Update tree.log, index, and brain on file creation/save",
})
vim.api.nvim_create_autocmd("BufDelete", {
group = group,
pattern = "*",
callback = function(ev)
local filepath = ev.file or ""
if filepath == "" or filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
schedule_tree_update()
end,
desc = "Update tree.log on file deletion",
})
vim.api.nvim_create_autocmd("DirChanged", {
group = group,
pattern = "*",
callback = function()
schedule_tree_update()
end,
desc = "Update tree.log on directory change",
})
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
pattern = "*",
callback = function()
local brain_loaded, brain = pcall(require, "codetyper.brain")
if brain_loaded and brain.is_initialized and brain.is_initialized() then
brain.shutdown()
end
end,
desc = "Shutdown brain and flush pending changes",
})
vim.api.nvim_create_autocmd("BufEnter", {
group = group,
pattern = "*",
callback = function(ev)
vim.defer_fn(function()
auto_index_file(ev.buf)
end, 100)
end,
desc = "Auto-index source files with coder companion",
})
local thinking_setup = require("codetyper.adapters.nvim.ui.thinking.setup")
thinking_setup()
end
return setup

View File

@@ -0,0 +1,27 @@
local ignored_files = require("codetyper.adapters.nvim.autocmds.ignored_files")
local is_in_ignored_directory = require("codetyper.adapters.nvim.autocmds.is_in_ignored_directory")
--- Check if a file should be ignored for coder companion creation
---@param filepath string Full file path
---@return boolean
local function should_ignore_for_coder(filepath)
local filename = vim.fn.fnamemodify(filepath, ":t")
for _, ignored in ipairs(ignored_files) do
if filename == ignored then
return true
end
end
if filename:match("^%.") then
return true
end
if is_in_ignored_directory(filepath) then
return true
end
return false
end
return should_ignore_for_coder

View File

@@ -0,0 +1,7 @@
local state = {
auto_opened_buffers = {},
auto_indexed_buffers = {},
brain_update_timers = {},
}
return state

View File

@@ -0,0 +1,29 @@
local supported_extensions = {
"ts",
"tsx",
"js",
"jsx",
"py",
"lua",
"go",
"rs",
"rb",
"java",
"c",
"cpp",
"cs",
"json",
"yaml",
"yml",
"md",
"html",
"css",
"scss",
"vue",
"svelte",
"php",
"sh",
"zsh",
}
return supported_extensions

View File

@@ -0,0 +1,91 @@
local utils = require("codetyper.support.utils")
--- Update brain with patterns from a file
---@param filepath string
local function update_brain_from_file(filepath)
local brain_loaded, brain = pcall(require, "codetyper.brain")
if not brain_loaded or not brain.is_initialized() then
return
end
local content = utils.read_file(filepath)
if not content or content == "" then
return
end
local ext = vim.fn.fnamemodify(filepath, ":e")
local lines = vim.split(content, "\n")
local functions = {}
local classes = {}
local imports = {}
for line_index, line in ipairs(lines) do
local func_name = line:match("^%s*function%s+([%w_:%.]+)%s*%(")
or line:match("^%s*local%s+function%s+([%w_]+)%s*%(")
or line:match("^%s*def%s+([%w_]+)%s*%(")
or line:match("^%s*func%s+([%w_]+)%s*%(")
or line:match("^%s*async%s+function%s+([%w_]+)%s*%(")
or line:match("^%s*public%s+.*%s+([%w_]+)%s*%(")
or line:match("^%s*private%s+.*%s+([%w_]+)%s*%(")
if func_name then
table.insert(functions, { name = func_name, line = line_index })
end
local class_name = line:match("^%s*class%s+([%w_]+)")
or line:match("^%s*public%s+class%s+([%w_]+)")
or line:match("^%s*interface%s+([%w_]+)")
or line:match("^%s*struct%s+([%w_]+)")
if class_name then
table.insert(classes, { name = class_name, line = line_index })
end
local import_path = line:match("import%s+.*%s+from%s+[\"']([^\"']+)[\"']")
or line:match("require%([\"']([^\"']+)[\"']%)")
or line:match("from%s+([%w_.]+)%s+import")
if import_path then
table.insert(imports, import_path)
end
end
if #functions == 0 and #classes == 0 then
return
end
local parts = {}
if #functions > 0 then
local func_names = {}
for func_index, func_entry in ipairs(functions) do
if func_index <= 5 then
table.insert(func_names, func_entry.name)
end
end
table.insert(parts, "functions: " .. table.concat(func_names, ", "))
end
if #classes > 0 then
local class_names = {}
for _, class_entry in ipairs(classes) do
table.insert(class_names, class_entry.name)
end
table.insert(parts, "classes: " .. table.concat(class_names, ", "))
end
local summary = vim.fn.fnamemodify(filepath, ":t") .. " - " .. table.concat(parts, "; ")
brain.learn({
type = "pattern_detected",
file = filepath,
timestamp = os.time(),
data = {
name = summary,
description = #functions .. " functions, " .. #classes .. " classes",
language = ext,
symbols = vim.tbl_map(function(func_entry)
return func_entry.name
end, functions),
example = nil,
},
})
end
return update_brain_from_file

View File

@@ -1,419 +0,0 @@
---@mod codetyper.commands Command definitions for Codetyper.nvim
local M = {}
local transform = require("codetyper.core.transform")
local utils = require("codetyper.support.utils")
--- Refresh tree.log manually
local function cmd_tree()
local tree = require("codetyper.support.tree")
if tree.update_tree_log() then
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
else
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
end
end
--- Open tree.log file
local function cmd_tree_view()
local tree = require("codetyper.support.tree")
local tree_log_path = tree.get_tree_log_path()
if not tree_log_path then
utils.notify("Could not find tree.log", vim.log.levels.WARN)
return
end
-- Ensure tree is up to date
tree.update_tree_log()
-- Open in a new split
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
vim.bo.readonly = true
vim.bo.modifiable = false
end
--- Reset processed prompts to allow re-processing
local function cmd_reset()
local autocmds = require("codetyper.adapters.nvim.autocmds")
autocmds.reset_processed()
end
--- Force update gitignore
local function cmd_gitignore()
local gitignore = require("codetyper.support.gitignore")
gitignore.force_update()
end
--- Index the entire project
local function cmd_index_project()
local indexer = require("codetyper.features.indexer")
utils.notify("Indexing project...", vim.log.levels.INFO)
indexer.index_project(function(index)
if index then
local msg = string.format(
"Indexed: %d files, %d functions, %d classes, %d exports",
index.stats.files,
index.stats.functions,
index.stats.classes,
index.stats.exports
)
utils.notify(msg, vim.log.levels.INFO)
else
utils.notify("Failed to index project", vim.log.levels.ERROR)
end
end)
end
--- Show index status
local function cmd_index_status()
local indexer = require("codetyper.features.indexer")
local memory = require("codetyper.features.indexer.memory")
local status = indexer.get_status()
local mem_stats = memory.get_stats()
local lines = {
"Project Index Status",
"====================",
"",
}
if status.indexed then
table.insert(lines, "Status: Indexed")
table.insert(lines, "Project Type: " .. (status.project_type or "unknown"))
table.insert(lines, "Last Indexed: " .. os.date("%Y-%m-%d %H:%M:%S", status.last_indexed))
table.insert(lines, "")
table.insert(lines, "Stats:")
table.insert(lines, " Files: " .. (status.stats.files or 0))
table.insert(lines, " Functions: " .. (status.stats.functions or 0))
table.insert(lines, " Classes: " .. (status.stats.classes or 0))
table.insert(lines, " Exports: " .. (status.stats.exports or 0))
else
table.insert(lines, "Status: Not indexed")
table.insert(lines, "Run :CoderIndexProject to index")
end
table.insert(lines, "")
table.insert(lines, "Memories:")
table.insert(lines, " Patterns: " .. mem_stats.patterns)
table.insert(lines, " Conventions: " .. mem_stats.conventions)
table.insert(lines, " Symbols: " .. mem_stats.symbols)
utils.notify(table.concat(lines, "\n"))
end
--- Show learned memories
local function cmd_memories()
local memory = require("codetyper.features.indexer.memory")
local all = memory.get_all()
local lines = {
"Learned Memories",
"================",
"",
"Patterns:",
}
local pattern_count = 0
for _, mem in pairs(all.patterns) do
pattern_count = pattern_count + 1
if pattern_count <= 10 then
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
end
end
if pattern_count > 10 then
table.insert(lines, " ... and " .. (pattern_count - 10) .. " more")
elseif pattern_count == 0 then
table.insert(lines, " (none)")
end
table.insert(lines, "")
table.insert(lines, "Conventions:")
local conv_count = 0
for _, mem in pairs(all.conventions) do
conv_count = conv_count + 1
if conv_count <= 10 then
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
end
end
if conv_count > 10 then
table.insert(lines, " ... and " .. (conv_count - 10) .. " more")
elseif conv_count == 0 then
table.insert(lines, " (none)")
end
utils.notify(table.concat(lines, "\n"))
end
--- Clear memories
---@param pattern string|nil Optional pattern to match
local function cmd_forget(pattern)
local memory = require("codetyper.features.indexer.memory")
if not pattern or pattern == "" then
-- Confirm before clearing all
vim.ui.select({ "Yes", "No" }, {
prompt = "Clear all memories?",
}, function(choice)
if choice == "Yes" then
memory.clear()
utils.notify("All memories cleared", vim.log.levels.INFO)
end
end)
else
memory.clear(pattern)
utils.notify("Cleared memories matching: " .. pattern, vim.log.levels.INFO)
end
end
--- Main command dispatcher
---@param args table Command arguments
--- Show LLM accuracy statistics
local function cmd_llm_stats()
local llm = require("codetyper.core.llm")
local stats = llm.get_accuracy_stats()
local lines = {
"LLM Provider Accuracy Statistics",
"================================",
"",
string.format("Ollama:"),
string.format(" Total requests: %d", stats.ollama.total),
string.format(" Correct: %d", stats.ollama.correct),
string.format(" Accuracy: %.1f%%", stats.ollama.accuracy * 100),
"",
string.format("Copilot:"),
string.format(" Total requests: %d", stats.copilot.total),
string.format(" Correct: %d", stats.copilot.correct),
string.format(" Accuracy: %.1f%%", stats.copilot.accuracy * 100),
"",
"Note: Smart selection prefers Ollama when brain memories",
"provide enough context. Accuracy improves over time via",
"pondering (verification with other LLMs).",
}
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
end
--- Report feedback on last LLM response
---@param was_good boolean Whether the response was good
local function cmd_llm_feedback(was_good)
local llm = require("codetyper.core.llm")
-- Default to ollama for feedback
local provider = "ollama"
llm.report_feedback(provider, was_good)
local feedback_type = was_good and "positive" or "negative"
utils.notify(string.format("Reported %s feedback for %s", feedback_type, provider), vim.log.levels.INFO)
end
--- Reset LLM accuracy statistics
local function cmd_llm_reset_stats()
local selector = require("codetyper.core.llm.selector")
selector.reset_accuracy_stats()
utils.notify("LLM accuracy statistics reset", vim.log.levels.INFO)
end
local function coder_cmd(args)
local subcommand = args.fargs[1] or "version"
local commands = {
["version"] = function()
local codetyper = require("codetyper")
utils.notify("Codetyper.nvim " .. codetyper.version, vim.log.levels.INFO)
end,
tree = cmd_tree,
["tree-view"] = cmd_tree_view,
reset = cmd_reset,
gitignore = cmd_gitignore,
["transform-selection"] = transform.cmd_transform_selection,
["index-project"] = cmd_index_project,
["index-status"] = cmd_index_status,
["llm-stats"] = cmd_llm_stats,
["llm-reset-stats"] = cmd_llm_reset_stats,
["cost"] = function()
local cost = require("codetyper.core.cost")
cost.toggle()
end,
["cost-clear"] = function()
local cost = require("codetyper.core.cost")
cost.clear()
end,
["credentials"] = function()
local credentials = require("codetyper.config.credentials")
credentials.show_status()
end,
["switch-provider"] = function()
local credentials = require("codetyper.config.credentials")
credentials.interactive_switch_provider()
end,
["model"] = function(args)
local credentials = require("codetyper.config.credentials")
local codetyper = require("codetyper")
local config = codetyper.get_config()
local provider = config.llm.provider
if provider ~= "copilot" then
utils.notify(
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
vim.log.levels.WARN
)
return
end
local model_arg = args.fargs[2]
if model_arg and model_arg ~= "" then
local cost = credentials.get_copilot_model_cost(model_arg) or "custom"
credentials.set_credentials("copilot", { model = model_arg, configured = true })
utils.notify("Copilot model set to: " .. model_arg .. "" .. cost, vim.log.levels.INFO)
else
credentials.interactive_copilot_config(true)
end
end,
}
local cmd_fn = commands[subcommand]
if cmd_fn then
cmd_fn(args)
else
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
end
end
--- Setup all commands
function M.setup()
vim.api.nvim_create_user_command("Coder", coder_cmd, {
nargs = "?",
complete = function()
return {
"version",
"tree",
"tree-view",
"reset",
"gitignore",
"transform-selection",
"index-project",
"index-status",
"llm-stats",
"llm-reset-stats",
"cost",
"cost-clear",
"credentials",
"switch-provider",
"model",
}
end,
desc = "Codetyper.nvim commands",
})
vim.api.nvim_create_user_command("CoderTree", function()
cmd_tree()
end, { desc = "Refresh tree.log" })
vim.api.nvim_create_user_command("CoderTreeView", function()
cmd_tree_view()
end, { desc = "View tree.log" })
vim.api.nvim_create_user_command("CoderTransformSelection", function()
transform.cmd_transform_selection()
end, { desc = "Transform visual selection with custom prompt input" })
-- Project indexer commands
vim.api.nvim_create_user_command("CoderIndexProject", function()
cmd_index_project()
end, { desc = "Index the entire project" })
vim.api.nvim_create_user_command("CoderIndexStatus", function()
cmd_index_status()
end, { desc = "Show project index status" })
-- TODO: re-enable CoderMemories, CoderForget when memory UI is reworked
-- TODO: re-enable CoderFeedback when feedback loop is reworked
-- TODO: re-enable CoderBrain when brain management UI is reworked
-- Cost estimation command
vim.api.nvim_create_user_command("CoderCost", function()
local cost = require("codetyper.core.cost")
cost.toggle()
end, { desc = "Show LLM cost estimation window" })
-- TODO: re-enable CoderAddApiKey when multi-provider support returns
vim.api.nvim_create_user_command("CoderCredentials", function()
local credentials = require("codetyper.config.credentials")
credentials.show_status()
end, { desc = "Show credentials status" })
vim.api.nvim_create_user_command("CoderSwitchProvider", function()
local credentials = require("codetyper.config.credentials")
credentials.interactive_switch_provider()
end, { desc = "Switch active LLM provider" })
-- Quick model switcher command (Copilot only)
vim.api.nvim_create_user_command("CoderModel", function(opts)
local credentials = require("codetyper.config.credentials")
local codetyper = require("codetyper")
local config = codetyper.get_config()
local provider = config.llm.provider
-- Only available for Copilot provider
if provider ~= "copilot" then
utils.notify(
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
vim.log.levels.WARN
)
return
end
-- If an argument is provided, set the model directly
if opts.args and opts.args ~= "" then
local cost = credentials.get_copilot_model_cost(opts.args) or "custom"
credentials.set_credentials("copilot", { model = opts.args, configured = true })
utils.notify("Copilot model set to: " .. opts.args .. "" .. cost, vim.log.levels.INFO)
return
end
-- Show interactive selector with costs (silent mode - no OAuth message)
credentials.interactive_copilot_config(true)
end, {
nargs = "?",
desc = "Quick switch Copilot model (only available with Copilot provider)",
complete = function()
local codetyper = require("codetyper")
local credentials = require("codetyper.config.credentials")
local config = codetyper.get_config()
if config.llm.provider == "copilot" then
return credentials.get_copilot_model_names()
end
return {}
end,
})
-- Setup default keymaps
M.setup_keymaps()
end
--- Setup default keymaps for transform commands
function M.setup_keymaps()
-- Visual mode: transform selection with custom prompt input
vim.keymap.set("v", "<leader>ctt", function()
transform.cmd_transform_selection()
end, {
silent = true,
desc = "Coder: Transform selection with prompt",
})
-- Normal mode: prompt only (no selection); request is entered in the prompt
vim.keymap.set("n", "<leader>ctt", function()
transform.cmd_transform_selection()
end, {
silent = true,
desc = "Coder: Open prompt window",
})
end
return M

View File

@@ -0,0 +1,23 @@
local utils = require("codetyper.support.utils")
--- Clear memories
---@param pattern string|nil Optional pattern to match
local function cmd_forget(pattern)
local memory = require("codetyper.features.indexer.memory")
if not pattern or pattern == "" then
vim.ui.select({ "Yes", "No" }, {
prompt = "Clear all memories?",
}, function(choice)
if choice == "Yes" then
memory.clear()
utils.notify("All memories cleared", vim.log.levels.INFO)
end
end)
else
memory.clear(pattern)
utils.notify("Cleared memories matching: " .. pattern, vim.log.levels.INFO)
end
end
return cmd_forget

View File

@@ -0,0 +1,7 @@
--- Force update gitignore
local function cmd_gitignore()
local gitignore = require("codetyper.support.gitignore")
gitignore.force_update()
end
return cmd_gitignore

View File

@@ -0,0 +1,25 @@
local utils = require("codetyper.support.utils")
--- Index the entire project
local function cmd_index_project()
local indexer = require("codetyper.features.indexer")
utils.notify("Indexing project...", vim.log.levels.INFO)
indexer.index_project(function(index)
if index then
local msg = string.format(
"Indexed: %d files, %d functions, %d classes, %d exports",
index.stats.files,
index.stats.functions,
index.stats.classes,
index.stats.exports
)
utils.notify(msg, vim.log.levels.INFO)
else
utils.notify("Failed to index project", vim.log.levels.ERROR)
end
end)
end
return cmd_index_project

View File

@@ -0,0 +1,41 @@
local utils = require("codetyper.support.utils")
--- Show index status
local function cmd_index_status()
local indexer = require("codetyper.features.indexer")
local memory = require("codetyper.features.indexer.memory")
local status = indexer.get_status()
local mem_stats = memory.get_stats()
local lines = {
"Project Index Status",
"====================",
"",
}
if status.indexed then
table.insert(lines, "Status: Indexed")
table.insert(lines, "Project Type: " .. (status.project_type or "unknown"))
table.insert(lines, "Last Indexed: " .. os.date("%Y-%m-%d %H:%M:%S", status.last_indexed))
table.insert(lines, "")
table.insert(lines, "Stats:")
table.insert(lines, " Files: " .. (status.stats.files or 0))
table.insert(lines, " Functions: " .. (status.stats.functions or 0))
table.insert(lines, " Classes: " .. (status.stats.classes or 0))
table.insert(lines, " Exports: " .. (status.stats.exports or 0))
else
table.insert(lines, "Status: Not indexed")
table.insert(lines, "Run :CoderIndexProject to index")
end
table.insert(lines, "")
table.insert(lines, "Memories:")
table.insert(lines, " Patterns: " .. mem_stats.patterns)
table.insert(lines, " Conventions: " .. mem_stats.conventions)
table.insert(lines, " Symbols: " .. mem_stats.symbols)
utils.notify(table.concat(lines, "\n"))
end
return cmd_index_status

View File

@@ -0,0 +1,14 @@
local utils = require("codetyper.support.utils")
--- Report feedback on last LLM response
---@param was_good boolean Whether the response was good
local function cmd_llm_feedback(was_good)
local llm = require("codetyper.core.llm")
local provider = "ollama"
llm.report_feedback(provider, was_good)
local feedback_type = was_good and "positive" or "negative"
utils.notify(string.format("Reported %s feedback for %s", feedback_type, provider), vim.log.levels.INFO)
end
return cmd_llm_feedback

View File

@@ -0,0 +1,10 @@
local utils = require("codetyper.support.utils")
--- Reset LLM accuracy statistics
local function cmd_llm_reset_stats()
local selector = require("codetyper.core.llm.selector")
selector.reset_accuracy_stats()
utils.notify("LLM accuracy statistics reset", vim.log.levels.INFO)
end
return cmd_llm_reset_stats

View File

@@ -0,0 +1,28 @@
--- Show LLM accuracy statistics
local function cmd_llm_stats()
local llm = require("codetyper.core.llm")
local stats = llm.get_accuracy_stats()
local lines = {
"LLM Provider Accuracy Statistics",
"================================",
"",
string.format("Ollama:"),
string.format(" Total requests: %d", stats.ollama.total),
string.format(" Correct: %d", stats.ollama.correct),
string.format(" Accuracy: %.1f%%", stats.ollama.accuracy * 100),
"",
string.format("Copilot:"),
string.format(" Total requests: %d", stats.copilot.total),
string.format(" Correct: %d", stats.copilot.correct),
string.format(" Accuracy: %.1f%%", stats.copilot.accuracy * 100),
"",
"Note: Smart selection prefers Ollama when brain memories",
"provide enough context. Accuracy improves over time via",
"pondering (verification with other LLMs).",
}
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
end
return cmd_llm_stats

View File

@@ -0,0 +1,47 @@
local utils = require("codetyper.support.utils")
--- Show learned memories
local function cmd_memories()
local memory = require("codetyper.features.indexer.memory")
local all = memory.get_all()
local lines = {
"Learned Memories",
"================",
"",
"Patterns:",
}
local pattern_count = 0
for _, mem in pairs(all.patterns) do
pattern_count = pattern_count + 1
if pattern_count <= 10 then
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
end
end
if pattern_count > 10 then
table.insert(lines, " ... and " .. (pattern_count - 10) .. " more")
elseif pattern_count == 0 then
table.insert(lines, " (none)")
end
table.insert(lines, "")
table.insert(lines, "Conventions:")
local conv_count = 0
for _, mem in pairs(all.conventions) do
conv_count = conv_count + 1
if conv_count <= 10 then
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
end
end
if conv_count > 10 then
table.insert(lines, " ... and " .. (conv_count - 10) .. " more")
elseif conv_count == 0 then
table.insert(lines, " (none)")
end
utils.notify(table.concat(lines, "\n"))
end
return cmd_memories

View File

@@ -0,0 +1,7 @@
--- Reset processed prompts to allow re-processing
local function cmd_reset()
local reset_processed = require("codetyper.adapters.nvim.autocmds.reset_processed")
reset_processed()
end
return cmd_reset

View File

@@ -0,0 +1,13 @@
local utils = require("codetyper.support.utils")
--- Refresh tree.log manually
local function cmd_tree()
local tree = require("codetyper.support.tree")
if tree.update_tree_log() then
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
else
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
end
end
return cmd_tree

View File

@@ -0,0 +1,20 @@
local utils = require("codetyper.support.utils")
--- Open tree.log file in a vertical split
local function cmd_tree_view()
local tree = require("codetyper.support.tree")
local tree_log_path = tree.get_tree_log_path()
if not tree_log_path then
utils.notify("Could not find tree.log", vim.log.levels.WARN)
return
end
tree.update_tree_log()
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
vim.bo.readonly = true
vim.bo.modifiable = false
end
return cmd_tree_view

View File

@@ -0,0 +1,80 @@
local utils = require("codetyper.support.utils")
local transform = require("codetyper.core.transform")
local cmd_tree = require("codetyper.adapters.nvim.commands.cmd_tree")
local cmd_tree_view = require("codetyper.adapters.nvim.commands.cmd_tree_view")
local cmd_reset = require("codetyper.adapters.nvim.commands.cmd_reset")
local cmd_gitignore = require("codetyper.adapters.nvim.commands.cmd_gitignore")
local cmd_index_project = require("codetyper.adapters.nvim.commands.cmd_index_project")
local cmd_index_status = require("codetyper.adapters.nvim.commands.cmd_index_status")
local cmd_llm_stats = require("codetyper.adapters.nvim.commands.cmd_llm_stats")
local cmd_llm_reset_stats = require("codetyper.adapters.nvim.commands.cmd_llm_reset_stats")
--- Main command dispatcher
---@param args table Command arguments
local function coder_cmd(args)
local subcommand = args.fargs[1] or "version"
local commands = {
["version"] = function()
local codetyper = require("codetyper")
utils.notify("Codetyper.nvim " .. codetyper.version, vim.log.levels.INFO)
end,
tree = cmd_tree,
["tree-view"] = cmd_tree_view,
reset = cmd_reset,
gitignore = cmd_gitignore,
["transform-selection"] = transform.cmd_transform_selection,
["index-project"] = cmd_index_project,
["index-status"] = cmd_index_status,
["llm-stats"] = cmd_llm_stats,
["llm-reset-stats"] = cmd_llm_reset_stats,
["cost"] = function()
local cost = require("codetyper.core.cost")
cost.toggle()
end,
["cost-clear"] = function()
local cost = require("codetyper.core.cost")
cost.clear()
end,
["credentials"] = function()
local credentials = require("codetyper.config.credentials")
credentials.show_status()
end,
["switch-provider"] = function()
local credentials = require("codetyper.config.credentials")
credentials.interactive_switch_provider()
end,
["model"] = function(cmd_args)
local credentials = require("codetyper.config.credentials")
local codetyper = require("codetyper")
local config = codetyper.get_config()
local provider = config.llm.provider
if provider ~= "copilot" then
utils.notify(
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
vim.log.levels.WARN
)
return
end
local model_arg = cmd_args.fargs[2]
if model_arg and model_arg ~= "" then
local model_cost = credentials.get_copilot_model_cost(model_arg) or "custom"
credentials.set_credentials("copilot", { model = model_arg, configured = true })
utils.notify("Copilot model set to: " .. model_arg .. "" .. model_cost, vim.log.levels.INFO)
else
credentials.interactive_copilot_config(true)
end
end,
}
local cmd_fn = commands[subcommand]
if cmd_fn then
cmd_fn(args)
else
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
end
end
return coder_cmd

View File

@@ -0,0 +1,116 @@
local utils = require("codetyper.support.utils")
local transform = require("codetyper.core.transform")
local coder_cmd = require("codetyper.adapters.nvim.commands.coder_cmd")
local cmd_tree = require("codetyper.adapters.nvim.commands.cmd_tree")
local cmd_tree_view = require("codetyper.adapters.nvim.commands.cmd_tree_view")
local cmd_index_project = require("codetyper.adapters.nvim.commands.cmd_index_project")
local cmd_index_status = require("codetyper.adapters.nvim.commands.cmd_index_status")
local setup_keymaps = require("codetyper.adapters.nvim.commands.setup_keymaps")
--- Setup all commands
local function setup()
vim.api.nvim_create_user_command("Coder", coder_cmd, {
nargs = "?",
complete = function()
return {
"version",
"tree",
"tree-view",
"reset",
"gitignore",
"transform-selection",
"index-project",
"index-status",
"llm-stats",
"llm-reset-stats",
"cost",
"cost-clear",
"credentials",
"switch-provider",
"model",
}
end,
desc = "Codetyper.nvim commands",
})
vim.api.nvim_create_user_command("CoderTree", function()
cmd_tree()
end, { desc = "Refresh tree.log" })
vim.api.nvim_create_user_command("CoderTreeView", function()
cmd_tree_view()
end, { desc = "View tree.log" })
vim.api.nvim_create_user_command("CoderTransformSelection", function()
transform.cmd_transform_selection()
end, { desc = "Transform visual selection with custom prompt input" })
vim.api.nvim_create_user_command("CoderIndexProject", function()
cmd_index_project()
end, { desc = "Index the entire project" })
vim.api.nvim_create_user_command("CoderIndexStatus", function()
cmd_index_status()
end, { desc = "Show project index status" })
-- TODO: re-enable CoderMemories, CoderForget when memory UI is reworked
-- TODO: re-enable CoderFeedback when feedback loop is reworked
-- TODO: re-enable CoderBrain when brain management UI is reworked
vim.api.nvim_create_user_command("CoderCost", function()
local cost = require("codetyper.core.cost")
cost.toggle()
end, { desc = "Show LLM cost estimation window" })
-- TODO: re-enable CoderAddApiKey when multi-provider support returns
vim.api.nvim_create_user_command("CoderCredentials", function()
local credentials = require("codetyper.config.credentials")
credentials.show_status()
end, { desc = "Show credentials status" })
vim.api.nvim_create_user_command("CoderSwitchProvider", function()
local credentials = require("codetyper.config.credentials")
credentials.interactive_switch_provider()
end, { desc = "Switch active LLM provider" })
vim.api.nvim_create_user_command("CoderModel", function(opts)
local credentials = require("codetyper.config.credentials")
local codetyper = require("codetyper")
local config = codetyper.get_config()
local provider = config.llm.provider
if provider ~= "copilot" then
utils.notify(
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
vim.log.levels.WARN
)
return
end
if opts.args and opts.args ~= "" then
local model_cost = credentials.get_copilot_model_cost(opts.args) or "custom"
credentials.set_credentials("copilot", { model = opts.args, configured = true })
utils.notify("Copilot model set to: " .. opts.args .. "" .. model_cost, vim.log.levels.INFO)
return
end
credentials.interactive_copilot_config(true)
end, {
nargs = "?",
desc = "Quick switch Copilot model (only available with Copilot provider)",
complete = function()
local codetyper = require("codetyper")
local credentials = require("codetyper.config.credentials")
local config = codetyper.get_config()
if config.llm.provider == "copilot" then
return credentials.get_copilot_model_names()
end
return {}
end,
})
setup_keymaps()
end
return setup

View File

@@ -0,0 +1,19 @@
local transform = require("codetyper.core.transform")
--- Setup default keymaps for transform commands
local function setup_keymaps()
vim.keymap.set("v", "<leader>ctt", function()
transform.cmd_transform_selection()
end, {
silent = true,
desc = "Coder: Transform selection with prompt",
})
vim.keymap.set("n", "<leader>ctt", function()
transform.cmd_transform_selection()
end, {
silent = true,
desc = "Coder: Open prompt window",
})
end
return setup_keymaps

View File

@@ -334,8 +334,8 @@ function M.cmd_transform_selection()
intent_override = doc_intent_override,
is_whole_file = is_whole_file,
}
local autocmds = require("codetyper.adapters.nvim.autocmds")
autocmds.process_single_prompt(bufnr, prompt, filepath, true)
local process_single_prompt = require("codetyper.adapters.nvim.autocmds.process_single_prompt")
process_single_prompt(bufnr, prompt, filepath, true)
end
local augroup = vim.api.nvim_create_augroup("CodetyperPrompt_" .. prompt_buf, { clear = true })

View File

@@ -28,17 +28,17 @@ function M.setup(opts)
M.config = config.setup(opts)
-- Initialize modules
local commands = require("codetyper.adapters.nvim.commands")
local commands_setup = require("codetyper.adapters.nvim.commands.setup")
local gitignore = require("codetyper.support.gitignore")
local autocmds = require("codetyper.adapters.nvim.autocmds")
local autocmds_setup = require("codetyper.adapters.nvim.autocmds.setup")
local tree = require("codetyper.support.tree")
local completion = require("codetyper.features.completion.inline")
-- Register commands
commands.setup()
commands_setup()
-- Setup autocommands
autocmds.setup()
autocmds_setup()
-- Setup file reference completion
completion.setup()