feat: add function completion, apply delay, and VimLeavePre cleanup
Major improvements to the event-driven prompt processing system: Function Completion: - Override intent to "complete" when prompt is inside function/method scope - Use Tree-sitter to detect enclosing scope and replace entire function - Special LLM prompt instructs to complete function body without duplicating - Patch apply uses "replace" strategy for scope range instead of appending Apply Delay: - Add `apply_delay_ms` config option (default 5000ms) for code review time - Log "Code ready. Applying in X seconds..." before applying patches - Configurable wait time before removing tags and injecting code VimLeavePre Cleanup: - Logs panel and queue windows close automatically on Neovim exit - Context modal closes on VimLeavePre - Scheduler stops timer and cleans up augroup on exit - Handle QuitPre for :qa, :wqa commands - Force close with buffer deletion for robust cleanup Response Cleaning: - Remove LLM special tokens (deepseek, llama markers) - Add blank line spacing before appended code - Log full raw LLM response in logs panel for debugging Documentation: - Add required dependencies (plenary.nvim, nvim-treesitter) - Add optional dependencies (nvim-treesitter-textobjects, nui.nvim) - Document all intent types including "complete" - Add Logs Panel section with features and keymaps - Update lazy.nvim example with dependencies Tests: - Add tests for patch create_from_event with different strategies - Fix assert.is_true to assert.is_truthy for string.match Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
66
README.md
66
README.md
@@ -46,6 +46,16 @@
|
||||
- curl (for API calls)
|
||||
- One of: Claude API key, OpenAI API key, Gemini API key, GitHub Copilot, or Ollama running locally
|
||||
|
||||
### Required Plugins
|
||||
|
||||
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Async utilities
|
||||
- [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) - Scope detection for functions/methods
|
||||
|
||||
### Optional Plugins
|
||||
|
||||
- [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects) - Better text object support
|
||||
- [nui.nvim](https://github.com/MunifTanjim/nui.nvim) - UI components
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
@@ -55,6 +65,12 @@
|
||||
```lua
|
||||
{
|
||||
"cargdev/codetyper.nvim",
|
||||
dependencies = {
|
||||
"nvim-lua/plenary.nvim", -- Required: async utilities
|
||||
"nvim-treesitter/nvim-treesitter", -- Required: scope detection
|
||||
"nvim-treesitter/nvim-treesitter-textobjects", -- Optional: text objects
|
||||
"MunifTanjim/nui.nvim", -- Optional: UI components
|
||||
},
|
||||
cmd = { "Coder", "CoderOpen", "CoderToggle", "CoderAgent" },
|
||||
keys = {
|
||||
{ "<leader>co", "<cmd>Coder open<cr>", desc = "Coder: Open" },
|
||||
@@ -167,6 +183,7 @@ require("codetyper").setup({
|
||||
escalation_threshold = 0.7, -- Below this confidence, escalate to remote
|
||||
max_concurrent = 2, -- Max parallel workers
|
||||
completion_delay_ms = 100, -- Delay injection after completion popup
|
||||
apply_delay_ms = 5000, -- Wait before applying code (ms), allows review
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -317,11 +334,54 @@ The plugin auto-detects prompt type:
|
||||
|
||||
| Keywords | Type | Behavior |
|
||||
|----------|------|----------|
|
||||
| `refactor`, `rewrite` | Refactor | Replaces code |
|
||||
| `add`, `create`, `implement` | Add | Inserts new code |
|
||||
| `document`, `comment` | Document | Adds documentation |
|
||||
| `complete`, `finish`, `implement`, `todo` | Complete | Completes function body (replaces scope) |
|
||||
| `refactor`, `rewrite`, `simplify` | Refactor | Replaces code |
|
||||
| `fix`, `debug`, `bug`, `error` | Fix | Fixes bugs (replaces scope) |
|
||||
| `add`, `create`, `generate` | Add | Inserts new code |
|
||||
| `document`, `comment`, `jsdoc` | Document | Adds documentation |
|
||||
| `optimize`, `performance`, `faster` | Optimize | Optimizes code (replaces scope) |
|
||||
| `explain`, `what`, `how` | Explain | Shows explanation only |
|
||||
|
||||
### Function Completion
|
||||
|
||||
When you write a prompt **inside** a function body, the plugin uses Tree-sitter to detect the enclosing scope and automatically switches to "complete" mode:
|
||||
|
||||
```typescript
|
||||
function getUserById(id: number): User | null {
|
||||
/@ return the user from the database by id, handle not found case @/
|
||||
}
|
||||
```
|
||||
|
||||
The LLM will complete the function body while keeping the exact same signature. The entire function scope is replaced with the completed version.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Logs Panel
|
||||
|
||||
The logs panel provides real-time visibility into LLM operations:
|
||||
|
||||
### Features
|
||||
|
||||
- **Generation Logs**: Shows all LLM requests, responses, and token usage
|
||||
- **Queue Display**: Shows pending and processing prompts
|
||||
- **Full Response View**: Complete LLM responses are logged for debugging
|
||||
- **Auto-cleanup**: Logs panel and queue windows automatically close when exiting Neovim
|
||||
|
||||
### Opening the Logs Panel
|
||||
|
||||
```vim
|
||||
:CoderLogs
|
||||
```
|
||||
|
||||
The logs panel opens automatically when processing prompts with the scheduler enabled.
|
||||
|
||||
### Keymaps
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `q` | Close logs panel |
|
||||
| `<Esc>` | Close logs panel |
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Agent Mode
|
||||
|
||||
@@ -160,4 +160,18 @@ function M.is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
-- Close context modal when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
M.close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -230,9 +230,22 @@ function M.format_entry(entry)
|
||||
response = "<",
|
||||
tool = "T",
|
||||
error = "!",
|
||||
warning = "?",
|
||||
success = "i",
|
||||
queue = "Q",
|
||||
patch = "P",
|
||||
})[entry.level] or "?"
|
||||
|
||||
return string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
|
||||
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
|
||||
|
||||
-- If this is a response entry with raw_response, append the full response
|
||||
if entry.data and entry.data.raw_response then
|
||||
local response = entry.data.raw_response
|
||||
-- Add separator and the full response
|
||||
base = base .. "\n" .. string.rep("-", 40) .. "\n" .. response .. "\n" .. string.rep("-", 40)
|
||||
end
|
||||
|
||||
return base
|
||||
end
|
||||
|
||||
--- Estimate token count for a string (rough approximation)
|
||||
|
||||
@@ -397,11 +397,30 @@ local function remove_prompt_tags(bufnr)
|
||||
return removed
|
||||
end
|
||||
|
||||
--- Check if it's safe to modify the buffer (not in insert mode)
|
||||
---@return boolean
|
||||
local function is_safe_to_modify()
|
||||
local mode = vim.fn.mode()
|
||||
-- Don't modify if in insert mode or completion is visible
|
||||
if mode == "i" or mode == "ic" or mode == "ix" then
|
||||
return false
|
||||
end
|
||||
if vim.fn.pumvisible() == 1 then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Apply a patch to the target buffer
|
||||
---@param patch PatchCandidate
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
function M.apply(patch)
|
||||
-- Check if safe to modify (not in insert mode)
|
||||
if not is_safe_to_modify() then
|
||||
return false, "user_typing"
|
||||
end
|
||||
|
||||
-- Check staleness first
|
||||
local is_stale, stale_reason = M.is_stale(patch)
|
||||
if is_stale then
|
||||
@@ -454,14 +473,87 @@ function M.apply(patch)
|
||||
-- Apply based on strategy
|
||||
local ok, err = pcall(function()
|
||||
if patch.injection_strategy == "replace" and patch.injection_range then
|
||||
-- For replace, we need to adjust the range since we removed tags
|
||||
-- Just append to end since the original context might have shifted
|
||||
vim.api.nvim_buf_set_lines(target_bufnr, line_count, line_count, false, code_lines)
|
||||
-- Replace the scope range with the new code
|
||||
-- The injection_range points to the function/method we're completing
|
||||
local start_line = patch.injection_range.start_line
|
||||
local end_line = patch.injection_range.end_line
|
||||
|
||||
-- Adjust for tag removal - find the new range by searching for the scope
|
||||
-- After removing tags, line numbers may have shifted
|
||||
-- Use the scope information to find the correct range
|
||||
if patch.scope and patch.scope.type then
|
||||
-- Try to find the scope using treesitter if available
|
||||
local found_range = nil
|
||||
pcall(function()
|
||||
local ts_utils = require("nvim-treesitter.ts_utils")
|
||||
local parsers = require("nvim-treesitter.parsers")
|
||||
local parser = parsers.get_parser(target_bufnr)
|
||||
if parser then
|
||||
local tree = parser:parse()[1]
|
||||
if tree then
|
||||
local root = tree:root()
|
||||
-- Find the function/method node that contains our original position
|
||||
local function find_scope_node(node)
|
||||
local node_type = node:type()
|
||||
local is_scope = node_type:match("function")
|
||||
or node_type:match("method")
|
||||
or node_type:match("class")
|
||||
or node_type:match("declaration")
|
||||
|
||||
if is_scope then
|
||||
local s_row, _, e_row, _ = node:range()
|
||||
-- Check if this scope roughly matches our expected range
|
||||
if math.abs(s_row - (start_line - 1)) <= 5 then
|
||||
found_range = { start_line = s_row + 1, end_line = e_row + 1 }
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
for child in node:iter_children() do
|
||||
if find_scope_node(child) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
find_scope_node(root)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if found_range then
|
||||
start_line = found_range.start_line
|
||||
end_line = found_range.end_line
|
||||
end
|
||||
end
|
||||
|
||||
-- Clamp to valid range
|
||||
start_line = math.max(1, start_line)
|
||||
end_line = math.min(line_count, end_line)
|
||||
|
||||
-- Replace the range (0-indexed for nvim_buf_set_lines)
|
||||
vim.api.nvim_buf_set_lines(target_bufnr, start_line - 1, end_line, false, code_lines)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Replacing lines %d-%d with %d lines of code", start_line, end_line, #code_lines),
|
||||
})
|
||||
end)
|
||||
elseif patch.injection_strategy == "insert" and patch.injection_range then
|
||||
-- Insert at end since original position might have shifted
|
||||
vim.api.nvim_buf_set_lines(target_bufnr, line_count, line_count, false, code_lines)
|
||||
-- Insert at the specified location
|
||||
local insert_line = patch.injection_range.start_line
|
||||
insert_line = math.max(1, math.min(line_count + 1, insert_line))
|
||||
vim.api.nvim_buf_set_lines(target_bufnr, insert_line - 1, insert_line - 1, false, code_lines)
|
||||
else
|
||||
-- Default: append to end
|
||||
-- Check if last line is empty, if not add a blank line for spacing
|
||||
local last_line = vim.api.nvim_buf_get_lines(target_bufnr, line_count - 1, line_count, false)[1] or ""
|
||||
if last_line:match("%S") then
|
||||
-- Last line has content, add blank line for spacing
|
||||
table.insert(code_lines, 1, "")
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(target_bufnr, line_count, line_count, false, code_lines)
|
||||
end
|
||||
end)
|
||||
@@ -491,22 +583,27 @@ end
|
||||
--- Flush all pending patches that are safe to apply
|
||||
---@return number applied_count
|
||||
---@return number stale_count
|
||||
---@return number deferred_count
|
||||
function M.flush_pending()
|
||||
local applied = 0
|
||||
local stale = 0
|
||||
local deferred = 0
|
||||
|
||||
for _, patch in ipairs(patches) do
|
||||
if patch.status == "pending" then
|
||||
local success, _ = M.apply(patch)
|
||||
for _, p in ipairs(patches) do
|
||||
if p.status == "pending" then
|
||||
local success, err = M.apply(p)
|
||||
if success then
|
||||
applied = applied + 1
|
||||
elseif err == "user_typing" then
|
||||
-- Keep pending, will retry later
|
||||
deferred = deferred + 1
|
||||
else
|
||||
stale = stale + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return applied, stale
|
||||
return applied, stale, deferred
|
||||
end
|
||||
|
||||
--- Cancel all pending patches for a buffer
|
||||
|
||||
@@ -12,6 +12,9 @@ local worker = require("codetyper.agent.worker")
|
||||
local confidence_mod = require("codetyper.agent.confidence")
|
||||
local context_modal = require("codetyper.agent.context_modal")
|
||||
|
||||
-- Setup context modal cleanup on exit
|
||||
context_modal.setup()
|
||||
|
||||
--- Scheduler state
|
||||
local state = {
|
||||
running = false,
|
||||
@@ -24,6 +27,7 @@ local state = {
|
||||
escalation_threshold = 0.7,
|
||||
max_concurrent = 2,
|
||||
completion_delay_ms = 100,
|
||||
apply_delay_ms = 5000, -- Wait before applying code
|
||||
remote_provider = "claude", -- Default fallback provider
|
||||
},
|
||||
}
|
||||
@@ -228,8 +232,19 @@ local function handle_worker_result(event, result)
|
||||
|
||||
queue.complete(event.id)
|
||||
|
||||
-- Schedule patch application
|
||||
M.schedule_patch_flush()
|
||||
-- Schedule patch application after delay (gives user time to review/cancel)
|
||||
local delay = state.config.apply_delay_ms or 5000
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Code ready. Applying in %.1f seconds...", delay / 1000),
|
||||
})
|
||||
end)
|
||||
|
||||
vim.defer_fn(function()
|
||||
M.schedule_patch_flush()
|
||||
end, delay)
|
||||
end
|
||||
|
||||
--- Dispatch next event from queue
|
||||
@@ -291,11 +306,23 @@ local function dispatch_next()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Track if we're already waiting to flush (avoid spam logs)
|
||||
local waiting_to_flush = false
|
||||
|
||||
--- Schedule patch flush after delay (completion safety)
|
||||
--- Will keep retrying until safe to inject or no pending patches
|
||||
function M.schedule_patch_flush()
|
||||
vim.defer_fn(function()
|
||||
-- Check if there are any pending patches
|
||||
local pending = patch.get_pending()
|
||||
if #pending == 0 then
|
||||
waiting_to_flush = false
|
||||
return -- Nothing to apply
|
||||
end
|
||||
|
||||
local safe, reason = M.is_safe_to_inject()
|
||||
if safe then
|
||||
waiting_to_flush = false
|
||||
local applied, stale = patch.flush_pending()
|
||||
if applied > 0 or stale > 0 then
|
||||
pcall(function()
|
||||
@@ -307,15 +334,20 @@ function M.schedule_patch_flush()
|
||||
end)
|
||||
end
|
||||
else
|
||||
-- Not safe yet, reschedule
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "debug",
|
||||
message = string.format("Patch flush deferred: %s", reason or "unknown"),
|
||||
})
|
||||
end)
|
||||
-- Will be retried on next InsertLeave or CursorHold
|
||||
-- Not safe yet (user is typing), reschedule to try again
|
||||
-- Only log once when we start waiting
|
||||
if not waiting_to_flush then
|
||||
waiting_to_flush = true
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Waiting for user to finish typing before applying code...",
|
||||
})
|
||||
end)
|
||||
end
|
||||
-- Retry after a delay - keep waiting for user to finish typing
|
||||
M.schedule_patch_flush()
|
||||
end
|
||||
end, state.config.completion_delay_ms)
|
||||
end
|
||||
@@ -390,6 +422,15 @@ local function setup_autocmds()
|
||||
end,
|
||||
desc = "Cleanup on buffer delete",
|
||||
})
|
||||
|
||||
-- Stop scheduler when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = augroup,
|
||||
callback = function()
|
||||
M.stop()
|
||||
end,
|
||||
desc = "Stop scheduler before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
--- Start the scheduler
|
||||
|
||||
@@ -94,6 +94,15 @@ local function clean_response(response, filetype)
|
||||
|
||||
local cleaned = response
|
||||
|
||||
-- Remove LLM special tokens (deepseek, llama, etc.)
|
||||
cleaned = cleaned:gsub("<|begin▁of▁sentence|>", "")
|
||||
cleaned = cleaned:gsub("<|end▁of▁sentence|>", "")
|
||||
cleaned = cleaned:gsub("<|im_start|>", "")
|
||||
cleaned = cleaned:gsub("<|im_end|>", "")
|
||||
cleaned = cleaned:gsub("<s>", "")
|
||||
cleaned = cleaned:gsub("</s>", "")
|
||||
cleaned = cleaned:gsub("<|endoftext|>", "")
|
||||
|
||||
-- Remove the original prompt tags /@ ... @/ if they appear in output
|
||||
-- Use [%s%S] to match any character including newlines (Lua's . doesn't match newlines)
|
||||
cleaned = cleaned:gsub("/@[%s%S]-@/", "")
|
||||
@@ -264,8 +273,35 @@ local function build_prompt(event)
|
||||
local scope_type = event.scope.type
|
||||
local scope_name = event.scope.name or "anonymous"
|
||||
|
||||
-- For replacement intents, provide the full scope to transform
|
||||
if event.intent and intent_mod.is_replacement(event.intent) then
|
||||
-- Special handling for "complete" intent - fill in the function body
|
||||
if event.intent and event.intent.type == "complete" then
|
||||
user_prompt = string.format(
|
||||
[[Complete this %s. Fill in the implementation based on the description.
|
||||
|
||||
IMPORTANT:
|
||||
- Keep the EXACT same function signature (name, parameters, return type)
|
||||
- Only provide the COMPLETE function with implementation
|
||||
- Do NOT create a new function or duplicate the signature
|
||||
- Do NOT add any text before or after the function
|
||||
|
||||
Current %s (incomplete):
|
||||
```%s
|
||||
%s
|
||||
```
|
||||
%s
|
||||
What it should do: %s
|
||||
|
||||
Return ONLY the complete %s with implementation. No explanations, no duplicates.]],
|
||||
scope_type,
|
||||
scope_type,
|
||||
filetype,
|
||||
event.scope_text,
|
||||
attached_content,
|
||||
event.prompt_content,
|
||||
scope_type
|
||||
)
|
||||
-- For other replacement intents, provide the full scope to transform
|
||||
elseif event.intent and intent_mod.is_replacement(event.intent) then
|
||||
user_prompt = string.format(
|
||||
[[Here is a %s named "%s" in a %s file:
|
||||
|
||||
@@ -491,6 +527,18 @@ function M.complete(worker, response, error, usage)
|
||||
return
|
||||
end
|
||||
|
||||
-- Log the full raw LLM response (for debugging)
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "response",
|
||||
message = "--- LLM Response ---",
|
||||
data = {
|
||||
raw_response = response,
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
-- Clean the response (remove markdown, explanations, etc.)
|
||||
local filetype = vim.fn.fnamemodify(worker.event.target_path or "", ":e")
|
||||
local cleaned_response = clean_response(response, filetype)
|
||||
|
||||
@@ -332,10 +332,7 @@ function M.check_for_closed_prompt()
|
||||
-- Clean prompt content (strip file references)
|
||||
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
|
||||
|
||||
-- Detect intent from prompt
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
-- Resolve scope in target file (use prompt position to find enclosing scope)
|
||||
-- Resolve scope in target file FIRST (need it to adjust intent)
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr
|
||||
@@ -354,6 +351,24 @@ function M.check_for_closed_prompt()
|
||||
}
|
||||
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
|
||||
if 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
|
||||
|
||||
-- Determine priority based on intent
|
||||
local priority = 2 -- Normal
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
@@ -472,10 +487,7 @@ function M.check_all_prompts()
|
||||
-- Clean prompt content (strip file references)
|
||||
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
|
||||
|
||||
-- Detect intent from prompt
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
-- Resolve scope in target file
|
||||
-- Resolve scope in target file FIRST (need it to adjust intent)
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr -- Use current buffer if target not loaded
|
||||
@@ -494,6 +506,24 @@ function M.check_all_prompts()
|
||||
}
|
||||
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
|
||||
if 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
|
||||
|
||||
-- Determine priority based on intent
|
||||
local priority = 2
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
|
||||
@@ -46,6 +46,7 @@ local defaults = {
|
||||
escalation_threshold = 0.7, -- Below this confidence, escalate to remote LLM
|
||||
max_concurrent = 2, -- Maximum concurrent workers
|
||||
completion_delay_ms = 100, -- Wait after completion popup closes
|
||||
apply_delay_ms = 5000, -- Wait before removing tags and applying code (ms)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ function M.setup(opts)
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local tree = require("codetyper.tree")
|
||||
local completion = require("codetyper.completion")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
|
||||
-- Register commands
|
||||
commands.setup()
|
||||
@@ -41,6 +42,9 @@ function M.setup(opts)
|
||||
-- Setup file reference completion
|
||||
completion.setup()
|
||||
|
||||
-- Setup logs panel (handles VimLeavePre cleanup)
|
||||
logs_panel.setup()
|
||||
|
||||
-- Ensure .gitignore has coder files excluded
|
||||
gitignore.ensure_ignored()
|
||||
|
||||
|
||||
@@ -274,38 +274,48 @@ function M.open()
|
||||
end
|
||||
|
||||
--- Close the logs panel
|
||||
function M.close()
|
||||
if not state.is_open then
|
||||
---@param force? boolean Force close even if not marked as open
|
||||
function M.close(force)
|
||||
if not state.is_open and not force then
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove log listener
|
||||
if state.listener_id then
|
||||
logs.remove_listener(state.listener_id)
|
||||
pcall(logs.remove_listener, state.listener_id)
|
||||
state.listener_id = nil
|
||||
end
|
||||
|
||||
-- Remove queue listener
|
||||
if state.queue_listener_id then
|
||||
queue.remove_listener(state.queue_listener_id)
|
||||
pcall(queue.remove_listener, state.queue_listener_id)
|
||||
state.queue_listener_id = nil
|
||||
end
|
||||
|
||||
-- Close queue window
|
||||
if state.queue_win and vim.api.nvim_win_is_valid(state.queue_win) then
|
||||
-- Close queue window first
|
||||
if state.queue_win then
|
||||
pcall(vim.api.nvim_win_close, state.queue_win, true)
|
||||
state.queue_win = nil
|
||||
end
|
||||
|
||||
-- Close logs window
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
if state.win then
|
||||
pcall(vim.api.nvim_win_close, state.win, true)
|
||||
state.win = nil
|
||||
end
|
||||
|
||||
-- Delete queue buffer
|
||||
if state.queue_buf then
|
||||
pcall(vim.api.nvim_buf_delete, state.queue_buf, { force = true })
|
||||
state.queue_buf = nil
|
||||
end
|
||||
|
||||
-- Delete logs buffer
|
||||
if state.buf then
|
||||
pcall(vim.api.nvim_buf_delete, state.buf, { force = true })
|
||||
state.buf = nil
|
||||
end
|
||||
|
||||
-- Reset state
|
||||
state.buf = nil
|
||||
state.win = nil
|
||||
state.queue_buf = nil
|
||||
state.queue_win = nil
|
||||
state.is_open = false
|
||||
end
|
||||
|
||||
@@ -331,4 +341,42 @@ function M.ensure_open()
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup autocmds for the logs panel
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeLogsPanel", { clear = true })
|
||||
|
||||
-- Close logs panel when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
-- Force close to ensure cleanup even in edge cases
|
||||
M.close(true)
|
||||
end,
|
||||
desc = "Close logs panel before exiting Neovim",
|
||||
})
|
||||
|
||||
-- Also clean up when QuitPre fires (handles :qa, :wqa, etc.)
|
||||
vim.api.nvim_create_autocmd("QuitPre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
-- Check if this is the last window (about to quit Neovim)
|
||||
local wins = vim.api.nvim_list_wins()
|
||||
local real_wins = 0
|
||||
for _, win in ipairs(wins) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local buftype = vim.bo[buf].buftype
|
||||
-- Count non-special windows
|
||||
if buftype == "" or buftype == "help" then
|
||||
real_wins = real_wins + 1
|
||||
end
|
||||
end
|
||||
-- If only logs/queue windows remain, close them
|
||||
if real_wins <= 1 then
|
||||
M.close(true)
|
||||
end
|
||||
end,
|
||||
desc = "Close logs panel on quit",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("patch", function()
|
||||
local id2 = patch.generate_id()
|
||||
|
||||
assert.is_not.equals(id1, id2)
|
||||
assert.is_true(id1:match("^patch_"))
|
||||
assert.is_truthy(id1:match("^patch_"))
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -302,4 +302,70 @@ describe("patch", function()
|
||||
assert.equals(1, #patch.get_pending())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("create_from_event", function()
|
||||
it("should create patch with replace strategy for complete intent", function()
|
||||
local event = {
|
||||
id = "evt_123",
|
||||
target_path = "/tmp/test.lua",
|
||||
bufnr = 1,
|
||||
range = { start_line = 5, end_line = 10 },
|
||||
scope_range = { start_line = 3, end_line = 12 },
|
||||
scope = { type = "function", name = "test_fn" },
|
||||
intent = {
|
||||
type = "complete",
|
||||
action = "replace",
|
||||
confidence = 0.9,
|
||||
keywords = {},
|
||||
},
|
||||
}
|
||||
|
||||
local p = patch.create_from_event(event, "function code", 0.9)
|
||||
|
||||
assert.equals("replace", p.injection_strategy)
|
||||
assert.is_truthy(p.injection_range)
|
||||
assert.equals(3, p.injection_range.start_line)
|
||||
assert.equals(12, p.injection_range.end_line)
|
||||
end)
|
||||
|
||||
it("should create patch with append strategy for add intent", function()
|
||||
local event = {
|
||||
id = "evt_456",
|
||||
target_path = "/tmp/test.lua",
|
||||
bufnr = 1,
|
||||
range = { start_line = 5, end_line = 10 },
|
||||
intent = {
|
||||
type = "add",
|
||||
action = "append",
|
||||
confidence = 0.8,
|
||||
keywords = {},
|
||||
},
|
||||
}
|
||||
|
||||
local p = patch.create_from_event(event, "new function", 0.8)
|
||||
|
||||
assert.equals("append", p.injection_strategy)
|
||||
end)
|
||||
|
||||
it("should create patch with insert strategy for insert action", function()
|
||||
local event = {
|
||||
id = "evt_789",
|
||||
target_path = "/tmp/test.lua",
|
||||
bufnr = 1,
|
||||
range = { start_line = 5, end_line = 10 },
|
||||
intent = {
|
||||
type = "add",
|
||||
action = "insert",
|
||||
confidence = 0.8,
|
||||
keywords = {},
|
||||
},
|
||||
}
|
||||
|
||||
local p = patch.create_from_event(event, "inserted code", 0.8)
|
||||
|
||||
assert.equals("insert", p.injection_strategy)
|
||||
assert.is_truthy(p.injection_range)
|
||||
assert.equals(5, p.injection_range.start_line)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user