1505 lines
45 KiB
Lua
1505 lines
45 KiB
Lua
---@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<string, boolean>
|
|
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
|
|
|
|
--- 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
|
|
if prompt.content and prompt.content ~= "" then
|
|
-- Generate unique key for this prompt
|
|
local prompt_key = get_prompt_key(bufnr, prompt)
|
|
|
|
-- Skip if already processed
|
|
if processed_prompts[prompt_key] then
|
|
goto continue
|
|
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)
|
|
|
|
::continue::
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Check for closed prompt with preference check
|
|
--- If user hasn't chosen auto/manual mode, ask them first
|
|
function M.check_for_closed_prompt_with_preference()
|
|
local preferences = require("codetyper.preferences")
|
|
local parser = require("codetyper.parser")
|
|
|
|
-- First check if there are any prompts to process
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local prompts = parser.find_prompts_in_buffer(bufnr)
|
|
if #prompts == 0 then
|
|
return
|
|
end
|
|
|
|
-- Check user preference
|
|
local auto_process = preferences.is_auto_process_enabled()
|
|
|
|
if auto_process == nil then
|
|
-- Not yet decided - ask the user (but only once per session)
|
|
if not asking_preference then
|
|
asking_preference = true
|
|
preferences.ask_auto_process_preference(function(enabled)
|
|
asking_preference = false
|
|
if enabled then
|
|
-- User chose automatic - process now
|
|
M.check_for_closed_prompt()
|
|
else
|
|
-- User chose manual - show hint
|
|
utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO)
|
|
end
|
|
end)
|
|
end
|
|
return
|
|
end
|
|
|
|
if auto_process then
|
|
-- Automatic mode - process prompts
|
|
M.check_for_closed_prompt()
|
|
end
|
|
-- Manual mode - do nothing, user will run :CoderProcess
|
|
end
|
|
|
|
--- Check all prompts with preference check
|
|
function M.check_all_prompts_with_preference()
|
|
local preferences = require("codetyper.preferences")
|
|
local parser = require("codetyper.parser")
|
|
|
|
-- First check if there are any prompts to process
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local prompts = parser.find_prompts_in_buffer(bufnr)
|
|
if #prompts == 0 then
|
|
return
|
|
end
|
|
|
|
-- Check if any prompts are unprocessed
|
|
local has_unprocessed = false
|
|
for _, prompt in ipairs(prompts) do
|
|
local prompt_key = get_prompt_key(bufnr, prompt)
|
|
if not processed_prompts[prompt_key] then
|
|
has_unprocessed = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not has_unprocessed then
|
|
return
|
|
end
|
|
|
|
-- Check user preference
|
|
local auto_process = preferences.is_auto_process_enabled()
|
|
|
|
if auto_process == nil then
|
|
-- Not yet decided - ask the user (but only once per session)
|
|
if not asking_preference then
|
|
asking_preference = true
|
|
preferences.ask_auto_process_preference(function(enabled)
|
|
asking_preference = false
|
|
if enabled then
|
|
-- User chose automatic - process now
|
|
M.check_all_prompts()
|
|
else
|
|
-- User chose manual - show hint
|
|
utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO)
|
|
end
|
|
end)
|
|
end
|
|
return
|
|
end
|
|
|
|
if auto_process then
|
|
-- Automatic mode - process prompts
|
|
M.check_all_prompts()
|
|
end
|
|
-- Manual mode - do nothing, user will run :CoderProcess
|
|
end
|
|
|
|
--- Reset processed prompts for a buffer (useful for re-processing)
|
|
---@param bufnr? number Buffer number (default: current)
|
|
function M.reset_processed(bufnr)
|
|
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
|
|
utils.notify("Prompt history cleared - prompts can be re-processed")
|
|
end
|
|
|
|
--- Track if we already opened the split for this buffer
|
|
---@type table<number, boolean>
|
|
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<string, uv_timer_t>
|
|
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<number, boolean>
|
|
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 = "<!--"
|
|
end
|
|
|
|
local close_comment = comment_prefix == "<!--" and " -->" 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
|