diff --git a/lua/codetyper/adapters/nvim/autocmds.lua b/lua/codetyper/adapters/nvim/autocmds.lua deleted file mode 100644 index 7e8dc94..0000000 --- a/lua/codetyper/adapters/nvim/autocmds.lua +++ /dev/null @@ -1,1429 +0,0 @@ ----@mod codetyper.autocmds Autocommands for Codetyper.nvim - -local M = {} - -local utils = require("codetyper.support.utils") - -local AUGROUP = require("codetyper.constants.constants").AUGROUP -local tree_update_timer = require("codetyper.constants.constants").tree_update_timer -local TREE_UPDATE_DEBOUNCE_MS = require("codetyper.constants.constants").TREE_UPDATE_DEBOUNCE_MS -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 - ---- 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 - ---- 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 - ---- Setup autocommands -function M.setup() - local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true }) - - -- Auto-check for closed prompts when leaving insert mode (works on ALL files) - vim.api.nvim_create_autocmd("InsertLeave", { - group = group, - pattern = "*", - callback = function() - -- Skip special buffers - local buftype = vim.bo.buftype - if buftype ~= "" then - return - end - -- Auto-save coder files only - 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 prompts and auto-process (respects preferences) - M.check_for_closed_prompt_with_preference() - end, - desc = "Check for closed prompt tags on InsertLeave", - }) - - -- Track mode changes for visual mode detection - vim.api.nvim_create_autocmd("ModeChanged", { - group = group, - pattern = "*", - callback = function(ev) - -- Extract old mode from pattern (format: "old_mode:new_mode") - local old_mode = ev.match:match("^(.-):") - if old_mode then - previous_mode = old_mode - end - end, - desc = "Track previous mode for visual mode detection", - }) - - -- Auto-process prompts when entering normal mode (works on ALL files) - vim.api.nvim_create_autocmd("ModeChanged", { - group = group, - pattern = "*:n", - callback = function() - -- Skip special buffers - local buftype = vim.bo.buftype - if buftype ~= "" then - return - end - - -- Skip if currently processing (avoid concurrent processing) - if is_processing then - return - end - - -- Skip if coming from visual mode (v, V, CTRL-V) - user is still editing - if previous_mode == "v" or previous_mode == "V" or previous_mode == "\22" then - return - end - - -- Cancel any pending processing timer - if prompt_process_timer then - prompt_process_timer:stop() - prompt_process_timer = nil - end - - -- Debounced processing - wait for user to truly be idle - prompt_process_timer = vim.defer_fn(function() - prompt_process_timer = nil - -- Double-check we're still in normal mode - local mode = vim.api.nvim_get_mode().mode - if mode ~= "n" then - return - end - M.check_all_prompts_with_preference() - end, PROMPT_PROCESS_DEBOUNCE_MS) - end, - desc = "Auto-process closed prompts when entering normal mode", - }) - - -- Also check on CursorHold as backup (works on ALL files) - vim.api.nvim_create_autocmd("CursorHold", { - group = group, - pattern = "*", - callback = function() - -- Skip special buffers - local buftype = vim.bo.buftype - if buftype ~= "" then - return - end - -- Skip if currently processing - if is_processing then - return - end - local mode = vim.api.nvim_get_mode().mode - if mode == "n" then - M.check_all_prompts_with_preference() - end - end, - desc = "Auto-process closed prompts when idle in normal mode", - }) - - -- Auto-set filetype for coder files based on extension - vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { - group = group, - pattern = "*.codetyper/*", - callback = function() - M.set_coder_filetype() - end, - desc = "Set filetype for coder files", - }) - - -- Cleanup on buffer close - vim.api.nvim_create_autocmd("BufWipeout", { - group = group, - pattern = "*.codetyper/*", - callback = function(ev) - -- Clear processed prompts for this buffer - 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 tracking - M.clear_auto_opened(bufnr) - end, - desc = "Cleanup on coder buffer close", - }) - - -- Update tree.log when files are created/written - vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, { - group = group, - pattern = "*", - callback = function(ev) - -- Skip coder files and tree.log itself - local filepath = ev.file or vim.fn.expand("%:p") - if filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then - return - end - -- Skip non-project files - if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.codetyper/") then - return - end - -- Schedule tree update with debounce - schedule_tree_update() - - -- Trigger incremental indexing if enabled - local ok_indexer, indexer = pcall(require, "codetyper.indexer") - if ok_indexer then - indexer.schedule_index_file(filepath) - end - - -- Update brain with file patterns - local ok_brain, brain = pcall(require, "codetyper.brain") - if ok_brain and brain.is_initialized and brain.is_initialized() then - vim.defer_fn(function() - M.update_brain_from_file(filepath) - end, 500) -- Debounce brain updates - end - end, - desc = "Update tree.log, index, and brain on file creation/save", - }) - - -- Update tree.log when files are deleted (via netrw or file explorer) - vim.api.nvim_create_autocmd("BufDelete", { - group = group, - pattern = "*", - callback = function(ev) - local filepath = ev.file or "" - -- Skip special buffers and coder files - 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", - }) - - -- Update tree on directory change - vim.api.nvim_create_autocmd("DirChanged", { - group = group, - pattern = "*", - callback = function() - schedule_tree_update() - end, - desc = "Update tree.log on directory change", - }) - - -- Shutdown brain on Vim exit - vim.api.nvim_create_autocmd("VimLeavePre", { - group = group, - pattern = "*", - callback = function() - local ok, brain = pcall(require, "codetyper.brain") - if ok and brain.is_initialized and brain.is_initialized() then - brain.shutdown() - end - end, - desc = "Shutdown brain and flush pending changes", - }) - - -- Auto-index: Create/open coder companion file when opening source files - vim.api.nvim_create_autocmd("BufEnter", { - group = group, - pattern = "*", - callback = function(ev) - -- Delay to ensure buffer is fully loaded - vim.defer_fn(function() - M.auto_index_file(ev.buf) - end, 100) - end, - desc = "Auto-index source files with coder companion", - }) - - -- Thinking indicator (throbber) cleanup on exit - local thinking_setup = require("codetyper.adapters.nvim.ui.thinking.setup") - thinking_setup() -end - ---- Create extmarks for injection range so position survives user edits (99-style). ----@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 - -- Clamp to valid 1-based line range (event range may refer to source buffer, target can be different) - 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 - ---- 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 - - -- Try resolving relative to cwd first - local cwd_path = cwd .. "/" .. ref - if utils.file_exists(cwd_path) then - file_path = cwd_path - else - -- Try resolving relative to base file directory - 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 - ---- Check if the buffer has a newly closed prompt and auto-process (works on ANY file) -function M.check_for_closed_prompt() - -- Skip if already processing - 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") - - -- Skip if no file - if current_file == "" then - is_processing = false - return - end - - -- Get current line - 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] - - -- Check if line contains closing tag - if has_closing_tag(current_line, config.patterns.close_tag) then - -- Find the complete prompt - local prompt = get_last_prompt(bufnr) - if prompt and prompt.content and prompt.content ~= "" then - -- Generate unique key for this prompt - local prompt_key = get_prompt_key(bufnr, prompt) - - -- Check if already processed - if processed_prompts[prompt_key] then - is_processing = false - return - end - - -- Mark as processed - processed_prompts[prompt_key] = true - - -- Check if scheduler is enabled - 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 - -- Event-driven: emit to queue - 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") - -- In-buffer placeholder "@thinking .... end thinking" is inserted when worker starts (scheduler) - - -- Take buffer snapshot - local snapshot = patch_mod.snapshot_buffer(bufnr, { - start_line = prompt.start_line, - end_line = prompt.end_line, - }) - - -- Get target path - for coder files, get the target; for regular files, use self - 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 - - -- Read attached files before cleaning - local attached_files = read_attached_files(prompt.content, current_file) - - -- Clean prompt content (strip file references) - local cleaned = clean_prompt(strip_file_references(prompt.content)) - - -- Check if we're working from a coder file - local is_from_coder_file = utils.is_coder_file(current_file) - - -- Resolve scope in target file FIRST (need it to adjust intent) - -- Only resolve scope if NOT from coder file (line numbers don't apply) - 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 - -- Prompt is in the actual source file, use line position for scope - 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 - -- Prompt is in coder file - load target if needed, but don't use scope - -- Code from coder files should append to target by default - 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 - - -- Detect intent from prompt - local intent = intent_mod.detect(cleaned) - - -- IMPORTANT: If prompt is inside a function/method and intent is "add", - -- override to "complete" since we're completing the function body - -- But NOT for coder files - they should use "add/append" by default - 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 - -- Override to complete the function instead of adding new code - intent = { - type = "complete", - scope_hint = "function", - confidence = intent.confidence, - action = "replace", - keywords = intent.keywords, - } - end - end - - -- For coder files, default to "add" with "append" action - 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 - - -- Determine priority based on intent - local priority = 2 -- Normal - if intent.type == "fix" or intent.type == "complete" then - priority = 1 -- High priority for fixes and completions - elseif intent.type == "test" or intent.type == "document" then - priority = 3 -- Lower priority for tests and docs - end - - -- Use captured injection range when provided, else prompt.start_line/end_line - 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 tc = vim.api.nvim_buf_line_count(target_bufnr) - tc = math.max(1, tc) - local rs = math.max(1, math.min(raw_start, tc)) - local re = math.max(1, math.min(raw_end, tc)) - if re < rs then - re = rs - end - local event_range = { start_line = rs, end_line = re } - - -- Extmarks for injection range (99-style: position survives user typing) - local range_for_marks = scope_range or event_range - local injection_marks = create_injection_marks(target_bufnr, range_for_marks) - - -- Enqueue the event (event.range = where to apply the generated code) - 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 - -- Legacy: direct processing - utils.notify("Processing prompt...", vim.log.levels.INFO) - vim.schedule(function() - vim.cmd("CoderProcess") - end) - end - end - end - is_processing = false -end - ---- Process a single prompt through the scheduler ---- This is the core processing logic used by both automatic and manual modes ----@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) -function M.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 - - -- Ensure scheduler is running - if not scheduler.status().running then - scheduler.start() - end - - -- Generate unique key for this prompt - local prompt_key = get_prompt_key(bufnr, prompt) - - -- Skip if already processed (unless overridden for manual mode) - if not skip_processed_check and processed_prompts[prompt_key] then - return - end - - -- Mark as processed - processed_prompts[prompt_key] = true - - -- Process this prompt - 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") - -- In-buffer placeholder "@thinking .... end thinking" is inserted when worker starts (scheduler) - - -- Take buffer snapshot - local snapshot = patch_mod.snapshot_buffer(bufnr, { - start_line = prompt.start_line, - end_line = prompt.end_line, - }) - - -- Get target path - for coder files, get the target; for regular files, use self - 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 - - -- Read attached files before cleaning - local attached_files = read_attached_files(prompt.content, current_file) - - -- Clean prompt content (strip file references) - local cleaned = clean_prompt(strip_file_references(prompt.content)) - - -- Resolve scope in target file FIRST (need it to adjust intent) - -- Only resolve scope if NOT from coder file (line numbers don't apply) - 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 - -- Prompt is in the actual source file, use line position for scope - 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 - -- Prompt is in coder file - load target if needed - 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 - - -- Detect intent from prompt (honor explicit override from transform-selection) - 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 - - -- For coder files, default to "add" with "append" action - 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 - - -- For whole-file selections, gather project tree context - 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 - - -- Determine priority based on intent - 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 - - -- Use captured injection range when provided (from transform-selection), else prompt.start_line/end_line - 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 - -- Clamp to target buffer (1-based, valid lines) - local tc = vim.api.nvim_buf_line_count(target_bufnr) - tc = math.max(1, tc) - local rs = math.max(1, math.min(raw_start, tc)) - local re = math.max(1, math.min(raw_end, tc)) - if re < rs then - re = rs - end - local event_range = { start_line = rs, end_line = re } - - -- Extmarks for injection range (99-style: position survives user typing) - local range_for_marks = scope_range or event_range - local injection_marks = create_injection_marks(target_bufnr, range_for_marks) - - -- Enqueue the event (event.range = where to apply the generated code) - 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 - ---- Check and process all closed prompts in the buffer (works on ANY file) -function M.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") - - -- Skip if no file - if current_file == "" then - return - end - - -- Find all prompts in buffer - local prompts = find_prompts_in_buffer(bufnr) - - if #prompts == 0 then - return - end - - -- Check if scheduler is enabled - 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 - M.process_single_prompt(bufnr, prompt, current_file) - end -end - ---- Check for closed prompt with preference check ---- If user hasn't chosen auto/manual mode, ask them first -function M.check_for_closed_prompt_with_preference() - local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer") - - -- First check if there are any prompts to process - 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 - -- Automatic mode - process prompts - M.check_for_closed_prompt() - end - -- Manual mode - do nothing, user will run :CoderProcess -end - ---- Check all prompts with preference check -function M.check_all_prompts_with_preference() - local preferences = require("codetyper.config.preferences") - local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer") - - -- First check if there are any prompts to process - local bufnr = vim.api.nvim_get_current_buf() - local prompts = find_prompts_in_buffer(bufnr) - if #prompts == 0 then - return - end - - -- Check if any prompts are unprocessed - local has_unprocessed = false - for _, prompt in ipairs(prompts) do - local prompt_key = get_prompt_key(bufnr, prompt) - if not processed_prompts[prompt_key] then - has_unprocessed = true - break - end - end - - if not has_unprocessed then - return - end - - if auto_process then - -- Automatic mode - process prompts - M.check_all_prompts() - end - -- Manual mode - do nothing, user will run :CoderProcess -end - ---- Reset processed prompts for a buffer (useful for re-processing) ----@param bufnr? number Buffer number (default: current) ----@param silent? boolean Suppress notification (default: false) -function M.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 - ---- Track if we already opened the split for this buffer ----@type table -local auto_opened_buffers = {} - ---- Clear auto-opened tracking for a buffer ----@param bufnr number Buffer number -function M.clear_auto_opened(bufnr) - auto_opened_buffers[bufnr] = nil -end - ---- Set appropriate filetype for coder files -function M.set_coder_filetype() - local filepath = vim.fn.expand("%:p") - - -- Extract the actual extension (e.g., index.codetyper/ts -> ts) - local ext = filepath:match("%.codetyper%.(%w+)$") - - if ext then - -- Map extension to filetype - 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 - ---- Clear all autocommands -function M.clear() - vim.api.nvim_del_augroup_by_name(AUGROUP) -end - ---- Debounce timers for brain updates per file ----@type table -local brain_update_timers = {} - ---- Update brain with patterns from a file ----@param filepath string -function M.update_brain_from_file(filepath) - local ok_brain, brain = pcall(require, "codetyper.brain") - if not ok_brain or not brain.is_initialized() then - return - end - - -- Read file content - 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") - - -- Extract key patterns from the file - local functions = {} - local classes = {} - local imports = {} - - for i, line in ipairs(lines) do - -- Functions - local func = 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 then - table.insert(functions, { name = func, line = i }) - end - - -- Classes - local class = 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 then - table.insert(classes, { name = class, line = i }) - end - - -- Imports - local imp = line:match("import%s+.*%s+from%s+[\"']([^\"']+)[\"']") - or line:match("require%([\"']([^\"']+)[\"']%)") - or line:match("from%s+([%w_.]+)%s+import") - if imp then - table.insert(imports, imp) - end - end - - -- Only store if file has meaningful content - if #functions == 0 and #classes == 0 then - return - end - - -- Build summary - local parts = {} - if #functions > 0 then - local func_names = {} - for i, f in ipairs(functions) do - if i <= 5 then - table.insert(func_names, f.name) - end - end - table.insert(parts, "functions: " .. table.concat(func_names, ", ")) - end - if #classes > 0 then - local class_names = {} - for _, c in ipairs(classes) do - table.insert(class_names, c.name) - end - table.insert(parts, "classes: " .. table.concat(class_names, ", ")) - end - - local summary = vim.fn.fnamemodify(filepath, ":t") .. " - " .. table.concat(parts, "; ") - - -- Learn this pattern - use "pattern_detected" type to match the pattern learner - 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(f) - return f.name - end, functions), - example = nil, - }, - }) -end - ---- Track buffers that have been auto-indexed ----@type table -local auto_indexed_buffers = {} - ---- Supported file extensions for auto-indexing -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", -} - ---- Check if extension is supported ----@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 - ---- Auto-index a file by creating/opening its coder companion ----@param bufnr number Buffer number ---- Directories to ignore for coder file creation -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", -} - ---- Files to ignore for coder file creation (exact names or patterns) -local ignored_files = { - -- Git files - ".gitignore", - ".gitattributes", - ".gitmodules", - -- Lock files - "package-lock.json", - "yarn.lock", - "pnpm-lock.yaml", - "Cargo.lock", - "Gemfile.lock", - "poetry.lock", - "composer.lock", - -- Config files that don't need coder companions - ".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", - -- Other non-code files - "README.md", - "CHANGELOG.md", - "LICENSE", - "LICENSE.md", - "CONTRIBUTING.md", - "Makefile", - "CMakeLists.txt", -} - ---- 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 - -- Check for /dirname/ or /dirname at end - if filepath:match("/" .. dir .. "/") or filepath:match("/" .. dir .. "$") then - return true - end - -- Also check for dirname/ at start (relative paths) - if filepath:match("^" .. dir .. "/") then - return true - end - end - return false -end - ---- 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") - - -- Check exact filename matches - for _, ignored in ipairs(ignored_files) do - if filename == ignored then - return true - end - end - - -- Check if file starts with dot (hidden/config files) - if filename:match("^%.") then - return true - end - - -- Check if in ignored directory - if is_in_ignored_directory(filepath) then - return true - end - - return false -end - -function M.auto_index_file(bufnr) - -- Skip if buffer is invalid - if not vim.api.nvim_buf_is_valid(bufnr) then - return - end - - -- Skip if already indexed - if auto_indexed_buffers[bufnr] then - return - end - - -- Get file path - local filepath = vim.api.nvim_buf_get_name(bufnr) - if not filepath or filepath == "" then - return - end - - -- Skip coder files - if utils.is_coder_file(filepath) then - return - end - - -- Skip special buffers - local buftype = vim.bo[bufnr].buftype - if buftype ~= "" then - return - end - - -- Skip unsupported file types - local ext = vim.fn.fnamemodify(filepath, ":e") - if ext == "" or not is_supported_extension(ext) then - return - end - - -- Skip ignored directories and files (node_modules, .git, config files, etc.) - if should_ignore_for_coder(filepath) then - return - end - - -- Skip if auto_index is disabled in config - local codetyper = require("codetyper") - local config = codetyper.get_config() - if config and config.auto_index == false then - return - end - - -- Mark as indexed - auto_indexed_buffers[bufnr] = true - - -- Get coder companion path - local coder_path = utils.get_coder_path(filepath) - - -- Check if coder file already exists - local coder_exists = utils.file_exists(coder_path) - - -- Create coder file with pseudo-code context if it doesn't exist - if not coder_exists then - local filename = vim.fn.fnamemodify(filepath, ":t") - local ext = vim.fn.fnamemodify(filepath, ":e") - - -- Determine comment style based on extension - local comment_prefix = "--" - local comment_block_start = "--[[" - local comment_block_end = "]]" - if - ext == "ts" - or ext == "tsx" - or ext == "js" - or ext == "jsx" - or ext == "java" - or ext == "c" - or ext == "cpp" - or ext == "cs" - or ext == "go" - or ext == "rs" - then - comment_prefix = "//" - comment_block_start = "/*" - comment_block_end = "*/" - elseif ext == "py" or ext == "rb" or ext == "yaml" or ext == "yml" then - comment_prefix = "#" - comment_block_start = '"""' - comment_block_end = '"""' - end - - -- Read target file to analyze its structure - local content = "" - pcall(function() - local lines = vim.fn.readfile(filepath) - if lines then - content = table.concat(lines, "\n") - end - end) - - -- Extract structure from the file - local functions = extract_functions(content, ext) - local classes = extract_classes(content, ext) - local imports = extract_imports(content, ext) - - -- Build pseudo-code context - local pseudo_code = {} - - -- Header - 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 .. "") - - -- Module purpose - 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 .. "") - - -- Dependencies section - 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 - - -- Classes section - 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 - - -- Functions section - 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 empty file, provide starter template - 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 - - -- Business rules section - 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 .. "") - - -- Footer with generation tags example - 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 - - -- Notify user about the coder companion - 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 - ---- Clear auto-indexed tracking for a buffer ----@param bufnr number Buffer number -function M.clear_auto_indexed(bufnr) - auto_indexed_buffers[bufnr] = nil -end - -return M diff --git a/lua/codetyper/adapters/nvim/autocmds/auto_index_file.lua b/lua/codetyper/adapters/nvim/autocmds/auto_index_file.lua new file mode 100644 index 0000000..edb9a26 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/auto_index_file.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/check_all_prompts.lua b/lua/codetyper/adapters/nvim/autocmds/check_all_prompts.lua new file mode 100644 index 0000000..e751761 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/check_all_prompts.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/check_all_prompts_with_preference.lua b/lua/codetyper/adapters/nvim/autocmds/check_all_prompts_with_preference.lua new file mode 100644 index 0000000..4db4691 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/check_all_prompts_with_preference.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt.lua b/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt.lua new file mode 100644 index 0000000..417fd19 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt_with_preference.lua b/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt_with_preference.lua new file mode 100644 index 0000000..75c1653 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt_with_preference.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/clear_auto_opened.lua b/lua/codetyper/adapters/nvim/autocmds/clear_auto_opened.lua new file mode 100644 index 0000000..f01e8ca --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/clear_auto_opened.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/create_injection_marks.lua b/lua/codetyper/adapters/nvim/autocmds/create_injection_marks.lua new file mode 100644 index 0000000..55d3434 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/create_injection_marks.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/get_prompt_key.lua b/lua/codetyper/adapters/nvim/autocmds/get_prompt_key.lua new file mode 100644 index 0000000..30de497 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/get_prompt_key.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/ignored_directories.lua b/lua/codetyper/adapters/nvim/autocmds/ignored_directories.lua new file mode 100644 index 0000000..96b9c16 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/ignored_directories.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/ignored_files.lua b/lua/codetyper/adapters/nvim/autocmds/ignored_files.lua new file mode 100644 index 0000000..9111189 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/ignored_files.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/is_in_ignored_directory.lua b/lua/codetyper/adapters/nvim/autocmds/is_in_ignored_directory.lua new file mode 100644 index 0000000..31226f6 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/is_in_ignored_directory.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/is_supported_extension.lua b/lua/codetyper/adapters/nvim/autocmds/is_supported_extension.lua new file mode 100644 index 0000000..1bcf35b --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/is_supported_extension.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/process_single_prompt.lua b/lua/codetyper/adapters/nvim/autocmds/process_single_prompt.lua new file mode 100644 index 0000000..57a7c3c --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/process_single_prompt.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/read_attached_files.lua b/lua/codetyper/adapters/nvim/autocmds/read_attached_files.lua new file mode 100644 index 0000000..d123484 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/read_attached_files.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/reset_processed.lua b/lua/codetyper/adapters/nvim/autocmds/reset_processed.lua new file mode 100644 index 0000000..34dfbf1 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/reset_processed.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/schedule_tree_update.lua b/lua/codetyper/adapters/nvim/autocmds/schedule_tree_update.lua new file mode 100644 index 0000000..6523fa5 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/schedule_tree_update.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/set_coder_filetype.lua b/lua/codetyper/adapters/nvim/autocmds/set_coder_filetype.lua new file mode 100644 index 0000000..ea94501 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/set_coder_filetype.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/setup.lua b/lua/codetyper/adapters/nvim/autocmds/setup.lua new file mode 100644 index 0000000..f21a39a --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/setup.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/should_ignore_for_coder.lua b/lua/codetyper/adapters/nvim/autocmds/should_ignore_for_coder.lua new file mode 100644 index 0000000..5631f54 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/should_ignore_for_coder.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/state.lua b/lua/codetyper/adapters/nvim/autocmds/state.lua new file mode 100644 index 0000000..a061053 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/state.lua @@ -0,0 +1,7 @@ +local state = { + auto_opened_buffers = {}, + auto_indexed_buffers = {}, + brain_update_timers = {}, +} + +return state diff --git a/lua/codetyper/adapters/nvim/autocmds/supported_extensions.lua b/lua/codetyper/adapters/nvim/autocmds/supported_extensions.lua new file mode 100644 index 0000000..02038a3 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/supported_extensions.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/autocmds/update_brain_from_file.lua b/lua/codetyper/adapters/nvim/autocmds/update_brain_from_file.lua new file mode 100644 index 0000000..20061a5 --- /dev/null +++ b/lua/codetyper/adapters/nvim/autocmds/update_brain_from_file.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands.lua b/lua/codetyper/adapters/nvim/commands.lua deleted file mode 100644 index 4d070e0..0000000 --- a/lua/codetyper/adapters/nvim/commands.lua +++ /dev/null @@ -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", "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", "ctt", function() - transform.cmd_transform_selection() - end, { - silent = true, - desc = "Coder: Open prompt window", - }) -end - -return M diff --git a/lua/codetyper/adapters/nvim/commands/cmd_forget.lua b/lua/codetyper/adapters/nvim/commands/cmd_forget.lua new file mode 100644 index 0000000..d5dafd5 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_forget.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_gitignore.lua b/lua/codetyper/adapters/nvim/commands/cmd_gitignore.lua new file mode 100644 index 0000000..9a44cf1 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_gitignore.lua @@ -0,0 +1,7 @@ +--- Force update gitignore +local function cmd_gitignore() + local gitignore = require("codetyper.support.gitignore") + gitignore.force_update() +end + +return cmd_gitignore diff --git a/lua/codetyper/adapters/nvim/commands/cmd_index_project.lua b/lua/codetyper/adapters/nvim/commands/cmd_index_project.lua new file mode 100644 index 0000000..cab6b7d --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_index_project.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_index_status.lua b/lua/codetyper/adapters/nvim/commands/cmd_index_status.lua new file mode 100644 index 0000000..3b67177 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_index_status.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_llm_feedback.lua b/lua/codetyper/adapters/nvim/commands/cmd_llm_feedback.lua new file mode 100644 index 0000000..618693c --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_llm_feedback.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_llm_reset_stats.lua b/lua/codetyper/adapters/nvim/commands/cmd_llm_reset_stats.lua new file mode 100644 index 0000000..67b126e --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_llm_reset_stats.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_llm_stats.lua b/lua/codetyper/adapters/nvim/commands/cmd_llm_stats.lua new file mode 100644 index 0000000..72cd7f2 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_llm_stats.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_memories.lua b/lua/codetyper/adapters/nvim/commands/cmd_memories.lua new file mode 100644 index 0000000..708d422 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_memories.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_reset.lua b/lua/codetyper/adapters/nvim/commands/cmd_reset.lua new file mode 100644 index 0000000..737d9ff --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_reset.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_tree.lua b/lua/codetyper/adapters/nvim/commands/cmd_tree.lua new file mode 100644 index 0000000..0871deb --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_tree.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/cmd_tree_view.lua b/lua/codetyper/adapters/nvim/commands/cmd_tree_view.lua new file mode 100644 index 0000000..36b9f84 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/cmd_tree_view.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/coder_cmd.lua b/lua/codetyper/adapters/nvim/commands/coder_cmd.lua new file mode 100644 index 0000000..5c6ea4e --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/coder_cmd.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/setup.lua b/lua/codetyper/adapters/nvim/commands/setup.lua new file mode 100644 index 0000000..3c93cdc --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/setup.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/commands/setup_keymaps.lua b/lua/codetyper/adapters/nvim/commands/setup_keymaps.lua new file mode 100644 index 0000000..48b4624 --- /dev/null +++ b/lua/codetyper/adapters/nvim/commands/setup_keymaps.lua @@ -0,0 +1,19 @@ +local transform = require("codetyper.core.transform") + +--- Setup default keymaps for transform commands +local function setup_keymaps() + vim.keymap.set("v", "ctt", function() + transform.cmd_transform_selection() + end, { + silent = true, + desc = "Coder: Transform selection with prompt", + }) + vim.keymap.set("n", "ctt", function() + transform.cmd_transform_selection() + end, { + silent = true, + desc = "Coder: Open prompt window", + }) +end + +return setup_keymaps diff --git a/lua/codetyper/core/transform.lua b/lua/codetyper/core/transform.lua index f20b618..d695bba 100644 --- a/lua/codetyper/core/transform.lua +++ b/lua/codetyper/core/transform.lua @@ -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 }) diff --git a/lua/codetyper/init.lua b/lua/codetyper/init.lua index 5ed5494..3178f4a 100644 --- a/lua/codetyper/init.lua +++ b/lua/codetyper/init.lua @@ -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()