Fixing the old configuration

This commit is contained in:
2026-03-18 21:56:45 -04:00
parent f6266c7d94
commit 9f229b26c9
51 changed files with 791 additions and 6549 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,4 @@
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/
.claude/
Makefile

View File

@@ -67,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Cost Tracking System** - Track LLM API costs across sessions
- New `:CoderCost` command opens cost estimation floating window
- Session costs tracked in real-time
- All-time costs persisted in `.coder/cost_history.json`
- All-time costs persisted in `.codetyper/cost_history.json`
- Per-model breakdown with token counts
- Pricing database for 50+ models (GPT-4/5, Claude, O-series, Gemini)
- Window keymaps: `q` close, `r` refresh, `c` clear session, `C` clear all
@@ -179,7 +179,7 @@ scheduler = {
- Default keymaps: `<leader>ctt`, `<leader>ctT`
- **Auto-Index Feature** - Automatically create coder companion files
- Creates `.coder.` companion files when opening source files
- Creates `.codetyper/` companion files when opening source files
- Language-aware templates
- **Logs Panel** - Real-time visibility into LLM operations

View File

@@ -24,7 +24,7 @@
- **Auto-Index**: Automatically create coder companion files on file open
- **Logs Panel**: Real-time visibility into LLM requests and token usage
- **Cost Tracking**: Persistent LLM cost estimation with session and all-time stats
- **Git Integration**: Automatically adds `.coder.*` files to `.gitignore`
- **Git Integration**: Automatically adds `.codetyper/*` files to `.gitignore`
- **Project Tree Logging**: Maintains a `tree.log` tracking your project structure
- **Brain System**: Knowledge graph that learns from your coding patterns
@@ -179,7 +179,7 @@ require("codetyper").setup({
patterns = {
open_tag = "/@",
close_tag = "@/",
file_pattern = "*.coder.*",
file_pattern = "*.codetyper/*",
},
-- Auto Features
@@ -310,7 +310,7 @@ llm = {
|---------|-------|-------------|
| `:Coder agentic-run <task>` | `:CoderAgenticRun` | Run agentic task |
| `:Coder agentic-list` | `:CoderAgenticList` | List available agents |
| `:Coder agentic-init` | `:CoderAgenticInit` | Initialize .coder/agents/ |
| `:Coder agentic-init` | `:CoderAgenticInit` | Initialize .codetyper/agents/ |
### Transform Commands
@@ -646,7 +646,7 @@ Features:
- Session and all-time statistics
- Per-model breakdown
- Pricing for 50+ models
- Persistent history in `.coder/cost_history.json`
- Persistent history in `.codetyper/cost_history.json`
---
@@ -682,7 +682,7 @@ Autonomous coding assistant with tool access:
```
your-project/
├── .coder/
├── .codetyper/
│ ├── tree.log
│ ├── cost_history.json
│ ├── brain/
@@ -690,7 +690,7 @@ your-project/
│ └── rules/
├── src/
│ ├── index.ts
│ └── index.coder.ts
│ └── index.codetyper/ts
└── .gitignore
```

View File

@@ -27,7 +27,6 @@ faster using LLM APIs with a unique workflow.
Key features:
- Split view with coder file and target file side by side
- Prompt-based code generation using /@ ... @/ tags
- Support for Claude, OpenAI, Gemini, Copilot, and Ollama providers
- Agent mode with autonomous tool use (read, edit, write, bash)
- Transform commands for inline prompt processing
@@ -106,11 +105,6 @@ Default configuration: >lua
position = "left",
border = "rounded",
},
patterns = {
open_tag = "/@",
close_tag = "@/",
file_pattern = "*.coder.*",
},
auto_gitignore = true,
auto_open_ask = true,
auto_index = false, -- Auto-create coder companion files
@@ -178,8 +172,8 @@ Run models locally with no API costs.
2. Run `:Coder open` to create/open the corresponding coder file
3. In the coder file, write prompts using the tag syntax:
>
/@ Create a function that fetches user data from an API
with error handling and returns a User object @/
Create a function that fetches user data from an API
with error handling and returns a User object
<
4. When you close the tag with `@/`, the plugin will:
- Send the prompt to the configured LLM
@@ -252,17 +246,6 @@ The plugin detects the type of request from your prompt:
Stop the currently running agent.
*:CoderTransform*
:CoderTransform
Transform all /@ @/ tags in the current file.
*:CoderTransformCursor*
:CoderTransformCursor
Transform the /@ @/ tag at cursor position.
*:CoderTransformVisual*
:CoderTransformVisual
Transform selected /@ @/ tags (visual mode).
:CoderLogs
Toggle the logs panel showing LLM request details.
@@ -272,7 +255,7 @@ The plugin detects the type of request from your prompt:
*:CoderTree*
:CoderTree
Manually refresh the tree.log file in .coder/ folder.
Manually refresh the tree.log file in .codetyper/ folder.
*:CoderTreeView*
:CoderTreeView
@@ -306,28 +289,28 @@ q Close agent panel
==============================================================================
9. TRANSFORM COMMANDS *codetyper-transform*
Transform commands allow you to process /@ @/ tags inline without
Transform commands allow you to process tags inline without
opening the split view.
*:CoderTransform*
:CoderTransform
Find and transform all /@ @/ tags in the current buffer.
Find and transform all tags in the current buffer.
Each tag is replaced with generated code.
*:CoderTransformCursor*
:CoderTransformCursor
Transform the /@ @/ tag at the current cursor position.
Transform the tag at the current cursor position.
Useful for processing a single prompt.
*:CoderTransformVisual*
:'<,'>CoderTransformVisual
Transform /@ @/ tags within the visual selection.
Transform tags within the visual selection.
Select lines containing tags and run this command.
Example~
>
// In your source file:
/@ Add input validation for email @/
Add input validation for email
// After running :CoderTransformCursor:
function validateEmail(email) {

View File

@@ -11,7 +11,7 @@ Codetyper.nvim is a Neovim plugin written in Lua that acts as an AI-powered codi
Instead of having an AI generate entire files, Codetyper lets developers maintain control:
1. Developer opens a source file (e.g., `index.ts`)
2. A companion "coder file" is created (`index.coder.ts`)
2. A companion "coder file" is created (`index.codetyper/ts`)
3. Developer writes prompts using special tags: `/@ prompt @/`
4. When the closing tag is typed, the LLM generates code
5. Generated code is shown as a conflict for review
@@ -68,10 +68,10 @@ lua/codetyper/
└── agent.lua # Agent-specific prompts
```
## .coder/ Folder
## .codetyper/ Folder
```
.coder/
.codetyper/
├── tree.log # Project structure, auto-updated
├── cost_history.json # LLM cost tracking history
├── brain/ # Knowledge graph storage
@@ -239,7 +239,7 @@ end
Track LLM API costs:
- Session costs tracked in real-time
- All-time costs in `.coder/cost_history.json`
- All-time costs in `.codetyper/cost_history.json`
- Pricing for 50+ models
### 10. Credentials Management
@@ -405,7 +405,7 @@ Stored in `~/.local/share/nvim/codetyper/configuration.json`
patterns = {
open_tag = "/@",
close_tag = "@/",
file_pattern = "*.coder.*",
file_pattern = "*.codetyper/*",
},
auto_gitignore = true,
auto_open_ask = true,
@@ -461,11 +461,11 @@ tools = {
| Target File | Coder File |
|-------------|------------|
| `index.ts` | `index.coder.ts` |
| `utils.py` | `utils.coder.py` |
| `main.lua` | `main.coder.lua` |
| `index.ts` | `index.codetyper/ts` |
| `utils.py` | `utils.codetyper/py` |
| `main.lua` | `main.codetyper/lua` |
Pattern: `name.coder.extension`
Pattern: `name.codetyper/extension`
## Dependencies

View File

@@ -154,7 +154,7 @@ function M.setup()
-- Auto-set filetype for coder files based on extension
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
group = group,
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function()
M.set_coder_filetype()
end,
@@ -164,7 +164,7 @@ function M.setup()
-- 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.*",
pattern = "*.codetyper/*",
callback = function()
-- Delay slightly to ensure buffer is fully loaded
vim.defer_fn(function()
@@ -177,7 +177,7 @@ function M.setup()
-- Cleanup on buffer close
vim.api.nvim_create_autocmd("BufWipeout", {
group = group,
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function(ev)
local window = require("codetyper.adapters.nvim.windows")
if window.is_open() then
@@ -203,11 +203,11 @@ function M.setup()
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
if filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
-- Skip non-project files
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.coder/") then
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.codetyper/") then
return
end
-- Schedule tree update with debounce
@@ -237,7 +237,7 @@ function M.setup()
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
if filepath == "" or filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
schedule_tree_update()
@@ -286,23 +286,6 @@ function M.setup()
thinking.setup()
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
--- Create extmarks for injection range so position survives user edits (99-style).
---@param target_bufnr number Target buffer (where code will be injected)
---@param range { start_line: number, end_line: number } Range to mark (1-based)
@@ -383,7 +366,6 @@ function M.check_for_closed_prompt()
end
is_processing = true
local config = get_config_safe()
local parser = require("codetyper.parser")
local bufnr = vim.api.nvim_get_current_buf()
@@ -793,7 +775,6 @@ 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.config.preferences")
local parser = require("codetyper.parser")
-- First check if there are any prompts to process
@@ -803,27 +784,6 @@ function M.check_for_closed_prompt_with_preference()
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()
@@ -857,27 +817,6 @@ function M.check_all_prompts_with_preference()
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()
@@ -1006,8 +945,8 @@ end
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+)$")
-- Extract the actual extension (e.g., index.codetyper/ts -> ts)
local ext = filepath:match("%.codetyper%.(%w+)$")
if ext then
-- Map extension to filetype
@@ -1196,7 +1135,7 @@ end
--- Directories to ignore for coder file creation
local ignored_directories = {
".git",
".coder",
".codetyper",
".claude",
".vscode",
".idea",
@@ -1432,7 +1371,6 @@ function M.auto_index_file(bufnr)
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
@@ -1534,12 +1472,11 @@ function M.auto_index_file(bufnr)
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
@@ -1568,7 +1505,6 @@ function M.auto_index_file(bufnr)
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(pseudo_code, comment_prefix .. " Use /@ @/ tags below to request code generation:")
table.insert(
pseudo_code,
comment_prefix

View File

@@ -1,7 +1,5 @@
---@mod codetyper.logs_panel Standalone logs panel for code generation
---
--- Shows real-time logs when generating code via /@ @/ prompts.
local M = {}
local logs = require("codetyper.adapters.nvim.ui.logs")
@@ -17,13 +15,13 @@ local queue = require("codetyper.core.events.queue")
---@field queue_listener_id number|nil Listener ID for queue
local state = {
buf = nil,
win = nil,
queue_buf = nil,
queue_win = nil,
is_open = false,
listener_id = nil,
queue_listener_id = nil,
buf = nil,
win = nil,
queue_buf = nil,
queue_win = nil,
is_open = false,
listener_id = nil,
queue_listener_id = nil,
}
--- Namespace for highlights
@@ -37,346 +35,346 @@ local QUEUE_HEIGHT = 8
--- Add a log entry to the buffer
---@param entry table Log entry
local function add_log_entry(entry)
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
vim.schedule(function()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
vim.schedule(function()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
-- Handle clear event
if entry.level == "clear" then
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
return
end
-- Handle clear event
if entry.level == "clear" then
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
return
end
vim.bo[state.buf].modifiable = true
vim.bo[state.buf].modifiable = true
local formatted = logs.format_entry(entry)
local formatted_lines = vim.split(formatted, "\n", { plain = true })
local line_count = vim.api.nvim_buf_line_count(state.buf)
local formatted = logs.format_entry(entry)
local formatted_lines = vim.split(formatted, "\n", { plain = true })
local line_count = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, -1, -1, false, formatted_lines)
vim.api.nvim_buf_set_lines(state.buf, -1, -1, false, formatted_lines)
-- Apply highlighting based on level
local hl_map = {
info = "DiagnosticInfo",
debug = "Comment",
request = "DiagnosticWarn",
response = "DiagnosticOk",
tool = "DiagnosticHint",
error = "DiagnosticError",
}
-- Apply highlighting based on level
local hl_map = {
info = "DiagnosticInfo",
debug = "Comment",
request = "DiagnosticWarn",
response = "DiagnosticOk",
tool = "DiagnosticHint",
error = "DiagnosticError",
}
local hl = hl_map[entry.level] or "Normal"
for i = 0, #formatted_lines - 1 do
vim.api.nvim_buf_add_highlight(state.buf, ns_logs, hl, line_count + i, 0, -1)
end
local hl = hl_map[entry.level] or "Normal"
for i = 0, #formatted_lines - 1 do
vim.api.nvim_buf_add_highlight(state.buf, ns_logs, hl, line_count + i, 0, -1)
end
vim.bo[state.buf].modifiable = false
vim.bo[state.buf].modifiable = false
-- Auto-scroll logs
if state.win and vim.api.nvim_win_is_valid(state.win) then
local new_count = vim.api.nvim_buf_line_count(state.buf)
pcall(vim.api.nvim_win_set_cursor, state.win, { new_count, 0 })
end
end)
-- Auto-scroll logs
if state.win and vim.api.nvim_win_is_valid(state.win) then
local new_count = vim.api.nvim_buf_line_count(state.buf)
pcall(vim.api.nvim_win_set_cursor, state.win, { new_count, 0 })
end
end)
end
--- Update the title with token counts
local function update_title()
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
return
end
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
return
end
local prompt_tokens, response_tokens = logs.get_token_totals()
local provider, model = logs.get_provider_info()
local prompt_tokens, response_tokens = logs.get_token_totals()
local provider, model = logs.get_provider_info()
if provider and state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.bo[state.buf].modifiable = true
local title = string.format("%s | %d/%d tokens", (provider or ""):upper(), prompt_tokens, response_tokens)
vim.api.nvim_buf_set_lines(state.buf, 0, 1, false, { title })
vim.bo[state.buf].modifiable = false
end
if provider and state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.bo[state.buf].modifiable = true
local title = string.format("%s | %d/%d tokens", (provider or ""):upper(), prompt_tokens, response_tokens)
vim.api.nvim_buf_set_lines(state.buf, 0, 1, false, { title })
vim.bo[state.buf].modifiable = false
end
end
--- Update the queue display
local function update_queue_display()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.schedule(function()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.schedule(function()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.bo[state.queue_buf].modifiable = true
vim.bo[state.queue_buf].modifiable = true
local lines = {
"Queue",
string.rep("", LOGS_WIDTH - 2),
}
local lines = {
"Queue",
string.rep("", LOGS_WIDTH - 2),
}
-- Get all events (pending and processing)
local pending = queue.get_pending()
local processing = queue.get_processing()
-- Get all events (pending and processing)
local pending = queue.get_pending()
local processing = queue.get_processing()
-- Add processing events first
for _, event in ipairs(processing) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("▶ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add processing events first
for _, event in ipairs(processing) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("▶ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add pending events
for _, event in ipairs(pending) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("○ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add pending events
for _, event in ipairs(pending) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("○ %s:%d %s", filename, line_num, prompt_preview))
end
if #pending == 0 and #processing == 0 then
table.insert(lines, " (empty)")
end
if #pending == 0 and #processing == 0 then
table.insert(lines, " (empty)")
end
vim.api.nvim_buf_set_lines(state.queue_buf, 0, -1, false, lines)
vim.api.nvim_buf_set_lines(state.queue_buf, 0, -1, false, lines)
-- Apply highlights
vim.api.nvim_buf_clear_namespace(state.queue_buf, ns_queue, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Title", 0, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", 1, 0, -1)
-- Apply highlights
vim.api.nvim_buf_clear_namespace(state.queue_buf, ns_queue, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Title", 0, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", 1, 0, -1)
local line_idx = 2
for _ = 1, #processing do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "DiagnosticWarn", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "String", line_idx, 2, -1)
line_idx = line_idx + 1
end
for _ = 1, #pending do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Normal", line_idx, 2, -1)
line_idx = line_idx + 1
end
local line_idx = 2
for _ = 1, #processing do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "DiagnosticWarn", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "String", line_idx, 2, -1)
line_idx = line_idx + 1
end
for _ = 1, #pending do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Normal", line_idx, 2, -1)
line_idx = line_idx + 1
end
vim.bo[state.queue_buf].modifiable = false
end)
vim.bo[state.queue_buf].modifiable = false
end)
end
--- Open the logs panel
function M.open()
if state.is_open then
return
end
if state.is_open then
return
end
-- Clear previous logs
logs.clear()
-- Clear previous logs
logs.clear()
-- Create logs buffer
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "hide"
vim.bo[state.buf].swapfile = false
-- Create logs buffer
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "hide"
vim.bo[state.buf].swapfile = false
-- Create window on the right
vim.cmd("botright vsplit")
state.win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.win, state.buf)
vim.api.nvim_win_set_width(state.win, LOGS_WIDTH)
-- Create window on the right
vim.cmd("botright vsplit")
state.win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.win, state.buf)
vim.api.nvim_win_set_width(state.win, LOGS_WIDTH)
-- Window options for logs
vim.wo[state.win].number = false
vim.wo[state.win].relativenumber = false
vim.wo[state.win].signcolumn = "no"
vim.wo[state.win].wrap = true
vim.wo[state.win].linebreak = true
vim.wo[state.win].winfixwidth = true
vim.wo[state.win].cursorline = false
-- Window options for logs
vim.wo[state.win].number = false
vim.wo[state.win].relativenumber = false
vim.wo[state.win].signcolumn = "no"
vim.wo[state.win].wrap = true
vim.wo[state.win].linebreak = true
vim.wo[state.win].winfixwidth = true
vim.wo[state.win].cursorline = false
-- Set initial content for logs
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
-- Set initial content for logs
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
-- Create queue buffer
state.queue_buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.queue_buf].buftype = "nofile"
vim.bo[state.queue_buf].bufhidden = "hide"
vim.bo[state.queue_buf].swapfile = false
-- Create queue buffer
state.queue_buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.queue_buf].buftype = "nofile"
vim.bo[state.queue_buf].bufhidden = "hide"
vim.bo[state.queue_buf].swapfile = false
-- Create queue window as horizontal split at bottom of logs window
vim.cmd("belowright split")
state.queue_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.queue_win, state.queue_buf)
vim.api.nvim_win_set_height(state.queue_win, QUEUE_HEIGHT)
-- Create queue window as horizontal split at bottom of logs window
vim.cmd("belowright split")
state.queue_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.queue_win, state.queue_buf)
vim.api.nvim_win_set_height(state.queue_win, QUEUE_HEIGHT)
-- Window options for queue
vim.wo[state.queue_win].number = false
vim.wo[state.queue_win].relativenumber = false
vim.wo[state.queue_win].signcolumn = "no"
vim.wo[state.queue_win].wrap = true
vim.wo[state.queue_win].linebreak = true
vim.wo[state.queue_win].winfixheight = true
vim.wo[state.queue_win].cursorline = false
-- Window options for queue
vim.wo[state.queue_win].number = false
vim.wo[state.queue_win].relativenumber = false
vim.wo[state.queue_win].signcolumn = "no"
vim.wo[state.queue_win].wrap = true
vim.wo[state.queue_win].linebreak = true
vim.wo[state.queue_win].winfixheight = true
vim.wo[state.queue_win].cursorline = false
-- Setup keymaps for logs buffer
local opts = { buffer = state.buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, opts)
vim.keymap.set("n", "<Esc>", M.close, opts)
-- Setup keymaps for logs buffer
local opts = { buffer = state.buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, opts)
vim.keymap.set("n", "<Esc>", M.close, opts)
-- Setup keymaps for queue buffer
local queue_opts = { buffer = state.queue_buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, queue_opts)
vim.keymap.set("n", "<Esc>", M.close, queue_opts)
-- Setup keymaps for queue buffer
local queue_opts = { buffer = state.queue_buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, queue_opts)
vim.keymap.set("n", "<Esc>", M.close, queue_opts)
-- Register log listener
state.listener_id = logs.add_listener(function(entry)
add_log_entry(entry)
if entry.level == "response" then
vim.schedule(update_title)
end
end)
-- Register log listener
state.listener_id = logs.add_listener(function(entry)
add_log_entry(entry)
if entry.level == "response" then
vim.schedule(update_title)
end
end)
-- Register queue listener
state.queue_listener_id = queue.add_listener(function()
update_queue_display()
end)
-- Register queue listener
state.queue_listener_id = queue.add_listener(function()
update_queue_display()
end)
-- Initial queue display
update_queue_display()
-- Initial queue display
update_queue_display()
state.is_open = true
state.is_open = true
-- Return focus to previous window
vim.cmd("wincmd p")
-- Return focus to previous window
vim.cmd("wincmd p")
logs.info("Logs panel opened")
logs.info("Logs panel opened")
end
--- Close the logs panel
---@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
if not state.is_open and not force then
return
end
-- Remove log listener
if state.listener_id then
pcall(logs.remove_listener, state.listener_id)
state.listener_id = nil
end
-- Remove log listener
if state.listener_id then
pcall(logs.remove_listener, state.listener_id)
state.listener_id = nil
end
-- Remove queue listener
if state.queue_listener_id then
pcall(queue.remove_listener, state.queue_listener_id)
state.queue_listener_id = nil
end
-- Remove queue listener
if state.queue_listener_id then
pcall(queue.remove_listener, state.queue_listener_id)
state.queue_listener_id = nil
end
-- 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 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 then
pcall(vim.api.nvim_win_close, state.win, true)
state.win = nil
end
-- Close logs window
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 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
-- Delete logs buffer
if state.buf then
pcall(vim.api.nvim_buf_delete, state.buf, { force = true })
state.buf = nil
end
state.is_open = false
state.is_open = false
end
--- Toggle the logs panel
function M.toggle()
if state.is_open then
M.close()
else
M.open()
end
if state.is_open then
M.close()
else
M.open()
end
end
--- Check if panel is open
---@return boolean
function M.is_open()
return state.is_open
return state.is_open
end
--- Ensure panel is open (call before starting generation)
function M.ensure_open()
if not state.is_open then
M.open()
end
if not state.is_open then
M.open()
end
end
--- Setup autocmds for the logs panel
function M.setup()
local group = vim.api.nvim_create_augroup("CodetypeLogsPanel", { clear = true })
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",
})
-- 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",
})
-- 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

View File

@@ -30,7 +30,7 @@ local defaults = {
auto_index = true, -- Index files on save
index_on_open = false, -- Index project when opening
max_file_size = 100000, -- Skip files larger than 100KB
excluded_dirs = { "node_modules", "dist", "build", ".git", ".coder", "__pycache__", "vendor", "target" },
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
memory = {
enabled = true, -- Enable memory persistence

View File

@@ -1,6 +1,6 @@
---@mod codetyper.preferences User preferences management
---@brief [[
--- Manages user preferences stored in .coder/preferences.json
--- Manages user preferences stored in .codetyper/preferences.json
--- Allows per-project configuration of plugin behavior.
---@brief ]]
@@ -9,13 +9,11 @@ local M = {}
local utils = require("codetyper.support.utils")
---@class CoderPreferences
---@field auto_process boolean Whether to auto-process /@ @/ tags (default: nil = ask)
---@field asked_auto_process boolean Whether we've asked the user about auto_process
--- Default preferences
local defaults = {
auto_process = nil, -- nil means "not yet decided"
asked_auto_process = false,
auto_process = nil, -- nil means "not yet decided"
asked_auto_process = false,
}
--- Cached preferences per project
@@ -25,190 +23,113 @@ local cache = {}
--- Get the preferences file path for current project
---@return string
local function get_preferences_path()
local cwd = vim.fn.getcwd()
return cwd .. "/.coder/preferences.json"
local cwd = vim.fn.getcwd()
return cwd .. "/.codetyper/preferences.json"
end
--- Ensure .coder directory exists
--- Ensure .codetyper directory exists
local function ensure_coder_dir()
local cwd = vim.fn.getcwd()
local coder_dir = cwd .. "/.coder"
if vim.fn.isdirectory(coder_dir) == 0 then
vim.fn.mkdir(coder_dir, "p")
end
local cwd = vim.fn.getcwd()
local coder_dir = cwd .. "/.codetyper"
if vim.fn.isdirectory(coder_dir) == 0 then
vim.fn.mkdir(coder_dir, "p")
end
end
--- Load preferences from file
---@return CoderPreferences
function M.load()
local cwd = vim.fn.getcwd()
local cwd = vim.fn.getcwd()
-- Check cache first
if cache[cwd] then
return cache[cwd]
end
-- Check cache first
if cache[cwd] then
return cache[cwd]
end
local path = get_preferences_path()
local prefs = vim.deepcopy(defaults)
local path = get_preferences_path()
local prefs = vim.deepcopy(defaults)
if utils.file_exists(path) then
local content = utils.read_file(path)
if content then
local ok, decoded = pcall(vim.json.decode, content)
if ok and decoded then
-- Merge with defaults
for k, v in pairs(decoded) do
prefs[k] = v
end
end
end
end
if utils.file_exists(path) then
local content = utils.read_file(path)
if content then
local ok, decoded = pcall(vim.json.decode, content)
if ok and decoded then
-- Merge with defaults
for k, v in pairs(decoded) do
prefs[k] = v
end
end
end
end
-- Cache it
cache[cwd] = prefs
return prefs
-- Cache it
cache[cwd] = prefs
return prefs
end
--- Save preferences to file
---@param prefs CoderPreferences
function M.save(prefs)
local cwd = vim.fn.getcwd()
ensure_coder_dir()
local cwd = vim.fn.getcwd()
ensure_coder_dir()
local path = get_preferences_path()
local ok, encoded = pcall(vim.json.encode, prefs)
if ok then
utils.write_file(path, encoded)
-- Update cache
cache[cwd] = prefs
end
local path = get_preferences_path()
local ok, encoded = pcall(vim.json.encode, prefs)
if ok then
utils.write_file(path, encoded)
-- Update cache
cache[cwd] = prefs
end
end
--- Get a specific preference
---@param key string
---@return any
function M.get(key)
local prefs = M.load()
return prefs[key]
local prefs = M.load()
return prefs[key]
end
--- Set a specific preference
---@param key string
---@param value any
function M.set(key, value)
local prefs = M.load()
prefs[key] = value
M.save(prefs)
local prefs = M.load()
prefs[key] = value
M.save(prefs)
end
--- Check if auto-process is enabled
---@return boolean|nil Returns true/false if set, nil if not yet decided
function M.is_auto_process_enabled()
return M.get("auto_process")
return M.get("auto_process")
end
--- Set auto-process preference
---@param enabled boolean
function M.set_auto_process(enabled)
M.set("auto_process", enabled)
M.set("asked_auto_process", true)
M.set("auto_process", enabled)
M.set("asked_auto_process", true)
end
--- Check if we've already asked the user about auto-process
---@return boolean
function M.has_asked_auto_process()
return M.get("asked_auto_process") == true
end
--- Ask user about auto-process preference (shows floating window)
---@param callback function(enabled: boolean) Called with user's choice
function M.ask_auto_process_preference(callback)
-- Check if already asked
if M.has_asked_auto_process() then
local enabled = M.is_auto_process_enabled()
if enabled ~= nil then
callback(enabled)
return
end
end
-- Create floating window to ask
local width = 60
local height = 7
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "wipe"
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
row = row,
col = col,
width = width,
height = height,
style = "minimal",
border = "rounded",
title = " Codetyper Preferences ",
title_pos = "center",
})
local lines = {
"",
" How would you like to process /@ @/ prompt tags?",
"",
" [a] Automatic - Process when you close the tag",
" [m] Manual - Only process with :CoderProcess",
"",
" Press 'a' or 'm' to choose (Esc to cancel)",
}
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
-- Highlight
local ns = vim.api.nvim_create_namespace("codetyper_prefs")
vim.api.nvim_buf_add_highlight(buf, ns, "Title", 1, 0, -1)
vim.api.nvim_buf_add_highlight(buf, ns, "String", 3, 2, 5)
vim.api.nvim_buf_add_highlight(buf, ns, "String", 4, 2, 5)
local function close_and_callback(enabled)
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
if enabled ~= nil then
M.set_auto_process(enabled)
local mode = enabled and "automatic" or "manual"
vim.notify("Codetyper: Set to " .. mode .. " mode (saved to .coder/preferences.json)", vim.log.levels.INFO)
end
if callback then
callback(enabled)
end
end
-- Keymaps
local opts = { buffer = buf, noremap = true, silent = true }
vim.keymap.set("n", "a", function() close_and_callback(true) end, opts)
vim.keymap.set("n", "A", function() close_and_callback(true) end, opts)
vim.keymap.set("n", "m", function() close_and_callback(false) end, opts)
vim.keymap.set("n", "M", function() close_and_callback(false) end, opts)
vim.keymap.set("n", "<Esc>", function() close_and_callback(nil) end, opts)
vim.keymap.set("n", "q", function() close_and_callback(nil) end, opts)
return M.get("asked_auto_process") == true
end
--- Clear cached preferences (useful when changing projects)
function M.clear_cache()
cache = {}
cache = {}
end
--- Toggle auto-process mode
function M.toggle_auto_process()
local current = M.is_auto_process_enabled()
local new_value = not current
M.set_auto_process(new_value)
local mode = new_value and "automatic" or "manual"
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
local current = M.is_auto_process_enabled()
local new_value = not current
M.set_auto_process(new_value)
local mode = new_value and "automatic" or "manual"
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
end
return M

View File

@@ -15,7 +15,7 @@ local COST_HISTORY_FILE = "cost_history.json"
---@return string File path
local function get_history_path()
local root = utils.get_project_root()
return root .. "/.coder/" .. COST_HISTORY_FILE
return root .. "/.codetyper/" .. COST_HISTORY_FILE
end
--- Default model for savings comparison (what you'd pay if not using Ollama)

View File

@@ -213,9 +213,9 @@ function M.create_from_event(event, generated_code, confidence, strategy)
end
end
-- Detect if this is an inline prompt (source == target, not a .coder. file)
-- Detect if this is an inline prompt (source == target, not a .codetyper/ file)
local is_inline = (source_bufnr == target_bufnr) or
(event.target_path and not event.target_path:match("%.coder%."))
(event.target_path and not event.target_path:match("%.codetyper%."))
-- Take snapshot of the scope range in target buffer (for staleness detection)
local snapshot_range = event.scope_range or event.range
@@ -452,89 +452,6 @@ function M.mark_rejected(id, reason)
return false
end
--- Remove /@ @/ prompt tags from buffer
---@param bufnr number Buffer number
---@return number Number of tag regions removed
local function remove_prompt_tags(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) then
return 0
end
local removed = 0
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-- Find and remove all /@ ... @/ regions (can be multiline)
local i = 1
while i <= #lines do
local line = lines[i]
local open_start = line:find("/@")
if open_start then
-- Found an opening tag, look for closing tag
local close_end = nil
local close_line = i
-- Check if closing tag is on same line
local after_open = line:sub(open_start + 2)
local same_line_close = after_open:find("@/")
if same_line_close then
-- Single line tag - remove just this portion
local before = line:sub(1, open_start - 1)
local after = line:sub(open_start + 2 + same_line_close + 1)
lines[i] = before .. after
-- If line is now empty or just whitespace, remove it
if lines[i]:match("^%s*$") then
table.remove(lines, i)
else
i = i + 1
end
removed = removed + 1
else
-- Multi-line tag - find the closing line
for j = i, #lines do
if lines[j]:find("@/") then
close_line = j
close_end = lines[j]:find("@/")
break
end
end
if close_end then
-- Remove lines from i to close_line
-- Keep content before /@ on first line and after @/ on last line
local before = lines[i]:sub(1, open_start - 1)
local after = lines[close_line]:sub(close_end + 2)
-- Remove the lines containing the tag
for _ = i, close_line do
table.remove(lines, i)
end
-- If there's content to keep, insert it back
local remaining = (before .. after):match("^%s*(.-)%s*$")
if remaining and remaining ~= "" then
table.insert(lines, i, remaining)
i = i + 1
end
removed = removed + 1
else
-- No closing tag found, skip this line
i = i + 1
end
end
else
i = i + 1
end
end
if removed > 0 then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return removed
end
--- Check if it's safe to modify the buffer (not in insert or visual mode)
---@return boolean
local function is_safe_to_modify()
@@ -625,46 +542,9 @@ function M.apply(patch)
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
local tags_removed = 0
-- For CODER FILES (source != target): Remove tags from source, inject into target
-- For INLINE PROMPTS (source == target): Include tag range in injection, no separate removal
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
tags_removed = remove_prompt_tags(source_bufnr)
pcall(function()
if tags_removed > 0 then
local logs = require("codetyper.adapters.nvim.ui.logs")
local source_name = vim.api.nvim_buf_get_name(source_bufnr)
logs.add({
type = "info",
message = string.format("Removed %d prompt tag(s) from %s",
tags_removed,
vim.fn.fnamemodify(source_name, ":t")),
})
end
end)
end
-- Get filetype for smart injection
local filetype = vim.fn.fnamemodify(patch.target_path or "", ":e")
-- SEARCH/REPLACE MODE: Use fuzzy matching to find and replace text
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
local search_replace = get_search_replace_module()
-- Remove the /@ @/ tags first (they shouldn't be in the file anymore)
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
tags_removed = remove_prompt_tags(source_bufnr)
if tags_removed > 0 then
pcall(function()
local logs = require("codetyper.adapters.nvim.ui.logs")
logs.add({
type = "info",
message = string.format("Removed %d prompt tag(s)", tags_removed),
})
end)
end
end
-- Apply SEARCH/REPLACE blocks
local success, err = search_replace.apply_to_buffer(target_bufnr, patch.search_replace_blocks)
@@ -1104,11 +984,7 @@ function M.apply_with_conflict(patch)
local source_bufnr = patch.source_bufnr
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
-- Remove tags from coder files
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- For SEARCH/REPLACE blocks, convert each block to a conflict
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
local search_replace = get_search_replace_module()
@@ -1147,11 +1023,6 @@ function M.apply_with_conflict(patch)
end
if applied_count > 0 then
-- Remove tags for inline prompts after inserting conflicts
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- Process conflicts (highlight, keymaps) and show menu
conflict.process_and_show_menu(target_bufnr)
@@ -1178,11 +1049,6 @@ function M.apply_with_conflict(patch)
local end_line = patch.injection_range.end_line
local new_lines = vim.split(patch.generated_code, "\n", { plain = true })
-- Remove tags for inline prompts
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- Insert conflict markers
conflict.insert_conflict(target_bufnr, start_line, end_line, new_lines, "AI SUGGESTION")

View File

@@ -25,7 +25,7 @@ local DEBOUNCE_MS = 500
---@return string Brain directory path
function M.get_brain_dir(root)
root = root or utils.get_project_root()
return root .. "/.coder/brain"
return root .. "/.codetyper/brain"
end
--- Ensure brain directory structure exists

View File

@@ -10,7 +10,7 @@ local utils = require("codetyper.support.utils")
---@return string|nil
local function get_resume_dir()
local root = utils.get_project_root() or vim.fn.getcwd()
return root .. "/.coder/tmp"
return root .. "/.codetyper/tmp"
end
--- Get the resume context file path

View File

@@ -468,7 +468,7 @@ local function dispatch_next()
local thinking = require("codetyper.adapters.nvim.ui.thinking")
thinking.ensure_shown()
local is_inline = event.target_path and not event.target_path:match("%.coder%.") and (event.bufnr == vim.fn.bufnr(event.target_path))
local is_inline = event.target_path and not event.target_path:match("%.codetyper%.") and (event.bufnr == vim.fn.bufnr(event.target_path))
local thinking_placeholder = require("codetyper.core.thinking_placeholder")
if is_inline then
-- 99-style: virtual text "⠋ Thinking..." at selection (no buffer change, SEARCH/REPLACE safe)

View File

@@ -258,7 +258,7 @@ local function get_coder_companion_path(target_path)
end
-- Skip if target is already a coder file
if target_path:match("%.coder%.") then
if target_path:match("%.codetyper%.") then
return nil
end
@@ -266,7 +266,7 @@ local function get_coder_companion_path(target_path)
local name = vim.fn.fnamemodify(target_path, ":t:r") -- filename without extension
local ext = vim.fn.fnamemodify(target_path, ":e")
local coder_path = dir .. "/" .. name .. ".coder." .. ext
local coder_path = dir .. "/" .. name .. ".codetyper/" .. ext
if vim.fn.filereadable(coder_path) == 1 then
return coder_path
end
@@ -382,13 +382,13 @@ end
---@return boolean
local function is_inline_prompt(event)
-- Inline prompts have a range with start_line/end_line from tag detection
-- and the source file is the same as target (not a .coder. file)
-- and the source file is the same as target (not a .codetyper/ file)
if not event.range or not event.range.start_line then
return false
end
-- Check if source path (if any) equals target, or if target has no .coder. in it
-- Check if source path (if any) equals target, or if target has no .codetyper/ in it
local target = event.target_path or ""
if target:match("%.coder%.") then
if target:match("%.codetyper%.") then
return false
end
return true

View File

@@ -1,7 +1,5 @@
---@mod codetyper.completion Insert mode completion for file references
---
--- Provides completion for @filename inside /@ @/ tags.
local M = {}
local parser = require("codetyper.parser")
@@ -11,182 +9,183 @@ local utils = require("codetyper.support.utils")
---@param prefix string Prefix to filter files
---@return table[] List of completion items
local function get_file_completions(prefix)
local cwd = vim.fn.getcwd()
local current_file = vim.fn.expand("%:p")
local current_dir = vim.fn.fnamemodify(current_file, ":h")
local files = {}
local cwd = vim.fn.getcwd()
local current_file = vim.fn.expand("%:p")
local current_dir = vim.fn.fnamemodify(current_file, ":h")
local files = {}
-- Use vim.fn.glob to find files matching the prefix
local pattern = prefix .. "*"
-- Use vim.fn.glob to find files matching the prefix
local pattern = prefix .. "*"
-- Determine base directory - use current file's directory if outside cwd
local base_dir = cwd
if current_dir ~= "" and not current_dir:find(cwd, 1, true) then
-- File is outside project, use its directory as base
base_dir = current_dir
end
-- Determine base directory - use current file's directory if outside cwd
local base_dir = cwd
if current_dir ~= "" and not current_dir:find(cwd, 1, true) then
-- File is outside project, use its directory as base
base_dir = current_dir
end
-- Search in base directory
local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true)
-- Search in base directory
local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true)
-- Search with ** for all subdirectories
local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true)
for _, m in ipairs(deep_matches) do
table.insert(matches, m)
end
-- Search with ** for all subdirectories
local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true)
for _, m in ipairs(deep_matches) do
table.insert(matches, m)
end
-- Also search in cwd if different from base_dir
if base_dir ~= cwd then
local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true)
for _, m in ipairs(cwd_matches) do
table.insert(matches, m)
end
local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true)
for _, m in ipairs(cwd_deep) do
table.insert(matches, m)
end
end
-- Also search in cwd if different from base_dir
if base_dir ~= cwd then
local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true)
for _, m in ipairs(cwd_matches) do
table.insert(matches, m)
end
local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true)
for _, m in ipairs(cwd_deep) do
table.insert(matches, m)
end
end
-- Also search specific directories if prefix doesn't have path
if not prefix:find("/") then
local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" }
for _, dir in ipairs(search_dirs) do
local dir_path = base_dir .. "/" .. dir
if vim.fn.isdirectory(dir_path) == 1 then
local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true)
for _, m in ipairs(dir_matches) do
table.insert(matches, m)
end
end
end
end
-- Also search specific directories if prefix doesn't have path
if not prefix:find("/") then
local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" }
for _, dir in ipairs(search_dirs) do
local dir_path = base_dir .. "/" .. dir
if vim.fn.isdirectory(dir_path) == 1 then
local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true)
for _, m in ipairs(dir_matches) do
table.insert(matches, m)
end
end
end
end
-- Convert to relative paths and deduplicate
local seen = {}
for _, match in ipairs(matches) do
-- Convert to relative path based on which base it came from
local rel_path
if match:find(base_dir, 1, true) == 1 then
rel_path = match:sub(#base_dir + 2)
elseif match:find(cwd, 1, true) == 1 then
rel_path = match:sub(#cwd + 2)
else
rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative
end
-- Convert to relative paths and deduplicate
local seen = {}
for _, match in ipairs(matches) do
-- Convert to relative path based on which base it came from
local rel_path
if match:find(base_dir, 1, true) == 1 then
rel_path = match:sub(#base_dir + 2)
elseif match:find(cwd, 1, true) == 1 then
rel_path = match:sub(#cwd + 2)
else
rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative
end
-- Skip directories, coder files, and hidden/generated files
if vim.fn.isdirectory(match) == 0
and not utils.is_coder_file(match)
and not rel_path:match("^%.")
and not rel_path:match("node_modules")
and not rel_path:match("%.git/")
and not rel_path:match("dist/")
and not rel_path:match("build/")
and not seen[rel_path]
then
seen[rel_path] = true
table.insert(files, {
word = rel_path,
abbr = rel_path,
kind = "File",
menu = "[ref]",
})
end
end
-- Skip directories, coder files, and hidden/generated files
if
vim.fn.isdirectory(match) == 0
and not utils.is_coder_file(match)
and not rel_path:match("^%.")
and not rel_path:match("node_modules")
and not rel_path:match("%.git/")
and not rel_path:match("dist/")
and not rel_path:match("build/")
and not seen[rel_path]
then
seen[rel_path] = true
table.insert(files, {
word = rel_path,
abbr = rel_path,
kind = "File",
menu = "[ref]",
})
end
end
-- Sort by length (shorter paths first)
table.sort(files, function(a, b)
return #a.word < #b.word
end)
-- Sort by length (shorter paths first)
table.sort(files, function(a, b)
return #a.word < #b.word
end)
-- Limit results
local result = {}
for i = 1, math.min(#files, 15) do
result[i] = files[i]
end
-- Limit results
local result = {}
for i = 1, math.min(#files, 15) do
result[i] = files[i]
end
return result
return result
end
--- Show file completion popup
function M.show_file_completion()
-- Check if we're in an open prompt tag
local is_inside = parser.is_cursor_in_open_tag()
if not is_inside then
return false
end
-- Check if we're in an open prompt tag
local is_inside = parser.is_cursor_in_open_tag()
if not is_inside then
return false
end
-- Get the prefix being typed
local prefix = parser.get_file_ref_prefix()
if prefix == nil then
return false
end
-- Get the prefix being typed
local prefix = parser.get_file_ref_prefix()
if prefix == nil then
return false
end
-- Get completions
local items = get_file_completions(prefix)
-- Get completions
local items = get_file_completions(prefix)
if #items == 0 then
-- Try with empty prefix to show all files
items = get_file_completions("")
end
if #items == 0 then
-- Try with empty prefix to show all files
items = get_file_completions("")
end
if #items > 0 then
-- Calculate start column (position right after @)
local cursor = vim.api.nvim_win_get_cursor(0)
local col = cursor[2] - #prefix + 1 -- 1-indexed for complete()
if #items > 0 then
-- Calculate start column (position right after @)
local cursor = vim.api.nvim_win_get_cursor(0)
local col = cursor[2] - #prefix + 1 -- 1-indexed for complete()
-- Show completion popup
vim.fn.complete(col, items)
return true
end
-- Show completion popup
vim.fn.complete(col, items)
return true
end
return false
return false
end
--- Setup completion for file references (works on ALL files)
function M.setup()
local group = vim.api.nvim_create_augroup("CoderCompletion", { clear = true })
local group = vim.api.nvim_create_augroup("CoderCompletion", { clear = true })
-- Trigger completion on @ in insert mode (works on ALL files)
vim.api.nvim_create_autocmd("InsertCharPre", {
group = group,
pattern = "*",
callback = function()
-- Skip special buffers
if vim.bo.buftype ~= "" then
return
end
-- Trigger completion on @ in insert mode (works on ALL files)
vim.api.nvim_create_autocmd("InsertCharPre", {
group = group,
pattern = "*",
callback = function()
-- Skip special buffers
if vim.bo.buftype ~= "" then
return
end
if vim.v.char == "@" then
-- Schedule completion popup after the @ is inserted
vim.schedule(function()
-- Check we're in an open tag
local is_inside = parser.is_cursor_in_open_tag()
if not is_inside then
return
end
if vim.v.char == "@" then
-- Schedule completion popup after the @ is inserted
vim.schedule(function()
-- Check we're in an open tag
local is_inside = parser.is_cursor_in_open_tag()
if not is_inside then
return
end
-- Check we're not typing @/ (closing tag)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local next_char = line:sub(cursor[2] + 2, cursor[2] + 2)
-- Check we're not typing @/ (closing tag)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local next_char = line:sub(cursor[2] + 2, cursor[2] + 2)
if next_char == "/" then
return
end
if next_char == "/" then
return
end
-- Show file completion
M.show_file_completion()
end)
end
end,
desc = "Trigger file completion on @ inside prompt tags",
})
-- Show file completion
M.show_file_completion()
end)
end
end,
desc = "Trigger file completion on @ inside prompt tags",
})
-- Also allow manual trigger with <C-x><C-f> style keybinding in insert mode
vim.keymap.set("i", "<C-x>@", function()
M.show_file_completion()
end, { silent = true, desc = "Coder: Complete file reference" })
-- Also allow manual trigger with <C-x><C-f> style keybinding in insert mode
vim.keymap.set("i", "<C-x>@", function()
M.show_file_completion()
end, { silent = true, desc = "Coder: Complete file reference" })
end
return M

View File

@@ -1,7 +1,7 @@
---@mod codetyper.indexer Project indexer for Codetyper.nvim
---@brief [[
--- Indexes project structure, dependencies, and code symbols.
--- Stores knowledge in .coder/ directory for enriching LLM context.
--- Stores knowledge in .codetyper/ directory for enriching LLM context.
---@brief ]]
local M = {}
@@ -24,7 +24,7 @@ local default_config = {
auto_index = true,
index_on_open = false,
max_file_size = 100000,
excluded_dirs = { "node_modules", "dist", "build", ".git", ".coder", "__pycache__", "vendor", "target" },
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
memory = {
enabled = true,
@@ -94,7 +94,7 @@ local function get_index_path()
if not root then
return nil
end
return root .. "/.coder/" .. INDEX_FILE
return root .. "/.codetyper/" .. INDEX_FILE
end
--- Create empty index structure
@@ -168,8 +168,8 @@ function M.save_index(index)
return false
end
-- Ensure .coder directory exists
local coder_dir = root .. "/.coder"
-- Ensure .codetyper directory exists
local coder_dir = root .. "/.codetyper"
utils.ensure_dir(coder_dir)
local path = get_index_path()

View File

@@ -1,6 +1,6 @@
---@mod codetyper.indexer.memory Memory persistence manager
---@brief [[
--- Stores and retrieves learned patterns and memories in .coder/memories/.
--- Stores and retrieves learned patterns and memories in .codetyper/memories/.
--- Supports session history for learning from interactions.
---@brief ]]
@@ -42,7 +42,7 @@ local function get_memories_dir()
if not root then
return nil
end
return root .. "/.coder/" .. MEMORIES_DIR
return root .. "/.codetyper/" .. MEMORIES_DIR
end
--- Get the sessions directory
@@ -52,7 +52,7 @@ local function get_sessions_dir()
if not root then
return nil
end
return root .. "/.coder/" .. SESSIONS_DIR
return root .. "/.codetyper/" .. SESSIONS_DIR
end
--- Ensure memories directory exists

View File

@@ -44,7 +44,7 @@ local DEFAULT_IGNORES = {
"^node_modules$",
"^__pycache__$",
"^%.git$",
"^%.coder$",
"^%.codetyper$",
"^dist$",
"^build$",
"^target$",

View File

@@ -2,7 +2,7 @@
---@brief [[
--- Codetyper.nvim is a Neovim plugin that acts as your coding partner.
--- It uses LLM APIs (OpenAI, Gemini, Copilot, Ollama) to help you
--- write code faster using special `.coder.*` files and inline prompt tags.
--- write code faster using special `.codetyper/*` files and inline prompt tags.
--- Features an event-driven scheduler with confidence scoring and
--- completion-aware injection timing.
---@brief ]]
@@ -44,7 +44,7 @@ function M.setup(opts)
-- Ensure .gitignore has coder files excluded
gitignore.ensure_ignored()
-- Initialize tree logging (creates .coder folder and initial tree.log)
-- Initialize tree logging (creates .codetyper folder and initial tree.log)
tree.setup()
-- Initialize project indexer if enabled

View File

@@ -5,31 +5,15 @@ local M = {}
local utils = require("codetyper.support.utils")
local logger = require("codetyper.support.logger")
--- Get config with safe fallback
---@return table config
local function get_config_safe()
logger.func_entry("parser", "get_config_safe", {})
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.get_config then
local config = codetyper.get_config()
if config and config.patterns then
logger.debug("parser", "get_config_safe: loaded config from codetyper")
logger.func_exit("parser", "get_config_safe", "success")
return config
end
end
logger.debug("parser", "get_config_safe: using fallback defaults")
logger.func_exit("parser", "get_config_safe", "fallback")
-- Fallback defaults
return {
patterns = {
open_tag = "/@",
close_tag = "@/",
},
}
-- Get current codetyper configuration at call time
local function get_config()
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.get_config then
return codetyper.get_config() or {}
end
-- Fall back to defaults if codetyper isn't available
local defaults = require("codetyper.config.defaults")
return defaults.get_defaults()
end
--- Find all prompts in buffer content
@@ -41,9 +25,9 @@ function M.find_prompts(content, open_tag, close_tag)
logger.func_entry("parser", "find_prompts", {
content_length = #content,
open_tag = open_tag,
close_tag = close_tag
close_tag = close_tag,
})
local prompts = {}
local escaped_open = utils.escape_pattern(open_tag)
local escaped_close = utils.escape_pattern(close_tag)
@@ -94,7 +78,13 @@ function M.find_prompts(content, open_tag, close_tag)
current_prompt.end_line = line_num
current_prompt.end_col = end_col + #close_tag - 1
table.insert(prompts, current_prompt)
logger.debug("parser", "find_prompts: multi-line prompt completed at line " .. line_num .. ", total lines: " .. #prompt_content)
logger.debug(
"parser",
"find_prompts: multi-line prompt completed at line "
.. line_num
.. ", total lines: "
.. #prompt_content
)
in_prompt = false
current_prompt = nil
prompt_content = {}
@@ -106,7 +96,7 @@ function M.find_prompts(content, open_tag, close_tag)
logger.debug("parser", "find_prompts: found " .. #prompts .. " prompts total")
logger.func_exit("parser", "find_prompts", "found " .. #prompts .. " prompts")
return prompts
end
@@ -115,16 +105,18 @@ end
---@return CoderPrompt[] List of found prompts
function M.find_prompts_in_buffer(bufnr)
logger.func_entry("parser", "find_prompts_in_buffer", { bufnr = bufnr })
local config = get_config_safe()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local content = table.concat(lines, "\n")
logger.debug("parser", "find_prompts_in_buffer: bufnr=" .. bufnr .. ", lines=" .. #lines .. ", content_length=" .. #content)
logger.debug(
"parser",
"find_prompts_in_buffer: bufnr=" .. bufnr .. ", lines=" .. #lines .. ", content_length=" .. #content
)
local cfg = get_config()
local result = M.find_prompts(content, cfg.patterns.open_tag, cfg.patterns.close_tag)
local result = M.find_prompts(content, config.patterns.open_tag, config.patterns.close_tag)
logger.func_exit("parser", "find_prompts_in_buffer", "found " .. #result .. " prompts")
return result
end
@@ -141,7 +133,7 @@ function M.get_prompt_at_cursor(bufnr)
logger.func_entry("parser", "get_prompt_at_cursor", {
bufnr = bufnr,
line = line,
col = col
col = col,
})
local prompts = M.find_prompts_in_buffer(bufnr)
@@ -149,15 +141,30 @@ function M.get_prompt_at_cursor(bufnr)
logger.debug("parser", "get_prompt_at_cursor: checking " .. #prompts .. " prompts")
for i, prompt in ipairs(prompts) do
logger.debug("parser", "get_prompt_at_cursor: checking prompt " .. i .. " (lines " .. prompt.start_line .. "-" .. prompt.end_line .. ")")
logger.debug(
"parser",
"get_prompt_at_cursor: checking prompt "
.. i
.. " (lines "
.. prompt.start_line
.. "-"
.. prompt.end_line
.. ")"
)
if line >= prompt.start_line and line <= prompt.end_line then
logger.debug("parser", "get_prompt_at_cursor: cursor line " .. line .. " is within prompt line range")
if line == prompt.start_line and col < prompt.start_col then
logger.debug("parser", "get_prompt_at_cursor: cursor col " .. col .. " is before prompt start_col " .. prompt.start_col)
logger.debug(
"parser",
"get_prompt_at_cursor: cursor col " .. col .. " is before prompt start_col " .. prompt.start_col
)
goto continue
end
if line == prompt.end_line and col > prompt.end_col then
logger.debug("parser", "get_prompt_at_cursor: cursor col " .. col .. " is after prompt end_col " .. prompt.end_col)
logger.debug(
"parser",
"get_prompt_at_cursor: cursor col " .. col .. " is after prompt end_col " .. prompt.end_col
)
goto continue
end
logger.debug("parser", "get_prompt_at_cursor: found prompt at cursor")
@@ -177,9 +184,9 @@ end
---@return CoderPrompt|nil Last prompt or nil
function M.get_last_prompt(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "get_last_prompt", { bufnr = bufnr })
local prompts = M.find_prompts_in_buffer(bufnr)
if #prompts > 0 then
@@ -199,7 +206,7 @@ end
---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type
function M.detect_prompt_type(content)
logger.func_entry("parser", "detect_prompt_type", { content_preview = content:sub(1, 50) })
local lower = content:lower()
if lower:match("refactor") then
@@ -230,15 +237,15 @@ end
---@return string Cleaned content
function M.clean_prompt(content)
logger.func_entry("parser", "clean_prompt", { content_length = #content })
-- Trim leading/trailing whitespace
content = content:match("^%s*(.-)%s*$")
-- Normalize multiple newlines
content = content:gsub("\n\n\n+", "\n\n")
logger.debug("parser", "clean_prompt: cleaned from " .. #content .. " chars")
logger.func_exit("parser", "clean_prompt", "length=" .. #content)
return content
end
@@ -248,12 +255,12 @@ end
---@return boolean
function M.has_closing_tag(line, close_tag)
logger.func_entry("parser", "has_closing_tag", { line_preview = line:sub(1, 30), close_tag = close_tag })
local result = line:find(utils.escape_pattern(close_tag)) ~= nil
logger.debug("parser", "has_closing_tag: result=" .. tostring(result))
logger.func_exit("parser", "has_closing_tag", result)
return result
end
@@ -262,25 +269,32 @@ end
---@return boolean
function M.has_unclosed_prompts(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "has_unclosed_prompts", { bufnr = bufnr })
local config = get_config_safe()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local content = table.concat(lines, "\n")
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
local cfg = get_config()
local escaped_open = utils.escape_pattern(cfg.patterns.open_tag)
local escaped_close = utils.escape_pattern(cfg.patterns.close_tag)
local _, open_count = content:gsub(escaped_open, "")
local _, close_count = content:gsub(escaped_close, "")
local has_unclosed = open_count > close_count
logger.debug("parser", "has_unclosed_prompts: open=" .. open_count .. ", close=" .. close_count .. ", unclosed=" .. tostring(has_unclosed))
logger.debug(
"parser",
"has_unclosed_prompts: open="
.. open_count
.. ", close="
.. close_count
.. ", unclosed="
.. tostring(has_unclosed)
)
logger.func_exit("parser", "has_unclosed_prompts", has_unclosed)
return has_unclosed
end
@@ -290,7 +304,7 @@ end
---@return string[] List of file references
function M.extract_file_references(content)
logger.func_entry("parser", "extract_file_references", { content_length = #content })
local files = {}
-- Pattern: @ followed by word char, dot, underscore, or dash as FIRST char
-- Then optionally more path characters including /
@@ -301,10 +315,10 @@ function M.extract_file_references(content)
logger.debug("parser", "extract_file_references: found file reference: " .. file)
end
end
logger.debug("parser", "extract_file_references: found " .. #files .. " file references")
logger.func_exit("parser", "extract_file_references", files)
return files
end
@@ -313,14 +327,14 @@ end
---@return string Cleaned content without file references
function M.strip_file_references(content)
logger.func_entry("parser", "strip_file_references", { content_length = #content })
-- Remove @filename patterns but preserve @/ closing tag
-- Pattern requires first char after @ to be word char, dot, underscore, or dash (NOT /)
local result = content:gsub("@([%w%._%-][%w%._%-/]*)", "")
logger.debug("parser", "strip_file_references: stripped " .. (#content - #result) .. " chars")
logger.func_exit("parser", "strip_file_references", "length=" .. #result)
return result
end
@@ -330,17 +344,16 @@ end
---@return number|nil start_line Line where the open tag starts
function M.is_cursor_in_open_tag(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "is_cursor_in_open_tag", { bufnr = bufnr })
local config = get_config_safe()
local cursor = vim.api.nvim_win_get_cursor(0)
local cursor_line = cursor[1]
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, cursor_line, false)
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
local cfg = get_config()
local escaped_open = utils.escape_pattern(cfg.patterns.open_tag)
local escaped_close = utils.escape_pattern(cfg.patterns.close_tag)
local open_count = 0
local close_count = 0
@@ -361,10 +374,20 @@ function M.is_cursor_in_open_tag(bufnr)
end
local is_inside = open_count > close_count
logger.debug("parser", "is_cursor_in_open_tag: open=" .. open_count .. ", close=" .. close_count .. ", is_inside=" .. tostring(is_inside) .. ", last_open_line=" .. tostring(last_open_line))
logger.debug(
"parser",
"is_cursor_in_open_tag: open="
.. open_count
.. ", close="
.. close_count
.. ", is_inside="
.. tostring(is_inside)
.. ", last_open_line="
.. tostring(last_open_line)
)
logger.func_exit("parser", "is_cursor_in_open_tag", { is_inside = is_inside, last_open_line = last_open_line })
return is_inside, is_inside and last_open_line or nil
end
@@ -373,7 +396,7 @@ end
---@return string|nil prefix The text after @ being typed, or nil if not typing a file ref
function M.get_file_ref_prefix(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "get_file_ref_prefix", { bufnr = bufnr })
local cursor = vim.api.nvim_win_get_cursor(0)
@@ -400,7 +423,7 @@ function M.get_file_ref_prefix(bufnr)
logger.debug("parser", "get_file_ref_prefix: prefix=" .. tostring(prefix))
logger.func_exit("parser", "get_file_ref_prefix", prefix)
return prefix
end

View File

@@ -6,8 +6,8 @@ local utils = require("codetyper.support.utils")
--- Patterns to add to .gitignore
local IGNORE_PATTERNS = {
"*.coder.*",
".coder/",
"*.codetyper/*",
".codetyper/",
}
--- Comment to identify codetyper entries
@@ -18,102 +18,102 @@ local CODER_COMMENT = "# Codetyper.nvim - AI coding partner files"
---@param pattern string Pattern to check
---@return boolean
local function pattern_exists(content, pattern)
local escaped = utils.escape_pattern(pattern)
return content:match("\n" .. escaped .. "\n") ~= nil
or content:match("^" .. escaped .. "\n") ~= nil
or content:match("\n" .. escaped .. "$") ~= nil
or content == pattern
local escaped = utils.escape_pattern(pattern)
return content:match("\n" .. escaped .. "\n") ~= nil
or content:match("^" .. escaped .. "\n") ~= nil
or content:match("\n" .. escaped .. "$") ~= nil
or content == pattern
end
--- Check if all patterns exist in gitignore content
---@param content string Gitignore content
---@return boolean, string[] All exist status and list of missing patterns
local function all_patterns_exist(content)
local missing = {}
for _, pattern in ipairs(IGNORE_PATTERNS) do
if not pattern_exists(content, pattern) then
table.insert(missing, pattern)
end
end
return #missing == 0, missing
local missing = {}
for _, pattern in ipairs(IGNORE_PATTERNS) do
if not pattern_exists(content, pattern) then
table.insert(missing, pattern)
end
end
return #missing == 0, missing
end
--- Get the path to .gitignore in project root
---@return string|nil Path to .gitignore or nil
function M.get_gitignore_path()
local root = utils.get_project_root()
if not root then
return nil
end
return root .. "/.gitignore"
local root = utils.get_project_root()
if not root then
return nil
end
return root .. "/.gitignore"
end
--- Check if coder files are already ignored
---@return boolean
function M.is_ignored()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local all_exist, _ = all_patterns_exist(content)
return all_exist
local all_exist, _ = all_patterns_exist(content)
return all_exist
end
--- Add coder patterns to .gitignore
---@return boolean Success status
function M.add_to_gitignore()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root", vim.log.levels.WARN)
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root", vim.log.levels.WARN)
return false
end
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
if content then
-- File exists, check which patterns are missing
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true -- All already ignored
end
patterns_to_add = missing
else
-- Create new .gitignore with all patterns
content = ""
patterns_to_add = IGNORE_PATTERNS
end
if content then
-- File exists, check which patterns are missing
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true -- All already ignored
end
patterns_to_add = missing
else
-- Create new .gitignore with all patterns
content = ""
patterns_to_add = IGNORE_PATTERNS
end
-- Build the patterns string
local patterns_str = table.concat(patterns_to_add, "\n")
-- Build the patterns string
local patterns_str = table.concat(patterns_to_add, "\n")
if content == "" then
-- New file
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
-- Append to existing
local newline = content:sub(-1) == "\n" and "" or "\n"
-- Check if comment already exists
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if content == "" then
-- New file
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
-- Append to existing
local newline = content:sub(-1) == "\n" and "" or "\n"
-- Check if comment already exists
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if utils.write_file(gitignore_path, content) then
utils.notify("Added coder patterns to .gitignore")
return true
else
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
return false
end
if utils.write_file(gitignore_path, content) then
utils.notify("Added coder patterns to .gitignore")
return true
else
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
return false
end
end
--- Ensure coder files are in .gitignore (called on setup)
@@ -122,115 +122,116 @@ end
---@param auto_gitignore? boolean Override auto_gitignore setting (default: true)
---@return boolean Success status
function M.ensure_ignored(auto_gitignore)
-- Only add to gitignore if this is a git project
if not utils.is_git_project() then
return false -- Not a git project, skip
end
-- Only add to gitignore if this is a git project
if not utils.is_git_project() then
return false -- Not a git project, skip
end
-- Default to true if not specified
if auto_gitignore == nil then
-- Try to get from config if available
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.is_initialized and codetyper.is_initialized() then
local config = codetyper.get_config()
auto_gitignore = config and config.auto_gitignore
else
auto_gitignore = true -- Default to true
end
end
if not auto_gitignore then
return true
end
if not auto_gitignore then
return true
end
if M.is_ignored() then
return true
end
if M.is_ignored() then
return true
end
-- Default to true if not specified
if auto_gitignore == nil then
-- Try to get from config if available
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.is_initialized and codetyper.is_initialized() then
local config = codetyper.get_config()
auto_gitignore = config and config.auto_gitignore
else
auto_gitignore = true -- Default to true
end
end
-- Silently add to gitignore (no notifications unless there's an error)
return M.add_to_gitignore_silent()
-- Silently add to gitignore (no notifications unless there's an error)
return M.add_to_gitignore_silent()
end
-- /@ @/
--- Add coder patterns to .gitignore silently (no notifications)
---@return boolean Success status
function M.add_to_gitignore_silent()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
if content then
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true
end
patterns_to_add = missing
else
content = ""
patterns_to_add = IGNORE_PATTERNS
end
if content then
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true
end
patterns_to_add = missing
else
content = ""
patterns_to_add = IGNORE_PATTERNS
end
local patterns_str = table.concat(patterns_to_add, "\n")
local patterns_str = table.concat(patterns_to_add, "\n")
if content == "" then
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
local newline = content:sub(-1) == "\n" and "" or "\n"
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if content == "" then
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
local newline = content:sub(-1) == "\n" and "" or "\n"
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
return utils.write_file(gitignore_path, content)
return utils.write_file(gitignore_path, content)
end
--- Remove coder patterns from .gitignore
---@return boolean Success status
function M.remove_from_gitignore()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
-- Remove the comment and all patterns
content = content:gsub(CODER_COMMENT .. "\n", "")
for _, pattern in ipairs(IGNORE_PATTERNS) do
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
end
-- Remove the comment and all patterns
content = content:gsub(CODER_COMMENT .. "\n", "")
for _, pattern in ipairs(IGNORE_PATTERNS) do
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
end
-- Clean up extra newlines
content = content:gsub("\n\n\n+", "\n\n")
-- Clean up extra newlines
content = content:gsub("\n\n\n+", "\n\n")
return utils.write_file(gitignore_path, content)
return utils.write_file(gitignore_path, content)
end
--- Get list of patterns being ignored
---@return string[] List of patterns
function M.get_ignore_patterns()
return vim.deepcopy(IGNORE_PATTERNS)
return vim.deepcopy(IGNORE_PATTERNS)
end
--- Force update gitignore (manual trigger)
---@return boolean Success status
function M.force_update()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
return false
end
utils.notify("Updating .gitignore at: " .. gitignore_path)
return M.add_to_gitignore()
utils.notify("Updating .gitignore at: " .. gitignore_path)
return M.add_to_gitignore()
end
return M

View File

@@ -5,7 +5,7 @@ local M = {}
local utils = require("codetyper.support.utils")
--- Name of the coder folder
local CODER_FOLDER = ".coder"
local CODER_FOLDER = ".codetyper"
--- Name of the tree log file
local TREE_LOG_FILE = "tree.log"
@@ -23,8 +23,8 @@ local DEFAULT_SETTINGS = {
["workbench.colorTheme"] = "Default Dark+",
}
--- Get the path to the .coder folder
---@return string|nil Path to .coder folder or nil
--- Get the path to the .codetyper folder
---@return string|nil Path to .codetyper folder or nil
function M.get_coder_folder()
local root = utils.get_project_root()
if not root then
@@ -94,7 +94,7 @@ function M.ensure_settings()
return utils.write_file(settings_path, pretty_json)
end
--- Ensure .coder folder exists
--- Ensure .codetyper folder exists
---@return boolean Success status
function M.ensure_coder_folder()
local coder_folder = M.get_coder_folder()
@@ -212,12 +212,12 @@ function M.generate_tree()
"^node_modules$",
"^__pycache__$",
"^%.git$",
"^%.coder$",
"^%.codetyper$",
"^dist$",
"^build$",
"^target$",
"^vendor$",
"%.coder%.", -- Coder files
"%.codetyper%.", -- Coder files
}
local lines = {
@@ -242,7 +242,7 @@ end
--- Update the tree.log file
---@return boolean Success status
function M.update_tree_log()
-- Ensure .coder folder exists
-- Ensure .codetyper folder exists
if not M.ensure_coder_folder() then
return false
end
@@ -273,13 +273,13 @@ local function is_project_initialized(root)
end
--- Initialize tree logging (called on setup)
--- Only creates .coder/ folder for git projects (has .git/ folder)
--- Only creates .codetyper/ folder for git projects (has .git/ folder)
---@param force? boolean Force re-initialization even if cached
---@return boolean success
function M.setup(force)
-- Only initialize for git projects
if not utils.is_git_project() then
return false -- Not a git project, don't create .coder/
return false -- Not a git project, don't create .codetyper/
end
local coder_folder = M.get_coder_folder()
@@ -297,7 +297,7 @@ function M.setup(force)
return true
end
-- Ensure .coder folder exists (silent, no asking)
-- Ensure .codetyper folder exists (silent, no asking)
if not M.ensure_coder_folder() then
-- Silent failure - don't bother user
return false
@@ -338,7 +338,7 @@ function M.get_stats()
end
-- Skip hidden and special folders
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.coder%.") then
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.codetyper%.") then
if type == "directory" then
stats.directories = stats.directories + 1
count_recursive(path .. "/" .. name)

View File

@@ -56,30 +56,30 @@ end
---@param filepath string File path to check
---@return boolean
function M.is_coder_file(filepath)
return filepath:match("%.coder%.") ~= nil
return filepath:match("%.codetyper%.") ~= nil
end
--- Get the target file path from a coder file path
---@param coder_path string Path to the coder file
---@return string Target file path
function M.get_target_path(coder_path)
-- Convert index.coder.ts -> index.ts
return coder_path:gsub("%.coder%.", ".")
-- Convert index.codetyper/ts -> index.ts
return coder_path:gsub("%.codetyper%.", ".")
end
--- Get the coder file path from a target file path
---@param target_path string Path to the target file
---@return string Coder file path
function M.get_coder_path(target_path)
-- Convert index.ts -> index.coder.ts
-- Convert index.ts -> index.codetyper/ts
local dir = vim.fn.fnamemodify(target_path, ":h")
local name = vim.fn.fnamemodify(target_path, ":t:r")
local ext = vim.fn.fnamemodify(target_path, ":e")
if dir == "." then
return name .. ".coder." .. ext
return name .. ".codetyper/" .. ext
end
return dir .. "/" .. name .. ".coder." .. ext
return dir .. "/" .. name .. ".codetyper/" .. ext
end
--- Check if a file exists

View File

@@ -47,7 +47,7 @@
---@field end_col number Ending column
---@class CoderFile
---@field coder_path string Path to the .coder.* file
---@field coder_path string Path to the .codetyper/* file
---@field target_path string Path to the target file
---@field filetype string The filetype/extension

View File

@@ -19,7 +19,7 @@ if fn.has("nvim-0.8.0") == 0 then
end
--- Initialize codetyper plugin fully
--- Creates .coder folder, settings.json, tree.log, .gitignore
--- Creates .codetyper folder, settings.json, tree.log, .gitignore
--- Also registers autocmds for /@ @/ prompt detection
---@return boolean success
local function init_coder_files()
@@ -38,7 +38,7 @@ local function init_coder_files()
return true
end
-- Initialize .coder folder and tree.log on project open
-- Initialize .codetyper folder and tree.log on project open
api.nvim_create_autocmd("VimEnter", {
callback = function()
-- Delay slightly to ensure cwd is set
@@ -46,7 +46,7 @@ api.nvim_create_autocmd("VimEnter", {
init_coder_files()
end, 100)
end,
desc = "Initialize Codetyper .coder folder on startup",
desc = "Initialize Codetyper .codetyper folder on startup",
})
-- Also initialize on directory change
@@ -56,12 +56,12 @@ api.nvim_create_autocmd("DirChanged", {
init_coder_files()
end, 100)
end,
desc = "Initialize Codetyper .coder folder on directory change",
desc = "Initialize Codetyper .codetyper folder on directory change",
})
-- Auto-initialize when opening a coder file (for nvim-tree, telescope, etc.)
api.nvim_create_autocmd({ "BufRead", "BufNewFile", "BufEnter" }, {
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function()
-- Initialize plugin if not already done
local codetyper = require("codetyper")

View File

@@ -1,47 +0,0 @@
-- Minimal init.lua for running tests
-- This sets up the minimum Neovim environment needed for testing
-- Add the plugin to the runtimepath
local plugin_root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h")
vim.opt.rtp:prepend(plugin_root)
-- Add plenary for testing (if available)
local plenary_path = vim.fn.expand("~/.local/share/nvim/lazy/plenary.nvim")
if vim.fn.isdirectory(plenary_path) == 1 then
vim.opt.rtp:prepend(plenary_path)
end
-- Alternative plenary paths
local alt_plenary_paths = {
vim.fn.expand("~/.local/share/nvim/site/pack/*/start/plenary.nvim"),
vim.fn.expand("~/.config/nvim/plugged/plenary.nvim"),
"/opt/homebrew/share/nvim/site/pack/packer/start/plenary.nvim",
}
for _, path in ipairs(alt_plenary_paths) do
local expanded = vim.fn.glob(path)
if expanded ~= "" and vim.fn.isdirectory(expanded) == 1 then
vim.opt.rtp:prepend(expanded)
break
end
end
-- Set up test environment
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.writebackup = false
-- Initialize codetyper with test defaults
require("codetyper").setup({
llm = {
provider = "ollama",
ollama = {
host = "http://localhost:11434",
model = "test-model",
},
},
scheduler = {
enabled = false, -- Disable scheduler during tests
},
auto_gitignore = false,
})

View File

@@ -1,62 +0,0 @@
#!/bin/bash
# Run codetyper.nvim tests using plenary.nvim
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Running codetyper.nvim tests...${NC}"
echo "Project root: $PROJECT_ROOT"
echo ""
# Check if plenary is installed
PLENARY_PATH=""
POSSIBLE_PATHS=(
"$HOME/.local/share/nvim/lazy/plenary.nvim"
"$HOME/.local/share/nvim/site/pack/packer/start/plenary.nvim"
"$HOME/.config/nvim/plugged/plenary.nvim"
"/opt/homebrew/share/nvim/site/pack/packer/start/plenary.nvim"
)
for path in "${POSSIBLE_PATHS[@]}"; do
if [ -d "$path" ]; then
PLENARY_PATH="$path"
break
fi
done
if [ -z "$PLENARY_PATH" ]; then
echo -e "${RED}Error: plenary.nvim not found!${NC}"
echo "Please install plenary.nvim first:"
echo " - With lazy.nvim: { 'nvim-lua/plenary.nvim' }"
echo " - With packer: use 'nvim-lua/plenary.nvim'"
exit 1
fi
echo "Found plenary at: $PLENARY_PATH"
echo ""
# Run tests
if [ "$1" == "--file" ] && [ -n "$2" ]; then
# Run specific test file
echo -e "${YELLOW}Running: $2${NC}"
nvim --headless \
-u "$SCRIPT_DIR/minimal_init.lua" \
-c "PlenaryBustedFile $SCRIPT_DIR/spec/$2"
else
# Run all tests
echo -e "${YELLOW}Running all tests in spec/ directory${NC}"
nvim --headless \
-u "$SCRIPT_DIR/minimal_init.lua" \
-c "PlenaryBustedDirectory $SCRIPT_DIR/spec/ {minimal_init = '$SCRIPT_DIR/minimal_init.lua'}"
fi
echo ""
echo -e "${GREEN}Tests completed!${NC}"

View File

@@ -1,252 +0,0 @@
--- Tests for brain/delta modules
describe("brain.delta", function()
local diff
local commit
local storage
local types
local test_root = "/tmp/codetyper_test_" .. os.time()
before_each(function()
-- Clear module cache
package.loaded["codetyper.brain.delta.diff"] = nil
package.loaded["codetyper.brain.delta.commit"] = nil
package.loaded["codetyper.brain.storage"] = nil
package.loaded["codetyper.brain.types"] = nil
diff = require("codetyper.brain.delta.diff")
commit = require("codetyper.brain.delta.commit")
storage = require("codetyper.brain.storage")
types = require("codetyper.brain.types")
storage.clear_cache()
vim.fn.mkdir(test_root, "p")
storage.ensure_dirs(test_root)
-- Mock get_project_root
local utils = require("codetyper.utils")
utils.get_project_root = function()
return test_root
end
end)
after_each(function()
vim.fn.delete(test_root, "rf")
storage.clear_cache()
end)
describe("diff.compute", function()
it("detects added values", function()
local diffs = diff.compute(nil, { a = 1 })
assert.equals(1, #diffs)
assert.equals("add", diffs[1].op)
end)
it("detects deleted values", function()
local diffs = diff.compute({ a = 1 }, nil)
assert.equals(1, #diffs)
assert.equals("delete", diffs[1].op)
end)
it("detects replaced values", function()
local diffs = diff.compute({ a = 1 }, { a = 2 })
assert.equals(1, #diffs)
assert.equals("replace", diffs[1].op)
assert.equals(1, diffs[1].from)
assert.equals(2, diffs[1].to)
end)
it("detects nested changes", function()
local before = { a = { b = 1 } }
local after = { a = { b = 2 } }
local diffs = diff.compute(before, after)
assert.equals(1, #diffs)
assert.equals("a.b", diffs[1].path)
end)
it("returns empty for identical values", function()
local diffs = diff.compute({ a = 1 }, { a = 1 })
assert.equals(0, #diffs)
end)
end)
describe("diff.apply", function()
it("applies add operation", function()
local base = { a = 1 }
local diffs = { { op = "add", path = "b", value = 2 } }
local result = diff.apply(base, diffs)
assert.equals(2, result.b)
end)
it("applies replace operation", function()
local base = { a = 1 }
local diffs = { { op = "replace", path = "a", to = 2 } }
local result = diff.apply(base, diffs)
assert.equals(2, result.a)
end)
it("applies delete operation", function()
local base = { a = 1, b = 2 }
local diffs = { { op = "delete", path = "a" } }
local result = diff.apply(base, diffs)
assert.is_nil(result.a)
assert.equals(2, result.b)
end)
it("applies nested changes", function()
local base = { a = { b = 1 } }
local diffs = { { op = "replace", path = "a.b", to = 2 } }
local result = diff.apply(base, diffs)
assert.equals(2, result.a.b)
end)
end)
describe("diff.reverse", function()
it("reverses add to delete", function()
local diffs = { { op = "add", path = "a", value = 1 } }
local reversed = diff.reverse(diffs)
assert.equals("delete", reversed[1].op)
end)
it("reverses delete to add", function()
local diffs = { { op = "delete", path = "a", value = 1 } }
local reversed = diff.reverse(diffs)
assert.equals("add", reversed[1].op)
end)
it("reverses replace", function()
local diffs = { { op = "replace", path = "a", from = 1, to = 2 } }
local reversed = diff.reverse(diffs)
assert.equals("replace", reversed[1].op)
assert.equals(2, reversed[1].from)
assert.equals(1, reversed[1].to)
end)
end)
describe("diff.equals", function()
it("returns true for identical states", function()
assert.is_true(diff.equals({ a = 1 }, { a = 1 }))
end)
it("returns false for different states", function()
assert.is_false(diff.equals({ a = 1 }, { a = 2 }))
end)
end)
describe("commit.create", function()
it("creates a delta commit", function()
local changes = {
{ op = "add", path = "test.node1", ah = "abc123" },
}
local delta = commit.create(changes, "Test commit", "test")
assert.is_not_nil(delta)
assert.is_not_nil(delta.h)
assert.equals("Test commit", delta.m.msg)
assert.equals(1, #delta.ch)
end)
it("updates HEAD", function()
local changes = { { op = "add", path = "test.node1", ah = "abc123" } }
local delta = commit.create(changes, "Test", "test")
local head = storage.get_head(test_root)
assert.equals(delta.h, head)
end)
it("links to parent", function()
local changes1 = { { op = "add", path = "test.node1", ah = "abc123" } }
local delta1 = commit.create(changes1, "First", "test")
local changes2 = { { op = "add", path = "test.node2", ah = "def456" } }
local delta2 = commit.create(changes2, "Second", "test")
assert.equals(delta1.h, delta2.p)
end)
it("returns nil for empty changes", function()
local delta = commit.create({}, "Empty")
assert.is_nil(delta)
end)
end)
describe("commit.get", function()
it("retrieves created delta", function()
local changes = { { op = "add", path = "test.node1", ah = "abc123" } }
local created = commit.create(changes, "Test", "test")
local retrieved = commit.get(created.h)
assert.is_not_nil(retrieved)
assert.equals(created.h, retrieved.h)
end)
it("returns nil for non-existent delta", function()
local retrieved = commit.get("nonexistent")
assert.is_nil(retrieved)
end)
end)
describe("commit.get_history", function()
it("returns delta chain", function()
commit.create({ { op = "add", path = "node1", ah = "1" } }, "First", "test")
commit.create({ { op = "add", path = "node2", ah = "2" } }, "Second", "test")
commit.create({ { op = "add", path = "node3", ah = "3" } }, "Third", "test")
local history = commit.get_history(10)
assert.equals(3, #history)
assert.equals("Third", history[1].m.msg)
assert.equals("Second", history[2].m.msg)
assert.equals("First", history[3].m.msg)
end)
it("respects limit", function()
for i = 1, 5 do
commit.create({ { op = "add", path = "node" .. i, ah = tostring(i) } }, "Commit " .. i, "test")
end
local history = commit.get_history(3)
assert.equals(3, #history)
end)
end)
describe("commit.summarize", function()
it("summarizes delta statistics", function()
local changes = {
{ op = "add", path = "nodes.a" },
{ op = "add", path = "nodes.b" },
{ op = "mod", path = "nodes.c" },
{ op = "del", path = "nodes.d" },
}
local delta = commit.create(changes, "Test", "test")
local summary = commit.summarize(delta)
assert.equals(2, summary.stats.adds)
assert.equals(4, summary.stats.total)
assert.is_true(vim.tbl_contains(summary.categories, "nodes"))
end)
end)
end)

View File

@@ -1,128 +0,0 @@
--- Tests for brain/hash.lua
describe("brain.hash", function()
local hash
before_each(function()
package.loaded["codetyper.brain.hash"] = nil
hash = require("codetyper.brain.hash")
end)
describe("compute", function()
it("returns 8-character hash", function()
local result = hash.compute("test string")
assert.equals(8, #result)
end)
it("returns consistent hash for same input", function()
local result1 = hash.compute("test")
local result2 = hash.compute("test")
assert.equals(result1, result2)
end)
it("returns different hash for different input", function()
local result1 = hash.compute("test1")
local result2 = hash.compute("test2")
assert.not_equals(result1, result2)
end)
it("handles empty string", function()
local result = hash.compute("")
assert.equals("00000000", result)
end)
it("handles nil", function()
local result = hash.compute(nil)
assert.equals("00000000", result)
end)
end)
describe("compute_table", function()
it("hashes table as JSON", function()
local result = hash.compute_table({ a = 1, b = 2 })
assert.equals(8, #result)
end)
it("returns consistent hash for same table", function()
local result1 = hash.compute_table({ x = "y" })
local result2 = hash.compute_table({ x = "y" })
assert.equals(result1, result2)
end)
end)
describe("node_id", function()
it("generates ID with correct format", function()
local id = hash.node_id("pat", "test content")
assert.truthy(id:match("^n_pat_%d+_%w+$"))
end)
it("generates unique IDs", function()
local id1 = hash.node_id("pat", "test1")
local id2 = hash.node_id("pat", "test2")
assert.not_equals(id1, id2)
end)
end)
describe("edge_id", function()
it("generates ID with correct format", function()
local id = hash.edge_id("source_node", "target_node")
assert.truthy(id:match("^e_%w+_%w+$"))
end)
it("returns same ID for same source/target", function()
local id1 = hash.edge_id("s1", "t1")
local id2 = hash.edge_id("s1", "t1")
assert.equals(id1, id2)
end)
end)
describe("delta_hash", function()
it("generates 8-character hash", function()
local changes = { { op = "add", path = "test" } }
local result = hash.delta_hash(changes, "parent", 12345)
assert.equals(8, #result)
end)
it("includes parent in hash", function()
local changes = { { op = "add", path = "test" } }
local result1 = hash.delta_hash(changes, "parent1", 12345)
local result2 = hash.delta_hash(changes, "parent2", 12345)
assert.not_equals(result1, result2)
end)
end)
describe("path_hash", function()
it("returns 8-character hash", function()
local result = hash.path_hash("/path/to/file.lua")
assert.equals(8, #result)
end)
end)
describe("matches", function()
it("returns true for matching hashes", function()
assert.is_true(hash.matches("abc12345", "abc12345"))
end)
it("returns false for different hashes", function()
assert.is_false(hash.matches("abc12345", "def67890"))
end)
end)
describe("random", function()
it("returns 8-character string", function()
local result = hash.random()
assert.equals(8, #result)
end)
it("generates different values", function()
local result1 = hash.random()
local result2 = hash.random()
-- Note: There's a tiny chance these could match, but very unlikely
assert.not_equals(result1, result2)
end)
it("contains only hex characters", function()
local result = hash.random()
assert.truthy(result:match("^[0-9a-f]+$"))
end)
end)
end)

View File

@@ -1,153 +0,0 @@
--- Tests for brain/learners pattern detection and extraction
describe("brain.learners", function()
local pattern_learner
before_each(function()
-- Clear module cache
package.loaded["codetyper.brain.learners.pattern"] = nil
package.loaded["codetyper.brain.types"] = nil
pattern_learner = require("codetyper.brain.learners.pattern")
end)
describe("pattern learner detection", function()
it("should detect code_completion events", function()
local event = { type = "code_completion", data = {} }
assert.is_true(pattern_learner.detect(event))
end)
it("should detect file_indexed events", function()
local event = { type = "file_indexed", data = {} }
assert.is_true(pattern_learner.detect(event))
end)
it("should detect code_analyzed events", function()
local event = { type = "code_analyzed", data = {} }
assert.is_true(pattern_learner.detect(event))
end)
it("should detect pattern_detected events", function()
local event = { type = "pattern_detected", data = {} }
assert.is_true(pattern_learner.detect(event))
end)
it("should NOT detect plain 'pattern' type events", function()
-- This was the bug - 'pattern' type was not in the valid_types list
local event = { type = "pattern", data = {} }
assert.is_false(pattern_learner.detect(event))
end)
it("should NOT detect unknown event types", function()
local event = { type = "unknown_type", data = {} }
assert.is_false(pattern_learner.detect(event))
end)
it("should NOT detect nil events", function()
assert.is_false(pattern_learner.detect(nil))
end)
it("should NOT detect events without type", function()
local event = { data = {} }
assert.is_false(pattern_learner.detect(event))
end)
end)
describe("pattern learner extraction", function()
it("should extract from pattern_detected events", function()
local event = {
type = "pattern_detected",
file = "/path/to/file.lua",
data = {
name = "Test pattern",
description = "Pattern description",
language = "lua",
symbols = { "func1", "func2" },
},
}
local extracted = pattern_learner.extract(event)
assert.is_not_nil(extracted)
assert.equals("Test pattern", extracted.summary)
assert.equals("Pattern description", extracted.detail)
assert.equals("lua", extracted.lang)
assert.equals("/path/to/file.lua", extracted.file)
end)
it("should handle pattern_detected with minimal data", function()
local event = {
type = "pattern_detected",
file = "/path/to/file.lua",
data = {
name = "Minimal pattern",
},
}
local extracted = pattern_learner.extract(event)
assert.is_not_nil(extracted)
assert.equals("Minimal pattern", extracted.summary)
assert.equals("Minimal pattern", extracted.detail)
end)
it("should extract from code_completion events", function()
local event = {
type = "code_completion",
file = "/path/to/file.lua",
data = {
intent = "add function",
code = "function test() end",
language = "lua",
},
}
local extracted = pattern_learner.extract(event)
assert.is_not_nil(extracted)
assert.is_true(extracted.summary:find("Code pattern") ~= nil)
assert.equals("function test() end", extracted.detail)
end)
end)
describe("should_learn validation", function()
it("should accept valid patterns", function()
local data = {
summary = "Valid pattern summary",
detail = "This is a detailed description of the pattern",
}
assert.is_true(pattern_learner.should_learn(data))
end)
it("should reject patterns without summary", function()
local data = {
summary = "",
detail = "Some detail",
}
assert.is_false(pattern_learner.should_learn(data))
end)
it("should reject patterns with nil summary", function()
local data = {
summary = nil,
detail = "Some detail",
}
assert.is_false(pattern_learner.should_learn(data))
end)
it("should reject patterns with very short detail", function()
local data = {
summary = "Valid summary",
detail = "short", -- Less than 10 chars
}
assert.is_false(pattern_learner.should_learn(data))
end)
it("should reject whitespace-only summaries", function()
local data = {
summary = " ",
detail = "Some valid detail here",
}
assert.is_false(pattern_learner.should_learn(data))
end)
end)
end)

View File

@@ -1,234 +0,0 @@
--- Tests for brain/graph/node.lua
describe("brain.graph.node", function()
local node
local storage
local types
local test_root = "/tmp/codetyper_test_" .. os.time()
before_each(function()
-- Clear module cache
package.loaded["codetyper.brain.graph.node"] = nil
package.loaded["codetyper.brain.storage"] = nil
package.loaded["codetyper.brain.types"] = nil
package.loaded["codetyper.brain.hash"] = nil
storage = require("codetyper.brain.storage")
types = require("codetyper.brain.types")
node = require("codetyper.brain.graph.node")
storage.clear_cache()
vim.fn.mkdir(test_root, "p")
storage.ensure_dirs(test_root)
-- Mock get_project_root
local utils = require("codetyper.utils")
utils.get_project_root = function()
return test_root
end
end)
after_each(function()
vim.fn.delete(test_root, "rf")
storage.clear_cache()
node.pending = {}
end)
describe("create", function()
it("creates a new node with correct structure", function()
local created = node.create(types.NODE_TYPES.PATTERN, {
s = "Test pattern summary",
d = "Test pattern detail",
}, {
f = "test.lua",
})
assert.is_not_nil(created.id)
assert.equals(types.NODE_TYPES.PATTERN, created.t)
assert.equals("Test pattern summary", created.c.s)
assert.equals("test.lua", created.ctx.f)
assert.equals(0.5, created.sc.w)
assert.equals(0, created.sc.u)
end)
it("generates unique IDs", function()
local node1 = node.create(types.NODE_TYPES.PATTERN, { s = "First" }, {})
local node2 = node.create(types.NODE_TYPES.PATTERN, { s = "Second" }, {})
assert.is_not_nil(node1.id)
assert.is_not_nil(node2.id)
assert.not_equals(node1.id, node2.id)
end)
it("updates meta node count", function()
local meta_before = storage.get_meta(test_root)
local count_before = meta_before.nc
node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local meta_after = storage.get_meta(test_root)
assert.equals(count_before + 1, meta_after.nc)
end)
it("tracks pending change", function()
node.pending = {}
node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
assert.equals(1, #node.pending)
assert.equals("add", node.pending[1].op)
end)
end)
describe("get", function()
it("retrieves created node", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local retrieved = node.get(created.id)
assert.is_not_nil(retrieved)
assert.equals(created.id, retrieved.id)
assert.equals("Test", retrieved.c.s)
end)
it("returns nil for non-existent node", function()
local retrieved = node.get("n_pat_0_nonexistent")
assert.is_nil(retrieved)
end)
end)
describe("update", function()
it("updates node content", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Original" }, {})
node.update(created.id, { c = { s = "Updated" } })
local updated = node.get(created.id)
assert.equals("Updated", updated.c.s)
end)
it("updates node scores", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
node.update(created.id, { sc = { w = 0.9 } })
local updated = node.get(created.id)
assert.equals(0.9, updated.sc.w)
end)
it("increments version", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local original_version = created.m.v
node.update(created.id, { c = { s = "Updated" } })
local updated = node.get(created.id)
assert.equals(original_version + 1, updated.m.v)
end)
it("returns nil for non-existent node", function()
local result = node.update("n_pat_0_nonexistent", { c = { s = "Test" } })
assert.is_nil(result)
end)
end)
describe("delete", function()
it("removes node", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local result = node.delete(created.id)
assert.is_true(result)
assert.is_nil(node.get(created.id))
end)
it("decrements meta node count", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local meta_before = storage.get_meta(test_root)
local count_before = meta_before.nc
node.delete(created.id)
local meta_after = storage.get_meta(test_root)
assert.equals(count_before - 1, meta_after.nc)
end)
it("returns false for non-existent node", function()
local result = node.delete("n_pat_0_nonexistent")
assert.is_false(result)
end)
end)
describe("find", function()
it("finds nodes by type", function()
node.create(types.NODE_TYPES.PATTERN, { s = "Pattern 1" }, {})
node.create(types.NODE_TYPES.PATTERN, { s = "Pattern 2" }, {})
node.create(types.NODE_TYPES.CORRECTION, { s = "Correction 1" }, {})
local patterns = node.find({ types = { types.NODE_TYPES.PATTERN } })
assert.equals(2, #patterns)
end)
it("finds nodes by file", function()
node.create(types.NODE_TYPES.PATTERN, { s = "Test 1" }, { f = "file1.lua" })
node.create(types.NODE_TYPES.PATTERN, { s = "Test 2" }, { f = "file2.lua" })
node.create(types.NODE_TYPES.PATTERN, { s = "Test 3" }, { f = "file1.lua" })
local found = node.find({ file = "file1.lua" })
assert.equals(2, #found)
end)
it("finds nodes by query", function()
node.create(types.NODE_TYPES.PATTERN, { s = "Foo bar baz" }, {})
node.create(types.NODE_TYPES.PATTERN, { s = "Something else" }, {})
local found = node.find({ query = "foo" })
assert.equals(1, #found)
assert.equals("Foo bar baz", found[1].c.s)
end)
it("respects limit", function()
for i = 1, 10 do
node.create(types.NODE_TYPES.PATTERN, { s = "Node " .. i }, {})
end
local found = node.find({ limit = 5 })
assert.equals(5, #found)
end)
end)
describe("record_usage", function()
it("increments usage count", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
node.record_usage(created.id, true)
local updated = node.get(created.id)
assert.equals(1, updated.sc.u)
end)
it("updates success rate", function()
local created = node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
node.record_usage(created.id, true)
node.record_usage(created.id, false)
local updated = node.get(created.id)
assert.equals(0.5, updated.sc.sr)
end)
end)
describe("get_and_clear_pending", function()
it("returns and clears pending changes", function()
node.pending = {}
node.create(types.NODE_TYPES.PATTERN, { s = "Test" }, {})
local pending = node.get_and_clear_pending()
assert.equals(1, #pending)
assert.equals(0, #node.pending)
end)
end)
end)

View File

@@ -1,173 +0,0 @@
--- Tests for brain/storage.lua
describe("brain.storage", function()
local storage
local test_root = "/tmp/codetyper_test_" .. os.time()
before_each(function()
-- Clear module cache to get fresh state
package.loaded["codetyper.brain.storage"] = nil
package.loaded["codetyper.brain.types"] = nil
storage = require("codetyper.brain.storage")
-- Clear cache before each test
storage.clear_cache()
-- Create test directory
vim.fn.mkdir(test_root, "p")
end)
after_each(function()
-- Clean up test directory
vim.fn.delete(test_root, "rf")
storage.clear_cache()
end)
describe("get_brain_dir", function()
it("returns correct path", function()
local dir = storage.get_brain_dir(test_root)
assert.equals(test_root .. "/.coder/brain", dir)
end)
end)
describe("ensure_dirs", function()
it("creates required directories", function()
local result = storage.ensure_dirs(test_root)
assert.is_true(result)
-- Check directories exist
assert.equals(1, vim.fn.isdirectory(test_root .. "/.coder/brain"))
assert.equals(1, vim.fn.isdirectory(test_root .. "/.coder/brain/nodes"))
assert.equals(1, vim.fn.isdirectory(test_root .. "/.coder/brain/indices"))
assert.equals(1, vim.fn.isdirectory(test_root .. "/.coder/brain/deltas"))
assert.equals(1, vim.fn.isdirectory(test_root .. "/.coder/brain/deltas/objects"))
end)
end)
describe("get_path", function()
it("returns correct path for simple key", function()
local path = storage.get_path("meta", test_root)
assert.equals(test_root .. "/.coder/brain/meta.json", path)
end)
it("returns correct path for nested key", function()
local path = storage.get_path("nodes.patterns", test_root)
assert.equals(test_root .. "/.coder/brain/nodes/patterns.json", path)
end)
it("returns correct path for deeply nested key", function()
local path = storage.get_path("deltas.objects.abc123", test_root)
assert.equals(test_root .. "/.coder/brain/deltas/objects/abc123.json", path)
end)
end)
describe("save and load", function()
it("saves and loads data correctly", function()
storage.ensure_dirs(test_root)
local data = { test = "value", count = 42 }
storage.save("meta", data, test_root, true) -- immediate
-- Clear cache and reload
storage.clear_cache()
local loaded = storage.load("meta", test_root)
assert.equals("value", loaded.test)
assert.equals(42, loaded.count)
end)
it("returns empty table for missing files", function()
storage.ensure_dirs(test_root)
local loaded = storage.load("nonexistent", test_root)
assert.same({}, loaded)
end)
end)
describe("get_meta", function()
it("creates default meta if not exists", function()
storage.ensure_dirs(test_root)
local meta = storage.get_meta(test_root)
assert.is_not_nil(meta.v)
assert.equals(0, meta.nc)
assert.equals(0, meta.ec)
assert.equals(0, meta.dc)
end)
end)
describe("update_meta", function()
it("updates meta values", function()
storage.ensure_dirs(test_root)
storage.update_meta({ nc = 5 }, test_root)
local meta = storage.get_meta(test_root)
assert.equals(5, meta.nc)
end)
end)
describe("get/save_nodes", function()
it("saves and retrieves nodes by type", function()
storage.ensure_dirs(test_root)
local nodes = {
["n_pat_123_abc"] = { id = "n_pat_123_abc", t = "pat" },
["n_pat_456_def"] = { id = "n_pat_456_def", t = "pat" },
}
storage.save_nodes("patterns", nodes, test_root)
storage.flush("nodes.patterns", test_root)
storage.clear_cache()
local loaded = storage.get_nodes("patterns", test_root)
assert.equals(2, vim.tbl_count(loaded))
assert.equals("n_pat_123_abc", loaded["n_pat_123_abc"].id)
end)
end)
describe("get/save_graph", function()
it("saves and retrieves graph", function()
storage.ensure_dirs(test_root)
local graph = {
adj = { node1 = { sem = { "node2" } } },
radj = { node2 = { sem = { "node1" } } },
}
storage.save_graph(graph, test_root)
storage.flush("graph", test_root)
storage.clear_cache()
local loaded = storage.get_graph(test_root)
assert.same({ "node2" }, loaded.adj.node1.sem)
end)
end)
describe("get/set_head", function()
it("stores and retrieves HEAD", function()
storage.ensure_dirs(test_root)
storage.set_head("abc12345", test_root)
storage.flush("meta", test_root) -- Ensure written to disk
storage.clear_cache()
local head = storage.get_head(test_root)
assert.equals("abc12345", head)
end)
end)
describe("exists", function()
it("returns false for non-existent brain", function()
assert.is_false(storage.exists(test_root))
end)
it("returns true after ensure_dirs", function()
storage.ensure_dirs(test_root)
assert.is_true(storage.exists(test_root))
end)
end)
end)

View File

@@ -1,194 +0,0 @@
--- Tests for coder file context injection
describe("coder context injection", function()
local test_dir
local original_filereadable
before_each(function()
test_dir = "/tmp/codetyper_coder_test_" .. os.time()
vim.fn.mkdir(test_dir, "p")
-- Store original function
original_filereadable = vim.fn.filereadable
end)
after_each(function()
vim.fn.delete(test_dir, "rf")
vim.fn.filereadable = original_filereadable
end)
describe("get_coder_companion_path logic", function()
-- Test the path generation logic (simulating the function behavior)
local function get_coder_companion_path(target_path, file_exists_check)
if not target_path or target_path == "" then
return nil
end
-- Skip if target is already a coder file
if target_path:match("%.coder%.") then
return nil
end
local dir = vim.fn.fnamemodify(target_path, ":h")
local name = vim.fn.fnamemodify(target_path, ":t:r")
local ext = vim.fn.fnamemodify(target_path, ":e")
local coder_path = dir .. "/" .. name .. ".coder." .. ext
if file_exists_check(coder_path) then
return coder_path
end
return nil
end
it("should generate correct coder path for source file", function()
local target = "/path/to/file.ts"
local expected = "/path/to/file.coder.ts"
local path = get_coder_companion_path(target, function() return true end)
assert.equals(expected, path)
end)
it("should return nil for empty path", function()
local path = get_coder_companion_path("", function() return true end)
assert.is_nil(path)
end)
it("should return nil for nil path", function()
local path = get_coder_companion_path(nil, function() return true end)
assert.is_nil(path)
end)
it("should return nil for coder files (avoid recursion)", function()
local target = "/path/to/file.coder.ts"
local path = get_coder_companion_path(target, function() return true end)
assert.is_nil(path)
end)
it("should return nil if coder file doesn't exist", function()
local target = "/path/to/file.ts"
local path = get_coder_companion_path(target, function() return false end)
assert.is_nil(path)
end)
it("should handle files with multiple dots", function()
local target = "/path/to/my.component.ts"
local expected = "/path/to/my.component.coder.ts"
local path = get_coder_companion_path(target, function() return true end)
assert.equals(expected, path)
end)
it("should handle different extensions", function()
local test_cases = {
{ target = "/path/file.lua", expected = "/path/file.coder.lua" },
{ target = "/path/file.py", expected = "/path/file.coder.py" },
{ target = "/path/file.js", expected = "/path/file.coder.js" },
{ target = "/path/file.go", expected = "/path/file.coder.go" },
}
for _, tc in ipairs(test_cases) do
local path = get_coder_companion_path(tc.target, function() return true end)
assert.equals(tc.expected, path, "Failed for: " .. tc.target)
end
end)
end)
describe("coder content filtering", function()
-- Test the filtering logic that skips template-only content
local function has_meaningful_content(lines)
for _, line in ipairs(lines) do
local trimmed = line:gsub("^%s*", "")
if not trimmed:match("^[%-#/]+%s*Coder companion")
and not trimmed:match("^[%-#/]+%s*Use /@ @/")
and not trimmed:match("^[%-#/]+%s*Example:")
and not trimmed:match("^<!%-%-")
and trimmed ~= ""
and not trimmed:match("^[%-#/]+%s*$") then
return true
end
end
return false
end
it("should detect meaningful content", function()
local lines = {
"-- Coder companion for test.lua",
"-- This file handles authentication",
"/@",
"Add login function",
"@/",
}
assert.is_true(has_meaningful_content(lines))
end)
it("should reject template-only content", function()
-- Template lines are filtered by specific patterns
-- Only header comments that match the template format are filtered
local lines = {
"-- Coder companion for test.lua",
"-- Use /@ @/ tags to write pseudo-code prompts",
"-- Example:",
"--",
"",
}
assert.is_false(has_meaningful_content(lines))
end)
it("should detect pseudo-code content", function()
local lines = {
"-- Authentication module",
"",
"-- This module should:",
"-- 1. Validate user credentials",
"-- 2. Generate JWT tokens",
"-- 3. Handle session management",
}
-- "-- Authentication module" doesn't match template patterns
assert.is_true(has_meaningful_content(lines))
end)
it("should handle JavaScript style comments", function()
local lines = {
"// Coder companion for test.ts",
"// Business logic for user authentication",
"",
"// The auth flow should:",
"// 1. Check OAuth token",
"// 2. Validate permissions",
}
-- "// Business logic..." doesn't match template patterns
assert.is_true(has_meaningful_content(lines))
end)
it("should handle empty lines", function()
local lines = {
"",
"",
"",
}
assert.is_false(has_meaningful_content(lines))
end)
end)
describe("context format", function()
it("should format context with proper header", function()
local function format_coder_context(content, ext)
return string.format(
"\n\n--- Business Context / Pseudo-code ---\n" ..
"The following describes the intended behavior and design for this file:\n" ..
"```%s\n%s\n```",
ext,
content
)
end
local formatted = format_coder_context("-- Auth logic here", "lua")
assert.is_true(formatted:find("Business Context") ~= nil)
assert.is_true(formatted:find("```lua") ~= nil)
assert.is_true(formatted:find("Auth logic here") ~= nil)
end)
end)
end)

View File

@@ -1,161 +0,0 @@
--- Tests for coder file ignore logic
describe("coder file ignore logic", function()
-- Directories to ignore
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
local ignored_files = {
".gitignore",
".gitattributes",
"package-lock.json",
"yarn.lock",
".env",
".eslintrc",
"tsconfig.json",
"README.md",
"LICENSE",
"Makefile",
}
local function is_in_ignored_directory(filepath)
for _, dir in ipairs(ignored_directories) do
if filepath:match("/" .. dir .. "/") or filepath:match("/" .. dir .. "$") then
return true
end
if filepath:match("^" .. dir .. "/") then
return true
end
end
return false
end
local function should_ignore_for_coder(filepath)
local filename = vim.fn.fnamemodify(filepath, ":t")
for _, ignored in ipairs(ignored_files) do
if filename == ignored then
return true
end
end
if filename:match("^%.") then
return true
end
if is_in_ignored_directory(filepath) then
return true
end
return false
end
describe("ignored directories", function()
it("should ignore files in node_modules", function()
assert.is_true(should_ignore_for_coder("/project/node_modules/lodash/index.js"))
assert.is_true(should_ignore_for_coder("/project/node_modules/react/index.js"))
end)
it("should ignore files in .git", function()
assert.is_true(should_ignore_for_coder("/project/.git/config"))
assert.is_true(should_ignore_for_coder("/project/.git/hooks/pre-commit"))
end)
it("should ignore files in .coder", function()
assert.is_true(should_ignore_for_coder("/project/.coder/brain/meta.json"))
end)
it("should ignore files in vendor", function()
assert.is_true(should_ignore_for_coder("/project/vendor/autoload.php"))
end)
it("should ignore files in dist/build", function()
assert.is_true(should_ignore_for_coder("/project/dist/bundle.js"))
assert.is_true(should_ignore_for_coder("/project/build/output.js"))
end)
it("should ignore files in __pycache__", function()
assert.is_true(should_ignore_for_coder("/project/__pycache__/module.cpython-39.pyc"))
end)
it("should NOT ignore regular source files", function()
assert.is_false(should_ignore_for_coder("/project/src/index.ts"))
assert.is_false(should_ignore_for_coder("/project/lib/utils.lua"))
assert.is_false(should_ignore_for_coder("/project/app/main.py"))
end)
end)
describe("ignored files", function()
it("should ignore .gitignore", function()
assert.is_true(should_ignore_for_coder("/project/.gitignore"))
end)
it("should ignore lock files", function()
assert.is_true(should_ignore_for_coder("/project/package-lock.json"))
assert.is_true(should_ignore_for_coder("/project/yarn.lock"))
end)
it("should ignore config files", function()
assert.is_true(should_ignore_for_coder("/project/tsconfig.json"))
assert.is_true(should_ignore_for_coder("/project/.eslintrc"))
end)
it("should ignore .env files", function()
assert.is_true(should_ignore_for_coder("/project/.env"))
end)
it("should ignore README and LICENSE", function()
assert.is_true(should_ignore_for_coder("/project/README.md"))
assert.is_true(should_ignore_for_coder("/project/LICENSE"))
end)
it("should ignore hidden/dot files", function()
assert.is_true(should_ignore_for_coder("/project/.hidden"))
assert.is_true(should_ignore_for_coder("/project/.secret"))
end)
it("should NOT ignore regular source files", function()
assert.is_false(should_ignore_for_coder("/project/src/app.ts"))
assert.is_false(should_ignore_for_coder("/project/components/Button.tsx"))
assert.is_false(should_ignore_for_coder("/project/utils/helpers.js"))
end)
end)
describe("edge cases", function()
it("should handle nested node_modules", function()
assert.is_true(should_ignore_for_coder("/project/packages/core/node_modules/dep/index.js"))
end)
it("should handle files named like directories but not in them", function()
-- A file named "node_modules.md" in root should be ignored (starts with .)
-- But a file in a folder that contains "node" should NOT be ignored
assert.is_false(should_ignore_for_coder("/project/src/node_utils.ts"))
end)
it("should handle relative paths", function()
assert.is_true(should_ignore_for_coder("node_modules/lodash/index.js"))
assert.is_false(should_ignore_for_coder("src/index.ts"))
end)
end)
end)

View File

@@ -1,148 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/confidence.lua
describe("confidence", function()
local confidence = require("codetyper.agent.confidence")
describe("weights", function()
it("should have weights that sum to 1.0", function()
local total = 0
for _, weight in pairs(confidence.weights) do
total = total + weight
end
assert.is_near(1.0, total, 0.001)
end)
end)
describe("score", function()
it("should return 0 for empty response", function()
local score, breakdown = confidence.score("", "some prompt")
assert.equals(0, score)
assert.equals(0, breakdown.weighted_total)
end)
it("should return high score for good response", function()
local good_response = [[
function validateEmail(email)
local pattern = "^[%w%.]+@[%w%.]+%.%w+$"
return string.match(email, pattern) ~= nil
end
]]
local score, breakdown = confidence.score(good_response, "create email validator")
assert.is_true(score > 0.7)
assert.is_true(breakdown.syntax > 0.5)
end)
it("should return lower score for response with uncertainty", function()
local uncertain_response = [[
-- I'm not sure if this is correct, maybe try:
function doSomething()
-- TODO: implement this
-- placeholder code here
end
]]
local score, _ = confidence.score(uncertain_response, "implement function")
assert.is_true(score < 0.7)
end)
it("should penalize unbalanced brackets", function()
local unbalanced = [[
function test() {
if (true) {
console.log("missing bracket")
]]
local _, breakdown = confidence.score(unbalanced, "test")
assert.is_true(breakdown.syntax < 0.7)
end)
it("should penalize short responses to long prompts", function()
local long_prompt = "Create a comprehensive function that handles user authentication, " ..
"validates credentials against the database, generates JWT tokens, " ..
"handles refresh tokens, and logs all authentication attempts"
local short_response = "done"
local score, breakdown = confidence.score(short_response, long_prompt)
assert.is_true(breakdown.length < 0.5)
end)
it("should penalize repetitive code", function()
local repetitive = [[
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
]]
local _, breakdown = confidence.score(repetitive, "test")
assert.is_true(breakdown.repetition < 0.7)
end)
it("should penalize truncated responses", function()
local truncated = [[
function process(data) {
const result = data.map(item => {
return {
id: item.id,
name: item...
]]
local _, breakdown = confidence.score(truncated, "test")
assert.is_true(breakdown.truncation < 1.0)
end)
end)
describe("needs_escalation", function()
it("should return true for low confidence", function()
assert.is_true(confidence.needs_escalation(0.5, 0.7))
assert.is_true(confidence.needs_escalation(0.3, 0.7))
end)
it("should return false for high confidence", function()
assert.is_false(confidence.needs_escalation(0.8, 0.7))
assert.is_false(confidence.needs_escalation(0.95, 0.7))
end)
it("should use default threshold of 0.7", function()
assert.is_true(confidence.needs_escalation(0.6))
assert.is_false(confidence.needs_escalation(0.8))
end)
end)
describe("level_name", function()
it("should return correct level names", function()
assert.equals("excellent", confidence.level_name(0.95))
assert.equals("good", confidence.level_name(0.85))
assert.equals("acceptable", confidence.level_name(0.75))
assert.equals("uncertain", confidence.level_name(0.6))
assert.equals("poor", confidence.level_name(0.3))
end)
end)
describe("format_breakdown", function()
it("should format breakdown correctly", function()
local breakdown = {
length = 0.8,
uncertainty = 0.9,
syntax = 1.0,
repetition = 0.85,
truncation = 0.95,
weighted_total = 0.9,
}
local formatted = confidence.format_breakdown(breakdown)
assert.is_true(formatted:match("len:0.80"))
assert.is_true(formatted:match("unc:0.90"))
assert.is_true(formatted:match("syn:1.00"))
end)
end)
end)

View File

@@ -1,137 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/config.lua
describe("config", function()
local config = require("codetyper.config")
describe("defaults", function()
local defaults = config.defaults
it("should have llm configuration", function()
assert.is_table(defaults.llm)
assert.equals("claude", defaults.llm.provider)
end)
it("should have scheduler configuration", function()
assert.is_table(defaults.scheduler)
assert.is_boolean(defaults.scheduler.enabled)
assert.is_boolean(defaults.scheduler.ollama_scout)
assert.is_number(defaults.scheduler.escalation_threshold)
end)
it("should have claude configuration", function()
assert.is_table(defaults.llm.claude)
assert.is_truthy(defaults.llm.claude.model)
end)
it("should have openai configuration", function()
assert.is_table(defaults.llm.openai)
assert.is_truthy(defaults.llm.openai.model)
end)
it("should have gemini configuration", function()
assert.is_table(defaults.llm.gemini)
assert.is_truthy(defaults.llm.gemini.model)
end)
it("should have ollama configuration", function()
assert.is_table(defaults.llm.ollama)
assert.is_truthy(defaults.llm.ollama.host)
assert.is_truthy(defaults.llm.ollama.model)
end)
end)
describe("merge", function()
it("should merge user config with defaults", function()
local user_config = {
llm = {
provider = "openai",
},
}
local merged = config.merge(user_config)
-- User value should override
assert.equals("openai", merged.llm.provider)
-- Other defaults should be preserved
assert.equals(25, merged.window.width)
end)
it("should deep merge nested tables", function()
local user_config = {
llm = {
claude = {
model = "claude-opus-4",
},
},
}
local merged = config.merge(user_config)
-- User value should override
assert.equals("claude-opus-4", merged.llm.claude.model)
-- Provider default should be preserved
assert.equals("claude", merged.llm.provider)
end)
it("should handle empty user config", function()
local merged = config.merge({})
assert.equals("claude", merged.llm.provider)
assert.equals(25, merged.window.width)
end)
it("should handle nil user config", function()
local merged = config.merge(nil)
assert.equals("claude", merged.llm.provider)
end)
end)
describe("validate", function()
it("should return true for valid config", function()
local valid_config = config.defaults
local is_valid, err = config.validate(valid_config)
assert.is_true(is_valid)
assert.is_nil(err)
end)
it("should validate provider value", function()
local invalid_config = vim.tbl_deep_extend("force", {}, config.defaults)
invalid_config.llm.provider = "invalid_provider"
local is_valid, err = config.validate(invalid_config)
assert.is_false(is_valid)
assert.is_truthy(err)
end)
it("should validate window width range", function()
local invalid_config = vim.tbl_deep_extend("force", {}, config.defaults)
invalid_config.window.width = 101 -- Over 100%
local is_valid, err = config.validate(invalid_config)
assert.is_false(is_valid)
end)
it("should validate window position", function()
local invalid_config = vim.tbl_deep_extend("force", {}, config.defaults)
invalid_config.window.position = "center" -- Invalid
local is_valid, err = config.validate(invalid_config)
assert.is_false(is_valid)
end)
it("should validate scheduler threshold range", function()
local invalid_config = vim.tbl_deep_extend("force", {}, config.defaults)
invalid_config.scheduler.escalation_threshold = 1.5 -- Over 1.0
local is_valid, err = config.validate(invalid_config)
assert.is_false(is_valid)
end)
end)
end)

View File

@@ -1,345 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/indexer/init.lua
describe("indexer", function()
local indexer
local utils
-- Mock cwd for testing
local test_cwd = "/tmp/codetyper_test_indexer"
before_each(function()
-- Reset modules
package.loaded["codetyper.indexer"] = nil
package.loaded["codetyper.indexer.scanner"] = nil
package.loaded["codetyper.indexer.analyzer"] = nil
package.loaded["codetyper.indexer.memory"] = nil
package.loaded["codetyper.utils"] = nil
indexer = require("codetyper.indexer")
utils = require("codetyper.utils")
-- Create test directory structure
vim.fn.mkdir(test_cwd, "p")
vim.fn.mkdir(test_cwd .. "/.coder", "p")
vim.fn.mkdir(test_cwd .. "/src", "p")
-- Mock getcwd to return test directory
vim.fn.getcwd = function()
return test_cwd
end
-- Mock get_project_root
package.loaded["codetyper.utils"].get_project_root = function()
return test_cwd
end
end)
after_each(function()
-- Clean up test directory
vim.fn.delete(test_cwd, "rf")
end)
describe("setup", function()
it("should accept configuration options", function()
indexer.setup({
enabled = true,
auto_index = false,
})
local config = indexer.get_config()
assert.is_false(config.auto_index)
end)
it("should use default configuration when no options provided", function()
indexer.setup()
local config = indexer.get_config()
assert.is_true(config.enabled)
end)
end)
describe("load_index", function()
it("should return nil when no index exists", function()
local index = indexer.load_index()
assert.is_nil(index)
end)
it("should load existing index from file", function()
-- Create a mock index file
local mock_index = {
version = 1,
project_root = test_cwd,
project_name = "test",
project_type = "node",
dependencies = {},
dev_dependencies = {},
files = {},
symbols = {},
last_indexed = os.time(),
stats = { files = 0, functions = 0, classes = 0, exports = 0 },
}
utils.write_file(test_cwd .. "/.coder/index.json", vim.json.encode(mock_index))
local index = indexer.load_index()
assert.is_table(index)
assert.equals("test", index.project_name)
assert.equals("node", index.project_type)
end)
it("should cache loaded index", function()
local mock_index = {
version = 1,
project_root = test_cwd,
project_name = "cached_test",
project_type = "lua",
dependencies = {},
dev_dependencies = {},
files = {},
symbols = {},
last_indexed = os.time(),
stats = { files = 0, functions = 0, classes = 0, exports = 0 },
}
utils.write_file(test_cwd .. "/.coder/index.json", vim.json.encode(mock_index))
local index1 = indexer.load_index()
local index2 = indexer.load_index()
assert.equals(index1.project_name, index2.project_name)
end)
end)
describe("save_index", function()
it("should save index to file", function()
local index = {
version = 1,
project_root = test_cwd,
project_name = "save_test",
project_type = "node",
dependencies = { express = "^4.18.0" },
dev_dependencies = {},
files = {},
symbols = {},
last_indexed = os.time(),
stats = { files = 0, functions = 0, classes = 0, exports = 0 },
}
local result = indexer.save_index(index)
assert.is_true(result)
-- Verify file was created
local content = utils.read_file(test_cwd .. "/.coder/index.json")
assert.is_truthy(content)
local decoded = vim.json.decode(content)
assert.equals("save_test", decoded.project_name)
end)
it("should create .coder directory if it does not exist", function()
vim.fn.delete(test_cwd .. "/.coder", "rf")
local index = {
version = 1,
project_root = test_cwd,
project_name = "test",
project_type = "unknown",
dependencies = {},
dev_dependencies = {},
files = {},
symbols = {},
last_indexed = os.time(),
stats = { files = 0, functions = 0, classes = 0, exports = 0 },
}
indexer.save_index(index)
assert.equals(1, vim.fn.isdirectory(test_cwd .. "/.coder"))
end)
end)
describe("index_project", function()
it("should create an index for the project", function()
-- Create some test files
utils.write_file(test_cwd .. "/package.json", '{"name":"test","dependencies":{}}')
utils.write_file(test_cwd .. "/src/main.lua", [[
local M = {}
function M.hello()
return "world"
end
return M
]])
indexer.setup({ index_extensions = { "lua" } })
local index = indexer.index_project()
assert.is_table(index)
assert.equals("node", index.project_type)
assert.is_truthy(index.stats.files >= 0)
end)
it("should detect project dependencies", function()
utils.write_file(test_cwd .. "/package.json", [[{
"name": "test",
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.0"
}
}]])
indexer.setup()
local index = indexer.index_project()
assert.is_table(index.dependencies)
assert.equals("^4.18.0", index.dependencies.express)
end)
it("should call callback when complete", function()
local callback_called = false
local callback_index = nil
indexer.setup()
indexer.index_project(function(index)
callback_called = true
callback_index = index
end)
assert.is_true(callback_called)
assert.is_table(callback_index)
end)
end)
describe("index_file", function()
it("should index a single file", function()
utils.write_file(test_cwd .. "/src/test.lua", [[
local M = {}
function M.add(a, b)
return a + b
end
function M.subtract(a, b)
return a - b
end
return M
]])
indexer.setup({ index_extensions = { "lua" } })
-- First create an initial index
indexer.index_project()
local file_index = indexer.index_file(test_cwd .. "/src/test.lua")
assert.is_table(file_index)
assert.equals("src/test.lua", file_index.path)
end)
it("should update symbols in the main index", function()
utils.write_file(test_cwd .. "/src/utils.lua", [[
local M = {}
function M.format_string(str)
return string.upper(str)
end
return M
]])
indexer.setup({ index_extensions = { "lua" } })
indexer.index_project()
indexer.index_file(test_cwd .. "/src/utils.lua")
local index = indexer.load_index()
assert.is_table(index.files)
end)
end)
describe("get_status", function()
it("should return indexed: false when no index exists", function()
local status = indexer.get_status()
assert.is_false(status.indexed)
assert.is_nil(status.stats)
end)
it("should return status when index exists", function()
indexer.setup()
indexer.index_project()
local status = indexer.get_status()
assert.is_true(status.indexed)
assert.is_table(status.stats)
assert.is_truthy(status.last_indexed)
end)
end)
describe("get_context_for", function()
it("should return context with project type", function()
utils.write_file(test_cwd .. "/package.json", '{"name":"test"}')
indexer.setup()
indexer.index_project()
local context = indexer.get_context_for({
file = test_cwd .. "/src/main.lua",
prompt = "add a function",
})
assert.is_table(context)
assert.equals("node", context.project_type)
end)
it("should find relevant symbols", function()
utils.write_file(test_cwd .. "/src/utils.lua", [[
local M = {}
function M.calculate_total(items)
return 0
end
return M
]])
indexer.setup({ index_extensions = { "lua" } })
indexer.index_project()
local context = indexer.get_context_for({
file = test_cwd .. "/src/main.lua",
prompt = "use calculate_total function",
})
assert.is_table(context)
-- Should find the calculate symbol
if context.relevant_symbols and context.relevant_symbols.calculate then
assert.is_table(context.relevant_symbols.calculate)
end
end)
end)
describe("clear", function()
it("should remove the index file", function()
indexer.setup()
indexer.index_project()
-- Verify index exists
assert.is_true(indexer.get_status().indexed)
indexer.clear()
-- Verify index is gone
local status = indexer.get_status()
assert.is_false(status.indexed)
end)
end)
describe("schedule_index_file", function()
it("should not index when disabled", function()
indexer.setup({ enabled = false })
-- This should not throw or cause issues
indexer.schedule_index_file(test_cwd .. "/src/test.lua")
end)
it("should not index when auto_index is false", function()
indexer.setup({ enabled = true, auto_index = false })
-- This should not throw or cause issues
indexer.schedule_index_file(test_cwd .. "/src/test.lua")
end)
end)
end)

View File

@@ -1,371 +0,0 @@
--- Tests for smart code injection with import handling
describe("codetyper.agent.inject", function()
local inject
before_each(function()
inject = require("codetyper.agent.inject")
end)
describe("parse_code", function()
describe("JavaScript/TypeScript", function()
it("should detect ES6 named imports", function()
local code = [[import { useState, useEffect } from 'react';
import { Button } from './components';
function App() {
return <div>Hello</div>;
}]]
local result = inject.parse_code(code, "typescript")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("useState"))
assert.truthy(result.imports[2]:match("Button"))
assert.truthy(#result.body > 0)
end)
it("should detect ES6 default imports", function()
local code = [[import React from 'react';
import axios from 'axios';
const api = axios.create();]]
local result = inject.parse_code(code, "javascript")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("React"))
assert.truthy(result.imports[2]:match("axios"))
end)
it("should detect require imports", function()
local code = [[const fs = require('fs');
const path = require('path');
module.exports = { fs, path };]]
local result = inject.parse_code(code, "javascript")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("fs"))
assert.truthy(result.imports[2]:match("path"))
end)
it("should detect multi-line imports", function()
local code = [[import {
useState,
useEffect,
useCallback
} from 'react';
function Component() {}]]
local result = inject.parse_code(code, "typescript")
assert.equals(1, #result.imports)
assert.truthy(result.imports[1]:match("useState"))
assert.truthy(result.imports[1]:match("useCallback"))
end)
it("should detect namespace imports", function()
local code = [[import * as React from 'react';
export default React;]]
local result = inject.parse_code(code, "tsx")
assert.equals(1, #result.imports)
assert.truthy(result.imports[1]:match("%* as React"))
end)
end)
describe("Python", function()
it("should detect simple imports", function()
local code = [[import os
import sys
import json
def main():
pass]]
local result = inject.parse_code(code, "python")
assert.equals(3, #result.imports)
assert.truthy(result.imports[1]:match("import os"))
assert.truthy(result.imports[2]:match("import sys"))
assert.truthy(result.imports[3]:match("import json"))
end)
it("should detect from imports", function()
local code = [[from typing import List, Dict
from pathlib import Path
def process(items: List[str]) -> None:
pass]]
local result = inject.parse_code(code, "py")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("from typing"))
assert.truthy(result.imports[2]:match("from pathlib"))
end)
end)
describe("Lua", function()
it("should detect require statements", function()
local code = [[local M = {}
local utils = require("codetyper.utils")
local config = require('codetyper.config')
function M.setup()
end
return M]]
local result = inject.parse_code(code, "lua")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("utils"))
assert.truthy(result.imports[2]:match("config"))
end)
end)
describe("Go", function()
it("should detect single imports", function()
local code = [[package main
import "fmt"
func main() {
fmt.Println("Hello")
}]]
local result = inject.parse_code(code, "go")
assert.equals(1, #result.imports)
assert.truthy(result.imports[1]:match('import "fmt"'))
end)
it("should detect grouped imports", function()
local code = [[package main
import (
"fmt"
"os"
"strings"
)
func main() {}]]
local result = inject.parse_code(code, "go")
assert.equals(1, #result.imports)
assert.truthy(result.imports[1]:match("fmt"))
assert.truthy(result.imports[1]:match("os"))
end)
end)
describe("Rust", function()
it("should detect use statements", function()
local code = [[use std::io;
use std::collections::HashMap;
fn main() {
let map = HashMap::new();
}]]
local result = inject.parse_code(code, "rs")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("std::io"))
assert.truthy(result.imports[2]:match("HashMap"))
end)
end)
describe("C/C++", function()
it("should detect include statements", function()
local code = [[#include <stdio.h>
#include "myheader.h"
int main() {
return 0;
}]]
local result = inject.parse_code(code, "c")
assert.equals(2, #result.imports)
assert.truthy(result.imports[1]:match("stdio"))
assert.truthy(result.imports[2]:match("myheader"))
end)
end)
end)
describe("merge_imports", function()
it("should merge without duplicates", function()
local existing = {
"import { useState } from 'react';",
"import { Button } from './components';",
}
local new_imports = {
"import { useEffect } from 'react';",
"import { useState } from 'react';", -- duplicate
"import { Card } from './components';",
}
local merged = inject.merge_imports(existing, new_imports)
assert.equals(4, #merged) -- Should not have duplicate useState
end)
it("should handle empty existing imports", function()
local existing = {}
local new_imports = {
"import os",
"import sys",
}
local merged = inject.merge_imports(existing, new_imports)
assert.equals(2, #merged)
end)
it("should handle empty new imports", function()
local existing = {
"import os",
"import sys",
}
local new_imports = {}
local merged = inject.merge_imports(existing, new_imports)
assert.equals(2, #merged)
end)
it("should handle whitespace variations in duplicates", function()
local existing = {
"import { useState } from 'react';",
}
local new_imports = {
"import {useState} from 'react';", -- Same but different spacing
}
local merged = inject.merge_imports(existing, new_imports)
assert.equals(1, #merged) -- Should detect as duplicate
end)
end)
describe("sort_imports", function()
it("should group imports by type for JavaScript", function()
local imports = {
"import React from 'react';",
"import { Button } from './components';",
"import axios from 'axios';",
"import path from 'path';",
}
local sorted = inject.sort_imports(imports, "javascript")
-- Check ordering: builtin -> third-party -> local
local found_builtin = false
local found_local = false
local builtin_pos = 0
local local_pos = 0
for i, imp in ipairs(sorted) do
if imp:match("path") then
found_builtin = true
builtin_pos = i
end
if imp:match("%.%/") then
found_local = true
local_pos = i
end
end
-- Local imports should come after third-party
if found_local and found_builtin then
assert.truthy(local_pos > builtin_pos)
end
end)
end)
describe("has_imports", function()
it("should return true when code has imports", function()
local code = [[import { useState } from 'react';
function App() {}]]
assert.is_true(inject.has_imports(code, "typescript"))
end)
it("should return false when code has no imports", function()
local code = [[function App() {
return <div>Hello</div>;
}]]
assert.is_false(inject.has_imports(code, "typescript"))
end)
it("should detect Python imports", function()
local code = [[from typing import List
def process(items: List[str]):
pass]]
assert.is_true(inject.has_imports(code, "python"))
end)
it("should detect Lua requires", function()
local code = [[local utils = require("utils")
local M = {}
return M]]
assert.is_true(inject.has_imports(code, "lua"))
end)
end)
describe("edge cases", function()
it("should handle empty code", function()
local result = inject.parse_code("", "javascript")
assert.equals(0, #result.imports)
assert.equals(1, #result.body) -- Empty string becomes one empty line
end)
it("should handle code with only imports", function()
local code = [[import React from 'react';
import { useState } from 'react';]]
local result = inject.parse_code(code, "javascript")
assert.equals(2, #result.imports)
assert.equals(0, #result.body)
end)
it("should handle code with only body", function()
local code = [[function hello() {
console.log("Hello");
}]]
local result = inject.parse_code(code, "javascript")
assert.equals(0, #result.imports)
assert.truthy(#result.body > 0)
end)
it("should handle imports in string literals (not detect as imports)", function()
local code = [[const example = "import { fake } from 'not-real';";
const config = { import: true };
function test() {}]]
local result = inject.parse_code(code, "javascript")
-- The first line looks like an import but is in a string
-- This is a known limitation - we accept some false positives
-- The important thing is we don't break the code
assert.truthy(#result.body >= 0)
end)
it("should handle mixed import styles in same file", function()
local code = [[import React from 'react';
const axios = require('axios');
import { useState } from 'react';
function App() {}]]
local result = inject.parse_code(code, "javascript")
assert.equals(3, #result.imports)
end)
end)
end)

View File

@@ -1,286 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/intent.lua
describe("intent", function()
local intent = require("codetyper.agent.intent")
describe("detect", function()
describe("complete intent", function()
it("should detect 'complete' keyword", function()
local result = intent.detect("complete this function")
assert.equals("complete", result.type)
assert.equals("replace", result.action)
end)
it("should detect 'finish' keyword", function()
local result = intent.detect("finish implementing this method")
assert.equals("complete", result.type)
end)
it("should detect 'implement' keyword", function()
local result = intent.detect("implement the sorting algorithm")
assert.equals("complete", result.type)
end)
it("should detect 'todo' keyword", function()
local result = intent.detect("fix the TODO here")
assert.equals("complete", result.type)
end)
end)
describe("refactor intent", function()
it("should detect 'refactor' keyword", function()
local result = intent.detect("refactor this messy code")
assert.equals("refactor", result.type)
assert.equals("replace", result.action)
end)
it("should detect 'rewrite' keyword", function()
local result = intent.detect("rewrite using async/await")
assert.equals("refactor", result.type)
end)
it("should detect 'simplify' keyword", function()
local result = intent.detect("simplify this logic")
assert.equals("refactor", result.type)
end)
it("should detect 'cleanup' keyword", function()
local result = intent.detect("cleanup this code")
assert.equals("refactor", result.type)
end)
end)
describe("fix intent", function()
it("should detect 'fix' keyword", function()
local result = intent.detect("fix the bug in this function")
assert.equals("fix", result.type)
assert.equals("replace", result.action)
end)
it("should detect 'debug' keyword", function()
local result = intent.detect("debug this issue")
assert.equals("fix", result.type)
end)
it("should detect 'bug' keyword", function()
local result = intent.detect("there's a bug here")
assert.equals("fix", result.type)
end)
it("should detect 'error' keyword", function()
local result = intent.detect("getting an error with this code")
assert.equals("fix", result.type)
end)
end)
describe("add intent", function()
it("should detect 'add' keyword", function()
local result = intent.detect("add input validation")
assert.equals("add", result.type)
assert.equals("insert", result.action)
end)
it("should detect 'create' keyword", function()
local result = intent.detect("create a new helper function")
assert.equals("add", result.type)
end)
it("should detect 'generate' keyword", function()
local result = intent.detect("generate a utility function")
assert.equals("add", result.type)
end)
end)
describe("document intent", function()
it("should detect 'document' keyword", function()
local result = intent.detect("document this function")
assert.equals("document", result.type)
assert.equals("replace", result.action)
end)
it("should detect 'jsdoc' keyword", function()
local result = intent.detect("add jsdoc comments")
assert.equals("document", result.type)
end)
it("should detect 'comment' keyword", function()
local result = intent.detect("add comments to explain")
assert.equals("document", result.type)
end)
end)
describe("test intent", function()
it("should detect 'test' keyword", function()
local result = intent.detect("write tests for this function")
assert.equals("test", result.type)
assert.equals("append", result.action)
end)
it("should detect 'unit test' keyword", function()
local result = intent.detect("create unit tests")
assert.equals("test", result.type)
end)
end)
describe("optimize intent", function()
it("should detect 'optimize' keyword", function()
local result = intent.detect("optimize this loop")
assert.equals("optimize", result.type)
assert.equals("replace", result.action)
end)
it("should detect 'performance' keyword", function()
local result = intent.detect("improve performance of this function")
assert.equals("optimize", result.type)
end)
it("should detect 'faster' keyword", function()
local result = intent.detect("make this faster")
assert.equals("optimize", result.type)
end)
end)
describe("explain intent", function()
it("should detect 'explain' keyword", function()
local result = intent.detect("explain what this does")
assert.equals("explain", result.type)
assert.equals("none", result.action)
end)
it("should detect 'what does' pattern", function()
local result = intent.detect("what does this function do")
assert.equals("explain", result.type)
end)
it("should detect 'how does' pattern", function()
local result = intent.detect("how does this algorithm work")
assert.equals("explain", result.type)
end)
end)
describe("default intent", function()
it("should default to 'add' for unknown prompts", function()
local result = intent.detect("make it blue")
assert.equals("add", result.type)
end)
end)
describe("scope hints", function()
it("should detect 'this function' scope hint", function()
local result = intent.detect("refactor this function")
assert.equals("function", result.scope_hint)
end)
it("should detect 'this class' scope hint", function()
local result = intent.detect("document this class")
assert.equals("class", result.scope_hint)
end)
it("should detect 'this file' scope hint", function()
local result = intent.detect("test this file")
assert.equals("file", result.scope_hint)
end)
end)
describe("confidence", function()
it("should have higher confidence with more keyword matches", function()
local result1 = intent.detect("fix")
local result2 = intent.detect("fix the bug error")
assert.is_true(result2.confidence >= result1.confidence)
end)
it("should cap confidence at 1.0", function()
local result = intent.detect("fix debug bug error issue solve")
assert.is_true(result.confidence <= 1.0)
end)
end)
end)
describe("modifies_code", function()
it("should return true for replacement intents", function()
assert.is_true(intent.modifies_code({ action = "replace" }))
end)
it("should return true for insertion intents", function()
assert.is_true(intent.modifies_code({ action = "insert" }))
end)
it("should return false for explain intent", function()
assert.is_false(intent.modifies_code({ action = "none" }))
end)
end)
describe("is_replacement", function()
it("should return true for replace action", function()
assert.is_true(intent.is_replacement({ action = "replace" }))
end)
it("should return false for insert action", function()
assert.is_false(intent.is_replacement({ action = "insert" }))
end)
end)
describe("is_insertion", function()
it("should return true for insert action", function()
assert.is_true(intent.is_insertion({ action = "insert" }))
end)
it("should return true for append action", function()
assert.is_true(intent.is_insertion({ action = "append" }))
end)
it("should return false for replace action", function()
assert.is_false(intent.is_insertion({ action = "replace" }))
end)
end)
describe("get_prompt_modifier", function()
it("should return modifier for each intent type", function()
local types = { "complete", "refactor", "fix", "add", "document", "test", "optimize", "explain" }
for _, type_name in ipairs(types) do
local modifier = intent.get_prompt_modifier({ type = type_name })
assert.is_truthy(modifier)
assert.is_true(#modifier > 0)
end
end)
it("should return add modifier for unknown type", function()
local modifier = intent.get_prompt_modifier({ type = "unknown" })
assert.is_truthy(modifier)
end)
end)
describe("format", function()
it("should format intent correctly", function()
local i = {
type = "refactor",
scope_hint = "function",
action = "replace",
confidence = 0.85,
}
local formatted = intent.format(i)
assert.is_true(formatted:match("refactor"))
assert.is_true(formatted:match("function"))
assert.is_true(formatted:match("replace"))
assert.is_true(formatted:match("0.85"))
end)
it("should handle nil scope_hint", function()
local i = {
type = "add",
scope_hint = nil,
action = "insert",
confidence = 0.5,
}
local formatted = intent.format(i)
assert.is_true(formatted:match("auto"))
end)
end)
end)

View File

@@ -1,174 +0,0 @@
--- Tests for smart LLM selection with memory-based confidence
describe("codetyper.llm.selector", function()
local selector
before_each(function()
selector = require("codetyper.llm.selector")
-- Reset stats for clean tests
selector.reset_accuracy_stats()
end)
describe("select_provider", function()
it("should return copilot when no brain memories exist", function()
local result = selector.select_provider("write a function", {
file_path = "/test/file.lua",
})
assert.equals("copilot", result.provider)
assert.equals(0, result.memory_count)
assert.truthy(result.reason:match("Insufficient context"))
end)
it("should return a valid selection result structure", function()
local result = selector.select_provider("test prompt", {})
assert.is_string(result.provider)
assert.is_number(result.confidence)
assert.is_number(result.memory_count)
assert.is_string(result.reason)
end)
it("should have confidence between 0 and 1", function()
local result = selector.select_provider("test", {})
assert.truthy(result.confidence >= 0)
assert.truthy(result.confidence <= 1)
end)
end)
describe("should_ponder", function()
it("should return true for medium confidence", function()
assert.is_true(selector.should_ponder(0.5))
assert.is_true(selector.should_ponder(0.6))
end)
it("should return false for low confidence", function()
assert.is_false(selector.should_ponder(0.2))
assert.is_false(selector.should_ponder(0.3))
end)
-- High confidence pondering is probabilistic, so we test the range
it("should sometimes ponder for high confidence (sampling)", function()
-- Run multiple times to test probabilistic behavior
local pondered_count = 0
for _ = 1, 100 do
if selector.should_ponder(0.9) then
pondered_count = pondered_count + 1
end
end
-- Should ponder roughly 20% of the time (PONDER_SAMPLE_RATE = 0.2)
-- Allow range of 5-40% due to randomness
assert.truthy(pondered_count >= 5, "Should ponder at least sometimes")
assert.truthy(pondered_count <= 40, "Should not ponder too often")
end)
end)
describe("get_accuracy_stats", function()
it("should return initial empty stats", function()
local stats = selector.get_accuracy_stats()
assert.equals(0, stats.ollama.total)
assert.equals(0, stats.ollama.correct)
assert.equals(0, stats.ollama.accuracy)
assert.equals(0, stats.copilot.total)
assert.equals(0, stats.copilot.correct)
assert.equals(0, stats.copilot.accuracy)
end)
end)
describe("report_feedback", function()
it("should track positive feedback", function()
selector.report_feedback("ollama", true)
selector.report_feedback("ollama", true)
selector.report_feedback("ollama", false)
local stats = selector.get_accuracy_stats()
assert.equals(3, stats.ollama.total)
assert.equals(2, stats.ollama.correct)
end)
it("should track copilot feedback separately", function()
selector.report_feedback("ollama", true)
selector.report_feedback("copilot", true)
selector.report_feedback("copilot", false)
local stats = selector.get_accuracy_stats()
assert.equals(1, stats.ollama.total)
assert.equals(2, stats.copilot.total)
end)
it("should calculate accuracy correctly", function()
selector.report_feedback("ollama", true)
selector.report_feedback("ollama", true)
selector.report_feedback("ollama", true)
selector.report_feedback("ollama", false)
local stats = selector.get_accuracy_stats()
assert.equals(0.75, stats.ollama.accuracy)
end)
end)
describe("reset_accuracy_stats", function()
it("should clear all stats", function()
selector.report_feedback("ollama", true)
selector.report_feedback("copilot", true)
selector.reset_accuracy_stats()
local stats = selector.get_accuracy_stats()
assert.equals(0, stats.ollama.total)
assert.equals(0, stats.copilot.total)
end)
end)
end)
describe("agreement calculation", function()
-- Test the internal agreement calculation through pondering behavior
-- Since calculate_agreement is local, we test its effects indirectly
it("should detect high agreement for similar responses", function()
-- This is tested through the pondering system
-- When responses are similar, agreement should be high
local selector = require("codetyper.llm.selector")
-- Verify that should_ponder returns predictable results
-- for medium confidence (where pondering always happens)
assert.is_true(selector.should_ponder(0.5))
end)
end)
describe("provider selection with accuracy history", function()
local selector
before_each(function()
selector = require("codetyper.llm.selector")
selector.reset_accuracy_stats()
end)
it("should factor in historical accuracy for selection", function()
-- Simulate high Ollama accuracy
for _ = 1, 10 do
selector.report_feedback("ollama", true)
end
-- Even with no brain context, historical accuracy should influence confidence
local result = selector.select_provider("test", {})
-- Confidence should be higher due to historical accuracy
-- but provider might still be copilot if no memories
assert.is_number(result.confidence)
end)
it("should have lower confidence for low historical accuracy", function()
-- Simulate low Ollama accuracy
for _ = 1, 10 do
selector.report_feedback("ollama", false)
end
local result = selector.select_provider("test", {})
-- With bad history and no memories, should definitely use copilot
assert.equals("copilot", result.provider)
end)
end)

View File

@@ -1,118 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/llm/init.lua
describe("llm", function()
local llm = require("codetyper.llm")
describe("extract_code", function()
it("should extract code from markdown code block", function()
local response = [[
Here is the code:
```lua
function hello()
print("Hello!")
end
```
That should work.
]]
local code = llm.extract_code(response)
assert.is_true(code:match("function hello"))
assert.is_true(code:match('print%("Hello!"%)'))
assert.is_false(code:match("```"))
assert.is_false(code:match("Here is the code"))
end)
it("should extract code from generic code block", function()
local response = [[
```
const x = 1;
const y = 2;
```
]]
local code = llm.extract_code(response)
assert.is_true(code:match("const x = 1"))
end)
it("should handle multiple code blocks (return first)", function()
local response = [[
```javascript
const first = true;
```
```javascript
const second = true;
```
]]
local code = llm.extract_code(response)
assert.is_true(code:match("first"))
end)
it("should return original if no code blocks", function()
local response = "function test() return true end"
local code = llm.extract_code(response)
assert.equals(response, code)
end)
it("should handle empty code blocks", function()
local response = [[
```
```
]]
local code = llm.extract_code(response)
assert.equals("", vim.trim(code))
end)
it("should preserve indentation in extracted code", function()
local response = [[
```lua
function test()
if true then
print("nested")
end
end
```
]]
local code = llm.extract_code(response)
assert.is_true(code:match(" if true then"))
assert.is_true(code:match(" print"))
end)
end)
describe("get_client", function()
it("should return a client with generate function", function()
-- This test depends on config, but verifies interface
local client = llm.get_client()
assert.is_table(client)
assert.is_function(client.generate)
end)
end)
describe("build_system_prompt", function()
it("should include language context when provided", function()
local context = {
language = "typescript",
file_path = "/test/file.ts",
}
local prompt = llm.build_system_prompt(context)
assert.is_true(prompt:match("typescript") or prompt:match("TypeScript"))
end)
it("should work with minimal context", function()
local prompt = llm.build_system_prompt({})
assert.is_string(prompt)
assert.is_true(#prompt > 0)
end)
end)
end)

View File

@@ -1,280 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/logs.lua
describe("logs", function()
local logs
before_each(function()
-- Reset module state before each test
package.loaded["codetyper.agent.logs"] = nil
logs = require("codetyper.agent.logs")
end)
describe("log", function()
it("should add entry to log", function()
logs.log("info", "test message")
local entries = logs.get_entries()
assert.equals(1, #entries)
assert.equals("info", entries[1].level)
assert.equals("test message", entries[1].message)
end)
it("should include timestamp", function()
logs.log("info", "test")
local entries = logs.get_entries()
assert.is_truthy(entries[1].timestamp)
assert.is_true(entries[1].timestamp:match("%d+:%d+:%d+"))
end)
it("should include optional data", function()
logs.log("info", "test", { key = "value" })
local entries = logs.get_entries()
assert.equals("value", entries[1].data.key)
end)
end)
describe("info", function()
it("should log with info level", function()
logs.info("info message")
local entries = logs.get_entries()
assert.equals("info", entries[1].level)
end)
end)
describe("debug", function()
it("should log with debug level", function()
logs.debug("debug message")
local entries = logs.get_entries()
assert.equals("debug", entries[1].level)
end)
end)
describe("error", function()
it("should log with error level", function()
logs.error("error message")
local entries = logs.get_entries()
assert.equals("error", entries[1].level)
assert.is_true(entries[1].message:match("ERROR"))
end)
end)
describe("warning", function()
it("should log with warning level", function()
logs.warning("warning message")
local entries = logs.get_entries()
assert.equals("warning", entries[1].level)
assert.is_true(entries[1].message:match("WARN"))
end)
end)
describe("request", function()
it("should log API request", function()
logs.request("claude", "claude-sonnet-4", 1000)
local entries = logs.get_entries()
assert.equals("request", entries[1].level)
assert.is_true(entries[1].message:match("CLAUDE"))
assert.is_true(entries[1].message:match("claude%-sonnet%-4"))
end)
it("should store provider info", function()
logs.request("openai", "gpt-4")
local provider, model = logs.get_provider_info()
assert.equals("openai", provider)
assert.equals("gpt-4", model)
end)
end)
describe("response", function()
it("should log API response with token counts", function()
logs.response(500, 200, "end_turn")
local entries = logs.get_entries()
assert.equals("response", entries[1].level)
assert.is_true(entries[1].message:match("500"))
assert.is_true(entries[1].message:match("200"))
end)
it("should accumulate token totals", function()
logs.response(100, 50)
logs.response(200, 100)
local prompt_tokens, response_tokens = logs.get_token_totals()
assert.equals(300, prompt_tokens)
assert.equals(150, response_tokens)
end)
end)
describe("tool", function()
it("should log tool execution", function()
logs.tool("read_file", "start", "/path/to/file.lua")
local entries = logs.get_entries()
assert.equals("tool", entries[1].level)
assert.is_true(entries[1].message:match("read_file"))
end)
it("should show correct status icons", function()
logs.tool("write_file", "success", "file created")
local entries = logs.get_entries()
assert.is_true(entries[1].message:match("OK"))
logs.tool("bash", "error", "command failed")
entries = logs.get_entries()
assert.is_true(entries[2].message:match("ERR"))
end)
end)
describe("thinking", function()
it("should log thinking step", function()
logs.thinking("Analyzing code structure")
local entries = logs.get_entries()
assert.equals("debug", entries[1].level)
assert.is_true(entries[1].message:match("> Analyzing"))
end)
end)
describe("add", function()
it("should add entry using type field", function()
logs.add({ type = "info", message = "test message" })
local entries = logs.get_entries()
assert.equals(1, #entries)
assert.equals("info", entries[1].level)
end)
it("should handle clear type", function()
logs.info("test")
logs.add({ type = "clear" })
local entries = logs.get_entries()
assert.equals(0, #entries)
end)
end)
describe("listeners", function()
it("should notify listeners on new entries", function()
local received = {}
logs.add_listener(function(entry)
table.insert(received, entry)
end)
logs.info("test message")
assert.equals(1, #received)
assert.equals("info", received[1].level)
end)
it("should support multiple listeners", function()
local count = 0
logs.add_listener(function() count = count + 1 end)
logs.add_listener(function() count = count + 1 end)
logs.info("test")
assert.equals(2, count)
end)
it("should remove listener by ID", function()
local count = 0
local id = logs.add_listener(function() count = count + 1 end)
logs.info("test1")
logs.remove_listener(id)
logs.info("test2")
assert.equals(1, count)
end)
end)
describe("clear", function()
it("should clear all entries", function()
logs.info("test1")
logs.info("test2")
logs.clear()
assert.equals(0, #logs.get_entries())
end)
it("should reset token totals", function()
logs.response(100, 50)
logs.clear()
local prompt, response = logs.get_token_totals()
assert.equals(0, prompt)
assert.equals(0, response)
end)
it("should notify listeners of clear", function()
local cleared = false
logs.add_listener(function(entry)
if entry.level == "clear" then
cleared = true
end
end)
logs.clear()
assert.is_true(cleared)
end)
end)
describe("format_entry", function()
it("should format entry for display", function()
logs.info("test message")
local entry = logs.get_entries()[1]
local formatted = logs.format_entry(entry)
assert.is_true(formatted:match("%[%d+:%d+:%d+%]"))
assert.is_true(formatted:match("i")) -- info prefix
assert.is_true(formatted:match("test message"))
end)
it("should use correct level prefixes", function()
local prefixes = {
{ level = "info", prefix = "i" },
{ level = "debug", prefix = "%." },
{ level = "request", prefix = ">" },
{ level = "response", prefix = "<" },
{ level = "tool", prefix = "T" },
{ level = "error", prefix = "!" },
}
for _, test in ipairs(prefixes) do
local entry = {
timestamp = "12:00:00",
level = test.level,
message = "test",
}
local formatted = logs.format_entry(entry)
assert.is_true(formatted:match(test.prefix), "Missing prefix for " .. test.level)
end
end)
end)
describe("estimate_tokens", function()
it("should estimate tokens from text", function()
local text = "This is a test string for token estimation."
local tokens = logs.estimate_tokens(text)
-- Rough estimate: ~4 chars per token
assert.is_true(tokens > 0)
assert.is_true(tokens < #text) -- Should be less than character count
end)
it("should handle empty string", function()
local tokens = logs.estimate_tokens("")
assert.equals(0, tokens)
end)
end)
end)

View File

@@ -1,341 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/indexer/memory.lua
describe("indexer.memory", function()
local memory
local utils
-- Mock cwd for testing
local test_cwd = "/tmp/codetyper_test_memory"
before_each(function()
-- Reset modules
package.loaded["codetyper.indexer.memory"] = nil
package.loaded["codetyper.utils"] = nil
memory = require("codetyper.indexer.memory")
utils = require("codetyper.utils")
-- Create test directory structure
vim.fn.mkdir(test_cwd, "p")
vim.fn.mkdir(test_cwd .. "/.coder", "p")
vim.fn.mkdir(test_cwd .. "/.coder/memories", "p")
vim.fn.mkdir(test_cwd .. "/.coder/memories/files", "p")
vim.fn.mkdir(test_cwd .. "/.coder/sessions", "p")
-- Mock getcwd to return test directory
vim.fn.getcwd = function()
return test_cwd
end
-- Mock get_project_root
package.loaded["codetyper.utils"].get_project_root = function()
return test_cwd
end
end)
after_each(function()
-- Clean up test directory
vim.fn.delete(test_cwd, "rf")
end)
describe("store_memory", function()
it("should store a pattern memory", function()
local mem = {
type = "pattern",
content = "Use snake_case for function names",
weight = 0.8,
}
local result = memory.store_memory(mem)
assert.is_true(result)
end)
it("should store a convention memory", function()
local mem = {
type = "convention",
content = "Project uses TypeScript",
weight = 0.9,
}
local result = memory.store_memory(mem)
assert.is_true(result)
end)
it("should assign an ID to the memory", function()
local mem = {
type = "pattern",
content = "Test memory",
}
memory.store_memory(mem)
assert.is_truthy(mem.id)
assert.is_true(mem.id:match("^mem_") ~= nil)
end)
it("should set timestamps", function()
local mem = {
type = "pattern",
content = "Test memory",
}
memory.store_memory(mem)
assert.is_truthy(mem.created_at)
assert.is_truthy(mem.updated_at)
end)
end)
describe("load_patterns", function()
it("should return empty table when no patterns exist", function()
local patterns = memory.load_patterns()
assert.is_table(patterns)
end)
it("should load stored patterns", function()
-- Store a pattern first
memory.store_memory({
type = "pattern",
content = "Test pattern",
weight = 0.5,
})
-- Force reload
package.loaded["codetyper.indexer.memory"] = nil
memory = require("codetyper.indexer.memory")
local patterns = memory.load_patterns()
assert.is_table(patterns)
local count = 0
for _ in pairs(patterns) do
count = count + 1
end
assert.is_true(count >= 1)
end)
end)
describe("load_conventions", function()
it("should return empty table when no conventions exist", function()
local conventions = memory.load_conventions()
assert.is_table(conventions)
end)
end)
describe("store_file_memory", function()
it("should store file-specific memory", function()
local file_index = {
functions = {
{ name = "test_func", line = 10, end_line = 20 },
},
classes = {},
exports = {},
imports = {},
}
local result = memory.store_file_memory("src/main.lua", file_index)
assert.is_true(result)
end)
end)
describe("load_file_memory", function()
it("should return nil when file memory does not exist", function()
local result = memory.load_file_memory("nonexistent.lua")
assert.is_nil(result)
end)
it("should load stored file memory", function()
local file_index = {
functions = {
{ name = "my_function", line = 5, end_line = 15 },
},
classes = {},
exports = {},
imports = {},
}
memory.store_file_memory("src/test.lua", file_index)
local loaded = memory.load_file_memory("src/test.lua")
assert.is_table(loaded)
assert.equals("src/test.lua", loaded.path)
assert.equals(1, #loaded.functions)
assert.equals("my_function", loaded.functions[1].name)
end)
end)
describe("get_relevant", function()
it("should return empty table when no memories exist", function()
local results = memory.get_relevant("test query", 10)
assert.is_table(results)
assert.equals(0, #results)
end)
it("should find relevant memories by keyword", function()
memory.store_memory({
type = "pattern",
content = "Use TypeScript for type safety",
weight = 0.8,
})
memory.store_memory({
type = "pattern",
content = "Use Python for data processing",
weight = 0.7,
})
local results = memory.get_relevant("TypeScript", 10)
assert.is_true(#results >= 1)
-- First result should contain TypeScript
local found = false
for _, r in ipairs(results) do
if r.content:find("TypeScript") then
found = true
break
end
end
assert.is_true(found)
end)
it("should limit results", function()
-- Store multiple memories
for i = 1, 20 do
memory.store_memory({
type = "pattern",
content = "Pattern number " .. i .. " about testing",
weight = 0.5,
})
end
local results = memory.get_relevant("testing", 5)
assert.is_true(#results <= 5)
end)
end)
describe("update_usage", function()
it("should increment used_count", function()
local mem = {
type = "pattern",
content = "Test pattern for usage tracking",
weight = 0.5,
}
memory.store_memory(mem)
memory.update_usage(mem.id)
-- Reload and check
package.loaded["codetyper.indexer.memory"] = nil
memory = require("codetyper.indexer.memory")
local patterns = memory.load_patterns()
if patterns[mem.id] then
assert.equals(1, patterns[mem.id].used_count)
end
end)
end)
describe("get_all", function()
it("should return all memory types", function()
memory.store_memory({ type = "pattern", content = "A pattern" })
memory.store_memory({ type = "convention", content = "A convention" })
local all = memory.get_all()
assert.is_table(all.patterns)
assert.is_table(all.conventions)
assert.is_table(all.symbols)
end)
end)
describe("clear", function()
it("should clear all memories when no pattern provided", function()
memory.store_memory({ type = "pattern", content = "Pattern 1" })
memory.store_memory({ type = "convention", content = "Convention 1" })
memory.clear()
local all = memory.get_all()
assert.equals(0, vim.tbl_count(all.patterns))
assert.equals(0, vim.tbl_count(all.conventions))
end)
it("should clear only matching memories when pattern provided", function()
local mem1 = { type = "pattern", content = "Pattern 1" }
local mem2 = { type = "pattern", content = "Pattern 2" }
memory.store_memory(mem1)
memory.store_memory(mem2)
-- Clear memories matching the first ID
memory.clear(mem1.id)
local patterns = memory.load_patterns()
assert.is_nil(patterns[mem1.id])
end)
end)
describe("prune", function()
it("should remove low-weight unused memories", function()
-- Store some low-weight memories
memory.store_memory({
type = "pattern",
content = "Low weight pattern",
weight = 0.05,
used_count = 0,
})
memory.store_memory({
type = "pattern",
content = "High weight pattern",
weight = 0.9,
used_count = 0,
})
local pruned = memory.prune(0.1)
-- Should have pruned at least one
assert.is_true(pruned >= 0)
end)
it("should not remove frequently used memories", function()
local mem = {
type = "pattern",
content = "Frequently used but low weight",
weight = 0.05,
used_count = 10,
}
memory.store_memory(mem)
memory.prune(0.1)
-- Memory should still exist because used_count > 0
local patterns = memory.load_patterns()
-- Note: prune only removes if used_count == 0 AND weight < threshold
if patterns[mem.id] then
assert.is_truthy(patterns[mem.id])
end
end)
end)
describe("get_stats", function()
it("should return memory statistics", function()
memory.store_memory({ type = "pattern", content = "P1" })
memory.store_memory({ type = "pattern", content = "P2" })
memory.store_memory({ type = "convention", content = "C1" })
local stats = memory.get_stats()
assert.is_table(stats)
assert.equals(2, stats.patterns)
assert.equals(1, stats.conventions)
assert.equals(3, stats.total)
end)
end)
end)

View File

@@ -1,207 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/parser.lua
describe("parser", function()
local parser = require("codetyper.parser")
describe("find_prompts", function()
it("should find single-line prompt", function()
local content = "/@ create a function @/"
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(1, #prompts)
assert.equals(" create a function ", prompts[1].content)
assert.equals(1, prompts[1].start_line)
assert.equals(1, prompts[1].end_line)
end)
it("should find multi-line prompt", function()
local content = [[
/@ create a function
that validates email
addresses @/
]]
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(1, #prompts)
assert.is_true(prompts[1].content:match("validates email"))
assert.equals(2, prompts[1].start_line)
assert.equals(4, prompts[1].end_line)
end)
it("should find multiple prompts", function()
local content = [[
/@ first prompt @/
some code here
/@ second prompt @/
more code
/@ third prompt
multiline @/
]]
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(3, #prompts)
assert.equals(" first prompt ", prompts[1].content)
assert.equals(" second prompt ", prompts[2].content)
assert.is_true(prompts[3].content:match("third prompt"))
end)
it("should return empty table when no prompts found", function()
local content = "just some regular code\nno prompts here"
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(0, #prompts)
end)
it("should handle prompts with special characters", function()
local content = "/@ add (function) with [brackets] @/"
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(1, #prompts)
assert.is_true(prompts[1].content:match("function"))
assert.is_true(prompts[1].content:match("brackets"))
end)
it("should handle empty prompt content", function()
local content = "/@ @/"
local prompts = parser.find_prompts(content, "/@", "@/")
assert.equals(1, #prompts)
assert.equals(" ", prompts[1].content)
end)
it("should handle custom tags", function()
local content = "<!-- prompt: create button -->"
local prompts = parser.find_prompts(content, "<!-- prompt:", "-->")
assert.equals(1, #prompts)
assert.is_true(prompts[1].content:match("create button"))
end)
end)
describe("detect_prompt_type", function()
it("should detect refactor type", function()
assert.equals("refactor", parser.detect_prompt_type("refactor this code"))
assert.equals("refactor", parser.detect_prompt_type("REFACTOR the function"))
end)
it("should detect add type", function()
assert.equals("add", parser.detect_prompt_type("add a new function"))
assert.equals("add", parser.detect_prompt_type("create a component"))
assert.equals("add", parser.detect_prompt_type("implement sorting algorithm"))
end)
it("should detect document type", function()
assert.equals("document", parser.detect_prompt_type("document this function"))
assert.equals("document", parser.detect_prompt_type("add jsdoc comments"))
assert.equals("document", parser.detect_prompt_type("comment the code"))
end)
it("should detect explain type", function()
assert.equals("explain", parser.detect_prompt_type("explain this code"))
assert.equals("explain", parser.detect_prompt_type("what does this do"))
assert.equals("explain", parser.detect_prompt_type("how does this work"))
end)
it("should return generic for unknown types", function()
assert.equals("generic", parser.detect_prompt_type("do something"))
assert.equals("generic", parser.detect_prompt_type("make it better"))
end)
end)
describe("clean_prompt", function()
it("should trim whitespace", function()
assert.equals("hello", parser.clean_prompt(" hello "))
assert.equals("hello", parser.clean_prompt("\n\nhello\n\n"))
end)
it("should normalize multiple newlines", function()
local input = "line1\n\n\n\nline2"
local expected = "line1\n\nline2"
assert.equals(expected, parser.clean_prompt(input))
end)
it("should preserve single newlines", function()
local input = "line1\nline2\nline3"
assert.equals(input, parser.clean_prompt(input))
end)
end)
describe("has_closing_tag", function()
it("should return true when closing tag exists", function()
assert.is_true(parser.has_closing_tag("some text @/", "@/"))
assert.is_true(parser.has_closing_tag("@/", "@/"))
end)
it("should return false when closing tag missing", function()
assert.is_false(parser.has_closing_tag("some text", "@/"))
assert.is_false(parser.has_closing_tag("", "@/"))
end)
end)
describe("extract_file_references", function()
it("should extract single file reference", function()
local files = parser.extract_file_references("fix this @utils.ts")
assert.equals(1, #files)
assert.equals("utils.ts", files[1])
end)
it("should extract multiple file references", function()
local files = parser.extract_file_references("use @config.ts and @helpers.lua")
assert.equals(2, #files)
assert.equals("config.ts", files[1])
assert.equals("helpers.lua", files[2])
end)
it("should extract file paths with directories", function()
local files = parser.extract_file_references("check @src/utils/helpers.ts")
assert.equals(1, #files)
assert.equals("src/utils/helpers.ts", files[1])
end)
it("should NOT extract closing tag @/", function()
local files = parser.extract_file_references("fix this @/")
assert.equals(0, #files)
end)
it("should handle mixed content with closing tag", function()
local files = parser.extract_file_references("use @config.ts to fix @/")
assert.equals(1, #files)
assert.equals("config.ts", files[1])
end)
it("should return empty table when no file refs", function()
local files = parser.extract_file_references("just some text")
assert.equals(0, #files)
end)
it("should handle relative paths", function()
local files = parser.extract_file_references("check @../config.json")
assert.equals(1, #files)
assert.equals("../config.json", files[1])
end)
end)
describe("strip_file_references", function()
it("should remove single file reference", function()
local result = parser.strip_file_references("fix this @utils.ts please")
assert.equals("fix this please", result)
end)
it("should remove multiple file references", function()
local result = parser.strip_file_references("use @config.ts and @helpers.lua")
assert.equals("use and ", result)
end)
it("should NOT remove closing tag", function()
local result = parser.strip_file_references("fix this @/")
-- @/ should remain since it's the closing tag pattern
assert.is_true(result:find("@/") ~= nil)
end)
it("should handle paths with directories", function()
local result = parser.strip_file_references("check @src/utils.ts here")
assert.equals("check here", result)
end)
end)
end)

View File

@@ -1,371 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/patch.lua
describe("patch", function()
local patch
before_each(function()
-- Reset module state before each test
package.loaded["codetyper.agent.patch"] = nil
patch = require("codetyper.agent.patch")
end)
describe("generate_id", function()
it("should generate unique IDs", function()
local id1 = patch.generate_id()
local id2 = patch.generate_id()
assert.is_not.equals(id1, id2)
assert.is_truthy(id1:match("^patch_"))
end)
end)
describe("snapshot_buffer", function()
local test_buf
before_each(function()
test_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(test_buf, 0, -1, false, {
"line 1",
"line 2",
"line 3",
"line 4",
"line 5",
})
end)
after_each(function()
if vim.api.nvim_buf_is_valid(test_buf) then
vim.api.nvim_buf_delete(test_buf, { force = true })
end
end)
it("should capture changedtick", function()
local snapshot = patch.snapshot_buffer(test_buf)
assert.is_number(snapshot.changedtick)
end)
it("should capture content hash", function()
local snapshot = patch.snapshot_buffer(test_buf)
assert.is_string(snapshot.content_hash)
assert.is_true(#snapshot.content_hash > 0)
end)
it("should snapshot specific range", function()
local snapshot = patch.snapshot_buffer(test_buf, { start_line = 2, end_line = 4 })
assert.equals(test_buf, snapshot.bufnr)
assert.is_truthy(snapshot.range)
assert.equals(2, snapshot.range.start_line)
assert.equals(4, snapshot.range.end_line)
end)
end)
describe("is_snapshot_stale", function()
local test_buf
before_each(function()
test_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(test_buf, 0, -1, false, {
"original content",
"line 2",
})
end)
after_each(function()
if vim.api.nvim_buf_is_valid(test_buf) then
vim.api.nvim_buf_delete(test_buf, { force = true })
end
end)
it("should return false for unchanged buffer", function()
local snapshot = patch.snapshot_buffer(test_buf)
local is_stale, reason = patch.is_snapshot_stale(snapshot)
assert.is_false(is_stale)
assert.is_nil(reason)
end)
it("should return true when content changes", function()
local snapshot = patch.snapshot_buffer(test_buf)
-- Modify buffer
vim.api.nvim_buf_set_lines(test_buf, 0, 1, false, { "modified content" })
local is_stale, reason = patch.is_snapshot_stale(snapshot)
assert.is_true(is_stale)
assert.equals("content_changed", reason)
end)
it("should return true for invalid buffer", function()
local snapshot = patch.snapshot_buffer(test_buf)
-- Delete buffer
vim.api.nvim_buf_delete(test_buf, { force = true })
local is_stale, reason = patch.is_snapshot_stale(snapshot)
assert.is_true(is_stale)
assert.equals("buffer_invalid", reason)
end)
end)
describe("queue_patch", function()
it("should add patch to queue", function()
local p = {
event_id = "test_event",
target_bufnr = 1,
target_path = "/test/file.lua",
original_snapshot = {
bufnr = 1,
changedtick = 0,
content_hash = "abc123",
},
generated_code = "function test() end",
confidence = 0.9,
}
local queued = patch.queue_patch(p)
assert.is_truthy(queued.id)
assert.equals("pending", queued.status)
local pending = patch.get_pending()
assert.equals(1, #pending)
end)
it("should set default status", function()
local p = {
event_id = "test",
generated_code = "code",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
}
local queued = patch.queue_patch(p)
assert.equals("pending", queued.status)
end)
end)
describe("get", function()
it("should return patch by ID", function()
local p = patch.queue_patch({
event_id = "test",
generated_code = "code",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
local found = patch.get(p.id)
assert.is_not_nil(found)
assert.equals(p.id, found.id)
end)
it("should return nil for unknown ID", function()
local found = patch.get("unknown_id")
assert.is_nil(found)
end)
end)
describe("mark_applied", function()
it("should mark patch as applied", function()
local p = patch.queue_patch({
event_id = "test",
generated_code = "code",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
local success = patch.mark_applied(p.id)
assert.is_true(success)
assert.equals("applied", patch.get(p.id).status)
assert.is_truthy(patch.get(p.id).applied_at)
end)
end)
describe("mark_stale", function()
it("should mark patch as stale with reason", function()
local p = patch.queue_patch({
event_id = "test",
generated_code = "code",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
local success = patch.mark_stale(p.id, "content_changed")
assert.is_true(success)
assert.equals("stale", patch.get(p.id).status)
assert.equals("content_changed", patch.get(p.id).stale_reason)
end)
end)
describe("stats", function()
it("should return correct statistics", function()
local p1 = patch.queue_patch({
event_id = "test1",
generated_code = "code1",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
patch.queue_patch({
event_id = "test2",
generated_code = "code2",
confidence = 0.9,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "y" },
})
patch.mark_applied(p1.id)
local stats = patch.stats()
assert.equals(2, stats.total)
assert.equals(1, stats.pending)
assert.equals(1, stats.applied)
end)
end)
describe("get_for_event", function()
it("should return patches for specific event", function()
patch.queue_patch({
event_id = "event_a",
generated_code = "code1",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
patch.queue_patch({
event_id = "event_b",
generated_code = "code2",
confidence = 0.9,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "y" },
})
patch.queue_patch({
event_id = "event_a",
generated_code = "code3",
confidence = 0.7,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "z" },
})
local event_a_patches = patch.get_for_event("event_a")
assert.equals(2, #event_a_patches)
end)
end)
describe("clear", function()
it("should clear all patches", function()
patch.queue_patch({
event_id = "test",
generated_code = "code",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
patch.clear()
assert.equals(0, #patch.get_pending())
assert.equals(0, patch.stats().total)
end)
end)
describe("cancel_for_buffer", function()
it("should cancel patches for specific buffer", function()
patch.queue_patch({
event_id = "test1",
target_bufnr = 1,
generated_code = "code1",
confidence = 0.8,
original_snapshot = { bufnr = 1, changedtick = 0, content_hash = "x" },
})
patch.queue_patch({
event_id = "test2",
target_bufnr = 2,
generated_code = "code2",
confidence = 0.9,
original_snapshot = { bufnr = 2, changedtick = 0, content_hash = "y" },
})
local cancelled = patch.cancel_for_buffer(1)
assert.equals(1, cancelled)
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)

View File

@@ -1,276 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/preferences.lua
-- Note: UI tests (floating window) are skipped per testing guidelines
describe("preferences", function()
local preferences
local utils
-- Mock cwd for testing
local test_cwd = "/tmp/codetyper_test_prefs"
before_each(function()
-- Reset modules
package.loaded["codetyper.preferences"] = nil
package.loaded["codetyper.utils"] = nil
preferences = require("codetyper.config.preferences")
utils = require("codetyper.utils")
-- Clear cache before each test
preferences.clear_cache()
-- Create test directory
vim.fn.mkdir(test_cwd, "p")
vim.fn.mkdir(test_cwd .. "/.coder", "p")
-- Mock getcwd to return test directory
vim.fn.getcwd = function()
return test_cwd
end
end)
after_each(function()
-- Clean up test directory
vim.fn.delete(test_cwd, "rf")
end)
describe("load", function()
it("should return defaults when no preferences file exists", function()
local prefs = preferences.load()
assert.is_table(prefs)
assert.is_nil(prefs.auto_process)
assert.is_false(prefs.asked_auto_process)
end)
it("should load preferences from file", function()
-- Create preferences file
local path = test_cwd .. "/.coder/preferences.json"
utils.write_file(path, '{"auto_process":true,"asked_auto_process":true}')
local prefs = preferences.load()
assert.is_true(prefs.auto_process)
assert.is_true(prefs.asked_auto_process)
end)
it("should merge file preferences with defaults", function()
-- Create partial preferences file
local path = test_cwd .. "/.coder/preferences.json"
utils.write_file(path, '{"auto_process":false}')
local prefs = preferences.load()
assert.is_false(prefs.auto_process)
-- Default for asked_auto_process should be preserved
assert.is_false(prefs.asked_auto_process)
end)
it("should cache preferences", function()
local prefs1 = preferences.load()
prefs1.test_value = "cached"
-- Load again - should get cached version
local prefs2 = preferences.load()
assert.equals("cached", prefs2.test_value)
end)
it("should handle invalid JSON gracefully", function()
local path = test_cwd .. "/.coder/preferences.json"
utils.write_file(path, "not valid json {{{")
local prefs = preferences.load()
-- Should return defaults
assert.is_table(prefs)
assert.is_nil(prefs.auto_process)
end)
end)
describe("save", function()
it("should save preferences to file", function()
local prefs = {
auto_process = true,
asked_auto_process = true,
}
preferences.save(prefs)
-- Verify file was created
local path = test_cwd .. "/.coder/preferences.json"
local content = utils.read_file(path)
assert.is_truthy(content)
local decoded = vim.json.decode(content)
assert.is_true(decoded.auto_process)
assert.is_true(decoded.asked_auto_process)
end)
it("should update cache after save", function()
local prefs = {
auto_process = true,
asked_auto_process = true,
}
preferences.save(prefs)
-- Load should return the saved values from cache
local loaded = preferences.load()
assert.is_true(loaded.auto_process)
end)
it("should create .coder directory if it does not exist", function()
-- Remove .coder directory
vim.fn.delete(test_cwd .. "/.coder", "rf")
local prefs = { auto_process = false }
preferences.save(prefs)
-- Directory should be created
assert.equals(1, vim.fn.isdirectory(test_cwd .. "/.coder"))
end)
end)
describe("get", function()
it("should get a specific preference value", function()
local path = test_cwd .. "/.coder/preferences.json"
utils.write_file(path, '{"auto_process":true}')
local value = preferences.get("auto_process")
assert.is_true(value)
end)
it("should return nil for non-existent key", function()
local value = preferences.get("non_existent_key")
assert.is_nil(value)
end)
end)
describe("set", function()
it("should set a specific preference value", function()
preferences.set("auto_process", true)
local value = preferences.get("auto_process")
assert.is_true(value)
end)
it("should persist the value to file", function()
preferences.set("auto_process", false)
-- Clear cache and reload
preferences.clear_cache()
local value = preferences.get("auto_process")
assert.is_false(value)
end)
end)
describe("is_auto_process_enabled", function()
it("should return nil when not set", function()
local result = preferences.is_auto_process_enabled()
assert.is_nil(result)
end)
it("should return true when enabled", function()
preferences.set("auto_process", true)
local result = preferences.is_auto_process_enabled()
assert.is_true(result)
end)
it("should return false when disabled", function()
preferences.set("auto_process", false)
local result = preferences.is_auto_process_enabled()
assert.is_false(result)
end)
end)
describe("set_auto_process", function()
it("should set auto_process to true", function()
preferences.set_auto_process(true)
assert.is_true(preferences.is_auto_process_enabled())
assert.is_true(preferences.has_asked_auto_process())
end)
it("should set auto_process to false", function()
preferences.set_auto_process(false)
assert.is_false(preferences.is_auto_process_enabled())
assert.is_true(preferences.has_asked_auto_process())
end)
it("should also set asked_auto_process to true", function()
preferences.set_auto_process(true)
assert.is_true(preferences.has_asked_auto_process())
end)
end)
describe("has_asked_auto_process", function()
it("should return false when not asked", function()
local result = preferences.has_asked_auto_process()
assert.is_false(result)
end)
it("should return true after setting auto_process", function()
preferences.set_auto_process(true)
local result = preferences.has_asked_auto_process()
assert.is_true(result)
end)
end)
describe("clear_cache", function()
it("should clear cached preferences", function()
-- Load to populate cache
local prefs = preferences.load()
prefs.test_marker = "before_clear"
-- Clear cache
preferences.clear_cache()
-- Load again - should not have the marker
local prefs_after = preferences.load()
assert.is_nil(prefs_after.test_marker)
end)
end)
describe("toggle_auto_process", function()
it("should toggle from nil to true", function()
-- Initially nil
assert.is_nil(preferences.is_auto_process_enabled())
preferences.toggle_auto_process()
-- Should be true (not nil becomes true)
assert.is_true(preferences.is_auto_process_enabled())
end)
it("should toggle from true to false", function()
preferences.set_auto_process(true)
preferences.toggle_auto_process()
assert.is_false(preferences.is_auto_process_enabled())
end)
it("should toggle from false to true", function()
preferences.set_auto_process(false)
preferences.toggle_auto_process()
assert.is_true(preferences.is_auto_process_enabled())
end)
end)
end)

View File

@@ -1,332 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/queue.lua
describe("queue", function()
local queue
before_each(function()
-- Reset module state before each test
package.loaded["codetyper.agent.queue"] = nil
queue = require("codetyper.agent.queue")
end)
describe("generate_id", function()
it("should generate unique IDs", function()
local id1 = queue.generate_id()
local id2 = queue.generate_id()
assert.is_not.equals(id1, id2)
assert.is_true(id1:match("^evt_"))
assert.is_true(id2:match("^evt_"))
end)
end)
describe("hash_content", function()
it("should generate consistent hashes", function()
local content = "test content"
local hash1 = queue.hash_content(content)
local hash2 = queue.hash_content(content)
assert.equals(hash1, hash2)
end)
it("should generate different hashes for different content", function()
local hash1 = queue.hash_content("content A")
local hash2 = queue.hash_content("content B")
assert.is_not.equals(hash1, hash2)
end)
end)
describe("enqueue", function()
it("should add event to queue", function()
local event = {
bufnr = 1,
prompt_content = "test prompt",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
}
local enqueued = queue.enqueue(event)
assert.is_not_nil(enqueued.id)
assert.equals("pending", enqueued.status)
assert.equals(1, queue.size())
end)
it("should set default priority to 2", function()
local event = {
bufnr = 1,
prompt_content = "test prompt",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
}
local enqueued = queue.enqueue(event)
assert.equals(2, enqueued.priority)
end)
it("should maintain priority order", function()
queue.enqueue({
bufnr = 1,
prompt_content = "low priority",
target_path = "/test/file.lua",
priority = 3,
range = { start_line = 1, end_line = 1 },
})
queue.enqueue({
bufnr = 1,
prompt_content = "high priority",
target_path = "/test/file.lua",
priority = 1,
range = { start_line = 1, end_line = 1 },
})
local first = queue.dequeue()
assert.equals("high priority", first.prompt_content)
end)
it("should generate content hash automatically", function()
local event = {
bufnr = 1,
prompt_content = "test prompt",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
}
local enqueued = queue.enqueue(event)
assert.is_not_nil(enqueued.content_hash)
end)
end)
describe("dequeue", function()
it("should return nil when queue is empty", function()
local event = queue.dequeue()
assert.is_nil(event)
end)
it("should return and mark event as processing", function()
queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
local event = queue.dequeue()
assert.is_not_nil(event)
assert.equals("processing", event.status)
end)
it("should skip non-pending events", function()
local evt1 = queue.enqueue({
bufnr = 1,
prompt_content = "first",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.enqueue({
bufnr = 1,
prompt_content = "second",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
-- Mark first as completed
queue.complete(evt1.id)
local event = queue.dequeue()
assert.equals("second", event.prompt_content)
end)
end)
describe("peek", function()
it("should return next pending without removing", function()
queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
local event1 = queue.peek()
local event2 = queue.peek()
assert.is_not_nil(event1)
assert.equals(event1.id, event2.id)
assert.equals("pending", event1.status)
end)
end)
describe("get", function()
it("should return event by ID", function()
local enqueued = queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
local event = queue.get(enqueued.id)
assert.is_not_nil(event)
assert.equals(enqueued.id, event.id)
end)
it("should return nil for unknown ID", function()
local event = queue.get("unknown_id")
assert.is_nil(event)
end)
end)
describe("update_status", function()
it("should update event status", function()
local enqueued = queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
local success = queue.update_status(enqueued.id, "completed")
assert.is_true(success)
assert.equals("completed", queue.get(enqueued.id).status)
end)
it("should return false for unknown ID", function()
local success = queue.update_status("unknown_id", "completed")
assert.is_false(success)
end)
it("should merge extra fields", function()
local enqueued = queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.update_status(enqueued.id, "completed", { error = "test error" })
local event = queue.get(enqueued.id)
assert.equals("test error", event.error)
end)
end)
describe("cancel_for_buffer", function()
it("should cancel all pending events for buffer", function()
queue.enqueue({
bufnr = 1,
prompt_content = "buffer 1 - first",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.enqueue({
bufnr = 1,
prompt_content = "buffer 1 - second",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.enqueue({
bufnr = 2,
prompt_content = "buffer 2",
target_path = "/test/file2.lua",
range = { start_line = 1, end_line = 1 },
})
local cancelled = queue.cancel_for_buffer(1)
assert.equals(2, cancelled)
assert.equals(1, queue.pending_count())
end)
end)
describe("stats", function()
it("should return correct statistics", function()
queue.enqueue({
bufnr = 1,
prompt_content = "pending",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
local evt = queue.enqueue({
bufnr = 1,
prompt_content = "to complete",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.complete(evt.id)
local stats = queue.stats()
assert.equals(2, stats.total)
assert.equals(1, stats.pending)
assert.equals(1, stats.completed)
end)
end)
describe("clear", function()
it("should clear all events", function()
queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.clear()
assert.equals(0, queue.size())
end)
it("should clear only specified status", function()
local evt = queue.enqueue({
bufnr = 1,
prompt_content = "to complete",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.complete(evt.id)
queue.enqueue({
bufnr = 1,
prompt_content = "pending",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
queue.clear("completed")
assert.equals(1, queue.size())
assert.equals(1, queue.pending_count())
end)
end)
describe("listeners", function()
it("should notify listeners on enqueue", function()
local notifications = {}
queue.add_listener(function(event_type, event, size)
table.insert(notifications, { type = event_type, event = event, size = size })
end)
queue.enqueue({
bufnr = 1,
prompt_content = "test",
target_path = "/test/file.lua",
range = { start_line = 1, end_line = 1 },
})
assert.equals(1, #notifications)
assert.equals("enqueue", notifications[1].type)
end)
end)
end)

View File

@@ -1,285 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/indexer/scanner.lua
describe("indexer.scanner", function()
local scanner
local utils
-- Mock cwd for testing
local test_cwd = "/tmp/codetyper_test_scanner"
before_each(function()
-- Reset modules
package.loaded["codetyper.indexer.scanner"] = nil
package.loaded["codetyper.utils"] = nil
scanner = require("codetyper.indexer.scanner")
utils = require("codetyper.utils")
-- Create test directory
vim.fn.mkdir(test_cwd, "p")
-- Mock getcwd to return test directory
vim.fn.getcwd = function()
return test_cwd
end
end)
after_each(function()
-- Clean up test directory
vim.fn.delete(test_cwd, "rf")
end)
describe("detect_project_type", function()
it("should detect node project from package.json", function()
utils.write_file(test_cwd .. "/package.json", '{"name":"test"}')
local project_type = scanner.detect_project_type(test_cwd)
assert.equals("node", project_type)
end)
it("should detect rust project from Cargo.toml", function()
utils.write_file(test_cwd .. "/Cargo.toml", '[package]\nname = "test"')
local project_type = scanner.detect_project_type(test_cwd)
assert.equals("rust", project_type)
end)
it("should detect go project from go.mod", function()
utils.write_file(test_cwd .. "/go.mod", "module example.com/test")
local project_type = scanner.detect_project_type(test_cwd)
assert.equals("go", project_type)
end)
it("should detect python project from pyproject.toml", function()
utils.write_file(test_cwd .. "/pyproject.toml", '[project]\nname = "test"')
local project_type = scanner.detect_project_type(test_cwd)
assert.equals("python", project_type)
end)
it("should return unknown for unrecognized project", function()
-- Empty directory
local project_type = scanner.detect_project_type(test_cwd)
assert.equals("unknown", project_type)
end)
end)
describe("parse_package_json", function()
it("should parse dependencies from package.json", function()
local pkg_content = [[{
"name": "test",
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.0"
},
"devDependencies": {
"jest": "^29.0.0"
}
}]]
utils.write_file(test_cwd .. "/package.json", pkg_content)
local result = scanner.parse_package_json(test_cwd)
assert.is_table(result.dependencies)
assert.is_table(result.dev_dependencies)
assert.equals("^4.18.0", result.dependencies.express)
assert.equals("^4.17.0", result.dependencies.lodash)
assert.equals("^29.0.0", result.dev_dependencies.jest)
end)
it("should return empty tables when package.json does not exist", function()
local result = scanner.parse_package_json(test_cwd)
assert.is_table(result.dependencies)
assert.is_table(result.dev_dependencies)
assert.equals(0, vim.tbl_count(result.dependencies))
end)
it("should handle malformed JSON gracefully", function()
utils.write_file(test_cwd .. "/package.json", "not valid json")
local result = scanner.parse_package_json(test_cwd)
assert.is_table(result.dependencies)
assert.equals(0, vim.tbl_count(result.dependencies))
end)
end)
describe("parse_cargo_toml", function()
it("should parse dependencies from Cargo.toml", function()
local cargo_content = [[
[package]
name = "test"
[dependencies]
serde = "1.0"
tokio = "1.28"
[dev-dependencies]
tempfile = "3.5"
]]
utils.write_file(test_cwd .. "/Cargo.toml", cargo_content)
local result = scanner.parse_cargo_toml(test_cwd)
assert.is_table(result.dependencies)
assert.equals("1.0", result.dependencies.serde)
assert.equals("1.28", result.dependencies.tokio)
assert.equals("3.5", result.dev_dependencies.tempfile)
end)
it("should return empty tables when Cargo.toml does not exist", function()
local result = scanner.parse_cargo_toml(test_cwd)
assert.equals(0, vim.tbl_count(result.dependencies))
end)
end)
describe("parse_go_mod", function()
it("should parse dependencies from go.mod", function()
local go_mod_content = [[
module example.com/test
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)
]]
utils.write_file(test_cwd .. "/go.mod", go_mod_content)
local result = scanner.parse_go_mod(test_cwd)
assert.is_table(result.dependencies)
assert.equals("v1.9.1", result.dependencies["github.com/gin-gonic/gin"])
assert.equals("v1.8.4", result.dependencies["github.com/stretchr/testify"])
end)
end)
describe("should_ignore", function()
it("should ignore hidden files", function()
local config = { excluded_dirs = {} }
assert.is_true(scanner.should_ignore(".hidden", config))
assert.is_true(scanner.should_ignore(".git", config))
end)
it("should ignore node_modules", function()
local config = { excluded_dirs = {} }
assert.is_true(scanner.should_ignore("node_modules", config))
end)
it("should ignore configured directories", function()
local config = { excluded_dirs = { "custom_ignore" } }
assert.is_true(scanner.should_ignore("custom_ignore", config))
end)
it("should not ignore regular files", function()
local config = { excluded_dirs = {} }
assert.is_false(scanner.should_ignore("main.lua", config))
assert.is_false(scanner.should_ignore("src", config))
end)
end)
describe("should_index", function()
it("should index files with allowed extensions", function()
vim.fn.mkdir(test_cwd .. "/src", "p")
utils.write_file(test_cwd .. "/src/main.lua", "-- test")
local config = {
index_extensions = { "lua", "ts", "js" },
max_file_size = 100000,
excluded_dirs = {},
}
assert.is_true(scanner.should_index(test_cwd .. "/src/main.lua", config))
end)
it("should not index coder files", function()
utils.write_file(test_cwd .. "/main.coder.lua", "-- test")
local config = {
index_extensions = { "lua" },
max_file_size = 100000,
excluded_dirs = {},
}
assert.is_false(scanner.should_index(test_cwd .. "/main.coder.lua", config))
end)
it("should not index files with disallowed extensions", function()
utils.write_file(test_cwd .. "/image.png", "binary")
local config = {
index_extensions = { "lua", "ts", "js" },
max_file_size = 100000,
excluded_dirs = {},
}
assert.is_false(scanner.should_index(test_cwd .. "/image.png", config))
end)
end)
describe("get_indexable_files", function()
it("should return list of indexable files", function()
vim.fn.mkdir(test_cwd .. "/src", "p")
utils.write_file(test_cwd .. "/src/main.lua", "-- main")
utils.write_file(test_cwd .. "/src/utils.lua", "-- utils")
utils.write_file(test_cwd .. "/README.md", "# Readme")
local config = {
index_extensions = { "lua" },
max_file_size = 100000,
excluded_dirs = { "node_modules" },
}
local files = scanner.get_indexable_files(test_cwd, config)
assert.equals(2, #files)
end)
it("should skip ignored directories", function()
vim.fn.mkdir(test_cwd .. "/src", "p")
vim.fn.mkdir(test_cwd .. "/node_modules", "p")
utils.write_file(test_cwd .. "/src/main.lua", "-- main")
utils.write_file(test_cwd .. "/node_modules/package.lua", "-- ignore")
local config = {
index_extensions = { "lua" },
max_file_size = 100000,
excluded_dirs = { "node_modules" },
}
local files = scanner.get_indexable_files(test_cwd, config)
-- Should only include src/main.lua
assert.equals(1, #files)
end)
end)
describe("get_language", function()
it("should return correct language for extensions", function()
assert.equals("lua", scanner.get_language("test.lua"))
assert.equals("typescript", scanner.get_language("test.ts"))
assert.equals("javascript", scanner.get_language("test.js"))
assert.equals("python", scanner.get_language("test.py"))
assert.equals("go", scanner.get_language("test.go"))
assert.equals("rust", scanner.get_language("test.rs"))
end)
it("should return extension as fallback", function()
assert.equals("unknown", scanner.get_language("test.unknown"))
end)
end)
end)

View File

@@ -1,139 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/utils.lua
describe("utils", function()
local utils = require("codetyper.utils")
describe("is_coder_file", function()
it("should return true for coder files", function()
assert.is_true(utils.is_coder_file("index.coder.ts"))
assert.is_true(utils.is_coder_file("main.coder.lua"))
assert.is_true(utils.is_coder_file("/path/to/file.coder.py"))
end)
it("should return false for regular files", function()
assert.is_false(utils.is_coder_file("index.ts"))
assert.is_false(utils.is_coder_file("main.lua"))
assert.is_false(utils.is_coder_file("coder.ts"))
end)
end)
describe("get_target_path", function()
it("should convert coder path to target path", function()
assert.equals("index.ts", utils.get_target_path("index.coder.ts"))
assert.equals("main.lua", utils.get_target_path("main.coder.lua"))
assert.equals("/path/to/file.py", utils.get_target_path("/path/to/file.coder.py"))
end)
end)
describe("get_coder_path", function()
it("should convert target path to coder path", function()
assert.equals("index.coder.ts", utils.get_coder_path("index.ts"))
assert.equals("main.coder.lua", utils.get_coder_path("main.lua"))
end)
it("should preserve directory path", function()
local result = utils.get_coder_path("/path/to/file.py")
assert.is_truthy(result:match("/path/to/"))
assert.is_truthy(result:match("file%.coder%.py"))
end)
end)
describe("escape_pattern", function()
it("should escape special pattern characters", function()
-- Note: @ is NOT a special Lua pattern character
-- Special chars are: ( ) . % + - * ? [ ] ^ $
assert.equals("/@", utils.escape_pattern("/@"))
assert.equals("@/", utils.escape_pattern("@/"))
assert.equals("hello%.world", utils.escape_pattern("hello.world"))
assert.equals("test%+pattern", utils.escape_pattern("test+pattern"))
end)
it("should handle multiple special characters", function()
local input = "(test)[pattern]"
local escaped = utils.escape_pattern(input)
-- Use string.find with plain=true to avoid pattern interpretation
assert.is_truthy(string.find(escaped, "%(", 1, true))
assert.is_truthy(string.find(escaped, "%)", 1, true))
assert.is_truthy(string.find(escaped, "%[", 1, true))
assert.is_truthy(string.find(escaped, "%]", 1, true))
end)
end)
describe("file operations", function()
local test_dir
local test_file
before_each(function()
test_dir = vim.fn.tempname()
utils.ensure_dir(test_dir)
test_file = test_dir .. "/test.txt"
end)
after_each(function()
vim.fn.delete(test_dir, "rf")
end)
describe("ensure_dir", function()
it("should create directory", function()
local new_dir = test_dir .. "/subdir"
local result = utils.ensure_dir(new_dir)
assert.is_true(result)
assert.equals(1, vim.fn.isdirectory(new_dir))
end)
it("should return true for existing directory", function()
local result = utils.ensure_dir(test_dir)
assert.is_true(result)
end)
end)
describe("write_file", function()
it("should write content to file", function()
local result = utils.write_file(test_file, "test content")
assert.is_true(result)
assert.is_true(utils.file_exists(test_file))
end)
end)
describe("read_file", function()
it("should read file content", function()
utils.write_file(test_file, "test content")
local content = utils.read_file(test_file)
assert.equals("test content", content)
end)
it("should return nil for non-existent file", function()
local content = utils.read_file("/non/existent/file.txt")
assert.is_nil(content)
end)
end)
describe("file_exists", function()
it("should return true for existing file", function()
utils.write_file(test_file, "content")
assert.is_true(utils.file_exists(test_file))
end)
it("should return false for non-existent file", function()
assert.is_false(utils.file_exists("/non/existent/file.txt"))
end)
end)
end)
describe("get_filetype", function()
it("should return filetype for buffer", function()
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].filetype = "lua"
local ft = utils.get_filetype(buf)
assert.equals("lua", ft)
vim.api.nvim_buf_delete(buf, { force = true })
end)
end)
end)

View File

@@ -1,269 +0,0 @@
---@diagnostic disable: undefined-global
-- Tests for lua/codetyper/agent/worker.lua response cleaning
-- We need to test the clean_response function
-- Since it's local, we'll create a test module that exposes it
describe("worker response cleaning", function()
-- Mock the clean_response function behavior directly
local function clean_response(response)
if not response then
return ""
end
local cleaned = response
-- Remove the original prompt tags /@ ... @/ if they appear in output
-- Use [%s%S] to match any character including newlines
cleaned = cleaned:gsub("/@[%s%S]-@/", "")
-- Try to extract code from markdown code blocks
local code_block = cleaned:match("```[%w]*\n(.-)\n```")
if not code_block then
code_block = cleaned:match("```[%w]*(.-)\n```")
end
if not code_block then
code_block = cleaned:match("```(.-)```")
end
if code_block then
cleaned = code_block
else
local explanation_starts = {
"^[Ii]'m sorry.-\n",
"^[Ii] apologize.-\n",
"^[Hh]ere is.-:\n",
"^[Hh]ere's.-:\n",
"^[Tt]his is.-:\n",
"^[Bb]ased on.-:\n",
"^[Ss]ure.-:\n",
"^[Oo][Kk].-:\n",
"^[Cc]ertainly.-:\n",
}
for _, pattern in ipairs(explanation_starts) do
cleaned = cleaned:gsub(pattern, "")
end
local explanation_ends = {
"\n[Tt]his code.-$",
"\n[Tt]his function.-$",
"\n[Tt]his is a.-$",
"\n[Ii] hope.-$",
"\n[Ll]et me know.-$",
"\n[Ff]eel free.-$",
"\n[Nn]ote:.-$",
"\n[Pp]lease replace.-$",
"\n[Pp]lease note.-$",
"\n[Yy]ou might want.-$",
"\n[Yy]ou may want.-$",
"\n[Mm]ake sure.-$",
"\n[Aa]lso,.-$",
"\n[Rr]emember.-$",
}
for _, pattern in ipairs(explanation_ends) do
cleaned = cleaned:gsub(pattern, "")
end
end
cleaned = cleaned:gsub("^```[%w]*\n?", "")
cleaned = cleaned:gsub("\n?```$", "")
cleaned = cleaned:match("^%s*(.-)%s*$") or cleaned
return cleaned
end
describe("clean_response", function()
it("should extract code from markdown code blocks", function()
local response = [[```java
public void test() {
System.out.println("Hello");
}
```]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("public void test") ~= nil)
assert.is_true(cleaned:find("```") == nil)
end)
it("should handle code blocks without language", function()
local response = [[```
function test()
print("hello")
end
```]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("function test") ~= nil)
assert.is_true(cleaned:find("```") == nil)
end)
it("should remove single-line prompt tags from response", function()
local response = [[/@ create a function @/
function test() end]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("/@") == nil)
assert.is_true(cleaned:find("@/") == nil)
assert.is_true(cleaned:find("function test") ~= nil)
end)
it("should remove multiline prompt tags from response", function()
local response = [[function test() end
/@
create a function
that does something
@/
function another() end]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("/@") == nil)
assert.is_true(cleaned:find("@/") == nil)
assert.is_true(cleaned:find("function test") ~= nil)
assert.is_true(cleaned:find("function another") ~= nil)
end)
it("should remove multiple prompt tags from response", function()
local response = [[function test() end
/@ first prompt @/
/@ second
multiline prompt @/
function another() end]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("/@") == nil)
assert.is_true(cleaned:find("@/") == nil)
assert.is_true(cleaned:find("function test") ~= nil)
assert.is_true(cleaned:find("function another") ~= nil)
end)
it("should remove apology prefixes", function()
local response = [[I'm sorry for any confusion.
Here is the code:
function test() end]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("sorry") == nil or cleaned:find("function test") ~= nil)
end)
it("should remove trailing explanations", function()
local response = [[function test() end
This code does something useful.]]
local cleaned = clean_response(response)
-- The ending pattern should be removed
assert.is_true(cleaned:find("function test") ~= nil)
end)
it("should handle empty response", function()
local cleaned = clean_response("")
assert.equals("", cleaned)
end)
it("should handle nil response", function()
local cleaned = clean_response(nil)
assert.equals("", cleaned)
end)
it("should preserve clean code", function()
local response = [[function test()
return true
end]]
local cleaned = clean_response(response)
assert.equals(response, cleaned)
end)
it("should handle complex markdown with explanation", function()
local response = [[Here is the implementation:
```lua
local function validate(input)
if not input then
return false
end
return true
end
```
Let me know if you need any changes.]]
local cleaned = clean_response(response)
assert.is_true(cleaned:find("local function validate") ~= nil)
assert.is_true(cleaned:find("```") == nil)
assert.is_true(cleaned:find("Let me know") == nil)
end)
end)
describe("needs_more_context detection", function()
local context_needed_patterns = {
"^%s*i need more context",
"^%s*i'm sorry.-i need more",
"^%s*i apologize.-i need more",
"^%s*could you provide more context",
"^%s*could you please provide more",
"^%s*can you clarify",
"^%s*please provide more context",
"^%s*more information needed",
"^%s*not enough context",
"^%s*i don't have enough",
"^%s*unclear what you",
"^%s*what do you mean by",
}
local function needs_more_context(response)
if not response then
return false
end
-- If response has substantial code, don't ask for context
local lines = vim.split(response, "\n")
local code_lines = 0
for _, line in ipairs(lines) do
if line:match("[{}();=]") or line:match("function") or line:match("def ")
or line:match("class ") or line:match("return ") or line:match("import ")
or line:match("public ") or line:match("private ") or line:match("local ") then
code_lines = code_lines + 1
end
end
if code_lines >= 3 then
return false
end
local lower = response:lower()
for _, pattern in ipairs(context_needed_patterns) do
if lower:match(pattern) then
return true
end
end
return false
end
it("should detect context needed phrases at start", function()
assert.is_true(needs_more_context("I need more context to help you"))
assert.is_true(needs_more_context("Could you provide more context?"))
assert.is_true(needs_more_context("Can you clarify what you want?"))
assert.is_true(needs_more_context("I'm sorry, but I need more information to help"))
end)
it("should not trigger on normal responses", function()
assert.is_false(needs_more_context("Here is your code"))
assert.is_false(needs_more_context("function test() end"))
assert.is_false(needs_more_context("The implementation is complete"))
end)
it("should not trigger when response has substantial code", function()
local response_with_code = [[Here is the code:
function test() {
return true;
}
function another() {
return false;
}]]
assert.is_false(needs_more_context(response_with_code))
end)
it("should not trigger on code with explanatory text", function()
local response = [[public void test() {
System.out.println("Hello");
}
Please replace the connection string with your actual database.]]
assert.is_false(needs_more_context(response))
end)
it("should handle nil response", function()
assert.is_false(needs_more_context(nil))
end)
end)
end)