Fixing the old configuration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,4 @@
|
||||
# Codetyper.nvim - AI coding partner files
|
||||
*.coder.*
|
||||
.coder/
|
||||
.claude/
|
||||
Makefile
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
12
README.md
12
README.md
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
18
llms.txt
18
llms.txt
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,7 @@ local DEFAULT_IGNORES = {
|
||||
"^node_modules$",
|
||||
"^__pycache__$",
|
||||
"^%.git$",
|
||||
"^%.coder$",
|
||||
"^%.codetyper$",
|
||||
"^dist$",
|
||||
"^build$",
|
||||
"^target$",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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}"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user