---@mod codetyper.autocmds Autocommands for Codetyper.nvim local M = {} local utils = require("codetyper.utils") --- Autocommand group name local AUGROUP = "Codetyper" --- Debounce timer for tree updates local tree_update_timer = nil local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce --- Track processed prompts to avoid re-processing ---@type table local processed_prompts = {} --- Track if we're currently asking for preferences local asking_preference = false --- Track if we're currently processing prompts (busy flag) local is_processing = false --- Track the previous mode for visual mode detection local previous_mode = "n" --- Debounce timer for prompt processing local prompt_process_timer = nil local PROMPT_PROCESS_DEBOUNCE_MS = 200 -- Wait 200ms after mode change before processing --- 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.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 = "*.coder.*", callback = function() M.set_coder_filetype() end, desc = "Set filetype for coder files", }) -- Auto-open split view when opening a coder file directly (e.g., from nvim-tree) vim.api.nvim_create_autocmd("BufEnter", { group = group, pattern = "*.coder.*", callback = function() -- Delay slightly to ensure buffer is fully loaded vim.defer_fn(function() M.auto_open_target_file() end, 50) end, desc = "Auto-open target file when coder file is opened", }) -- Cleanup on buffer close vim.api.nvim_create_autocmd("BufWipeout", { group = group, pattern = "*.coder.*", callback = function(ev) local window = require("codetyper.window") if window.is_open() then window.close_split() end -- 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("%.coder%.") or filepath:match("tree%.log$") then return end -- Skip non-project files if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.coder/") 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("%.coder%.") 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", }) end --- Get config with fallback defaults local function get_config_safe() local codetyper = require("codetyper") local config = codetyper.get_config() -- Return defaults if not initialized if not config or not config.patterns then return { patterns = { open_tag = "/@", close_tag = "@/", file_pattern = "*.coder.*", }, } end return config 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 parser = require("codetyper.parser") local file_refs = parser.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 config = get_config_safe() local parser = require("codetyper.parser") 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 parser.has_closing_tag(current_line, config.patterns.close_tag) then -- Find the complete prompt local prompt = parser.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.agent.queue") local patch_mod = require("codetyper.agent.patch") local intent_mod = require("codetyper.agent.intent") local scope_mod = require("codetyper.agent.scope") local logs_panel = require("codetyper.logs_panel") -- Open logs panel to show progress logs_panel.ensure_open() -- 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 = parser.clean_prompt(parser.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 -- Enqueue the event queue.enqueue({ id = queue.generate_id(), bufnr = bufnr, range = { start_line = prompt.start_line, end_line = prompt.end_line }, 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, }) 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 parser = require("codetyper.parser") local scheduler = require("codetyper.agent.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.agent.queue") local patch_mod = require("codetyper.agent.patch") local intent_mod = require("codetyper.agent.intent") local scope_mod = require("codetyper.agent.scope") local logs_panel = require("codetyper.logs_panel") -- Open logs panel to show progress logs_panel.ensure_open() -- 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 = parser.clean_prompt(parser.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 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 if intent.type == "fix" or intent.type == "complete" then priority = 1 elseif intent.type == "test" or intent.type == "document" then priority = 3 end -- Enqueue the event queue.enqueue({ id = queue.generate_id(), bufnr = bufnr, range = { start_line = prompt.start_line, end_line = prompt.end_line }, 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, }) 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 parser = require("codetyper.parser") 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 = parser.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 preferences = require("codetyper.preferences") local parser = require("codetyper.parser") -- First check if there are any prompts to process local bufnr = vim.api.nvim_get_current_buf() local prompts = parser.find_prompts_in_buffer(bufnr) if #prompts == 0 then return end -- Check user preference local auto_process = preferences.is_auto_process_enabled() if auto_process == nil then -- Not yet decided - ask the user (but only once per session) if not asking_preference then asking_preference = true preferences.ask_auto_process_preference(function(enabled) asking_preference = false if enabled then -- User chose automatic - process now M.check_for_closed_prompt() else -- User chose manual - show hint utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO) end end) end return end if auto_process then -- Automatic mode - process prompts M.check_for_closed_prompt() end -- Manual mode - do nothing, user will run :CoderProcess end --- Check all prompts with preference check function M.check_all_prompts_with_preference() local preferences = require("codetyper.preferences") local parser = require("codetyper.parser") -- First check if there are any prompts to process local bufnr = vim.api.nvim_get_current_buf() local prompts = parser.find_prompts_in_buffer(bufnr) if #prompts == 0 then return end -- Check if any prompts are unprocessed local has_unprocessed = false for _, prompt in ipairs(prompts) do local prompt_key = get_prompt_key(bufnr, prompt) if not processed_prompts[prompt_key] then has_unprocessed = true break end end if not has_unprocessed then return end -- Check user preference local auto_process = preferences.is_auto_process_enabled() if auto_process == nil then -- Not yet decided - ask the user (but only once per session) if not asking_preference then asking_preference = true preferences.ask_auto_process_preference(function(enabled) asking_preference = false if enabled then -- User chose automatic - process now M.check_all_prompts() else -- User chose manual - show hint utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO) end end) end return end if auto_process then -- Automatic mode - process prompts M.check_all_prompts() end -- Manual mode - do nothing, user will run :CoderProcess end --- Reset processed prompts for a buffer (useful for re-processing) ---@param bufnr? number Buffer number (default: current) ---@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 = {} --- Auto-open target file when a coder file is opened directly function M.auto_open_target_file() local window = require("codetyper.window") -- Skip if split is already open if window.is_open() then return end local bufnr = vim.api.nvim_get_current_buf() -- Skip if we already handled this buffer if auto_opened_buffers[bufnr] then return end local current_file = vim.fn.expand("%:p") -- Skip empty paths if not current_file or current_file == "" then return end -- Verify it's a coder file if not utils.is_coder_file(current_file) then return end -- Skip if we're in a special buffer (nvim-tree, etc.) local buftype = vim.bo[bufnr].buftype if buftype ~= "" then return end -- Mark as handled auto_opened_buffers[bufnr] = true -- Get the target file path local target_path = utils.get_target_path(current_file) -- Check if target file exists if not utils.file_exists(target_path) then utils.notify("Target file not found: " .. vim.fn.fnamemodify(target_path, ":t"), vim.log.levels.WARN) return end -- Get config with fallback defaults local codetyper = require("codetyper") local config = codetyper.get_config() -- Fallback width if config not fully loaded (percentage, e.g., 25 = 25%) local width_pct = (config and config.window and config.window.width) or 25 local width = math.ceil(vim.o.columns * (width_pct / 100)) -- Store current coder window local coder_win = vim.api.nvim_get_current_win() local coder_buf = bufnr -- Open target file in a vertical split on the right local ok, err = pcall(function() vim.cmd("vsplit " .. vim.fn.fnameescape(target_path)) end) if not ok then utils.notify("Failed to open target file: " .. tostring(err), vim.log.levels.ERROR) auto_opened_buffers[bufnr] = nil -- Allow retry return end -- Now we're in the target window (right side) local target_win = vim.api.nvim_get_current_win() local target_buf = vim.api.nvim_get_current_buf() -- Set the coder window width (left side) pcall(vim.api.nvim_win_set_width, coder_win, width) -- Update window module state window._coder_win = coder_win window._coder_buf = coder_buf window._target_win = target_win window._target_buf = target_buf -- Set up window options for coder window pcall(function() vim.wo[coder_win].number = true vim.wo[coder_win].relativenumber = true vim.wo[coder_win].signcolumn = "yes" end) utils.notify("Opened target: " .. vim.fn.fnamemodify(target_path, ":t")) end --- 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.coder.ts -> ts) local ext = filepath:match("%.coder%.(%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", ".coder", ".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 .. " Use /@ @/ tags for specific generation requests.") 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 .. " /@") 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 .. " @/") 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 .. " Use /@ @/ tags below to request code generation:") 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 --- Open the coder companion for the current file ---@param open_split? boolean Whether to open in split view (default: true) function M.open_coder_companion(open_split) open_split = open_split ~= false -- Default to true local filepath = vim.fn.expand("%:p") if not filepath or filepath == "" then utils.notify("No file open", vim.log.levels.WARN) return end if utils.is_coder_file(filepath) then utils.notify("Already in coder file", vim.log.levels.INFO) return end local coder_path = utils.get_coder_path(filepath) -- Create if it doesn't exist if not utils.file_exists(coder_path) then local filename = vim.fn.fnamemodify(filepath, ":t") local ext = vim.fn.fnamemodify(filepath, ":e") local comment_prefix = "--" if vim.tbl_contains({ "js", "jsx", "ts", "tsx", "java", "c", "cpp", "cs", "go", "rs", "php" }, ext) then comment_prefix = "//" elseif vim.tbl_contains({ "py", "sh", "zsh", "yaml", "yml" }, ext) then comment_prefix = "#" elseif vim.tbl_contains({ "html", "md" }, ext) then comment_prefix = "" or "" local template = string.format( [[%s Coder companion for %s%s %s Use /@ @/ tags to write pseudo-code prompts%s %s Example:%s %s /@%s %s Add a function that validates user input%s %s - Check for empty strings%s %s - Validate email format%s %s @/%s ]], comment_prefix, filename, close_comment, comment_prefix, close_comment, comment_prefix, close_comment, comment_prefix, close_comment, comment_prefix, close_comment, comment_prefix, close_comment, comment_prefix, close_comment, comment_prefix, close_comment ) utils.write_file(coder_path, template) end if open_split then -- Use the window module to open split view local window = require("codetyper.window") window.open_split(coder_path, filepath) else -- Just open the coder file vim.cmd("edit " .. vim.fn.fnameescape(coder_path)) 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