From 9f229b26c9fafb759631a8121349118e36475229 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Wed, 18 Mar 2026 21:56:45 -0400 Subject: [PATCH] Fixing the old configuration --- .gitignore | 2 - CHANGELOG.md | 4 +- README.md | 12 +- doc/codetyper.txt | 33 +- llms.txt | 18 +- lua/codetyper/adapters/nvim/autocmds.lua | 84 +-- lua/codetyper/adapters/nvim/ui/logs_panel.lua | 534 +++++++++--------- lua/codetyper/config/defaults.lua | 2 +- lua/codetyper/config/preferences.lua | 193 ++----- lua/codetyper/core/cost/init.lua | 2 +- lua/codetyper/core/diff/patch.lua | 140 +---- lua/codetyper/core/memory/storage.lua | 2 +- lua/codetyper/core/scheduler/resume.lua | 2 +- lua/codetyper/core/scheduler/scheduler.lua | 2 +- lua/codetyper/core/scheduler/worker.lua | 10 +- lua/codetyper/features/completion/inline.lua | 293 +++++----- lua/codetyper/features/indexer/init.lua | 10 +- lua/codetyper/features/indexer/memory.lua | 6 +- lua/codetyper/features/indexer/scanner.lua | 2 +- lua/codetyper/init.lua | 4 +- lua/codetyper/parser.lua | 165 +++--- lua/codetyper/support/gitignore.lua | 291 +++++----- lua/codetyper/support/tree.lua | 22 +- lua/codetyper/support/utils.lua | 12 +- lua/codetyper/types.lua | 2 +- plugin/codetyper.lua | 10 +- tests/minimal_init.lua | 47 -- tests/run_tests.sh | 62 -- tests/spec/brain_delta_spec.lua | 252 --------- tests/spec/brain_hash_spec.lua | 128 ----- tests/spec/brain_learners_spec.lua | 153 ----- tests/spec/brain_node_spec.lua | 234 -------- tests/spec/brain_storage_spec.lua | 173 ------ tests/spec/coder_context_spec.lua | 194 ------- tests/spec/coder_ignore_spec.lua | 161 ------ tests/spec/confidence_spec.lua | 148 ----- tests/spec/config_spec.lua | 137 ----- tests/spec/indexer_spec.lua | 345 ----------- tests/spec/inject_spec.lua | 371 ------------ tests/spec/intent_spec.lua | 286 ---------- tests/spec/llm_selector_spec.lua | 174 ------ tests/spec/llm_spec.lua | 118 ---- tests/spec/logs_spec.lua | 280 --------- tests/spec/memory_spec.lua | 341 ----------- tests/spec/parser_spec.lua | 207 ------- tests/spec/patch_spec.lua | 371 ------------ tests/spec/preferences_spec.lua | 276 --------- tests/spec/queue_spec.lua | 332 ----------- tests/spec/scanner_spec.lua | 285 ---------- tests/spec/utils_spec.lua | 139 ----- tests/spec/worker_spec.lua | 269 --------- 51 files changed, 791 insertions(+), 6549 deletions(-) delete mode 100644 tests/minimal_init.lua delete mode 100755 tests/run_tests.sh delete mode 100644 tests/spec/brain_delta_spec.lua delete mode 100644 tests/spec/brain_hash_spec.lua delete mode 100644 tests/spec/brain_learners_spec.lua delete mode 100644 tests/spec/brain_node_spec.lua delete mode 100644 tests/spec/brain_storage_spec.lua delete mode 100644 tests/spec/coder_context_spec.lua delete mode 100644 tests/spec/coder_ignore_spec.lua delete mode 100644 tests/spec/confidence_spec.lua delete mode 100644 tests/spec/config_spec.lua delete mode 100644 tests/spec/indexer_spec.lua delete mode 100644 tests/spec/inject_spec.lua delete mode 100644 tests/spec/intent_spec.lua delete mode 100644 tests/spec/llm_selector_spec.lua delete mode 100644 tests/spec/llm_spec.lua delete mode 100644 tests/spec/logs_spec.lua delete mode 100644 tests/spec/memory_spec.lua delete mode 100644 tests/spec/parser_spec.lua delete mode 100644 tests/spec/patch_spec.lua delete mode 100644 tests/spec/preferences_spec.lua delete mode 100644 tests/spec/queue_spec.lua delete mode 100644 tests/spec/scanner_spec.lua delete mode 100644 tests/spec/utils_spec.lua delete mode 100644 tests/spec/worker_spec.lua diff --git a/.gitignore b/.gitignore index 67e03a1..ca35a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ # Codetyper.nvim - AI coding partner files -*.coder.* -.coder/ .claude/ Makefile diff --git a/CHANGELOG.md b/CHANGELOG.md index e56064a..5501179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: `ctt`, `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 diff --git a/README.md b/README.md index 1a9db47..ffb216c 100644 --- a/README.md +++ b/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 ` | `: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 ``` diff --git a/doc/codetyper.txt b/doc/codetyper.txt index a78ddbe..67b2124 100644 --- a/doc/codetyper.txt +++ b/doc/codetyper.txt @@ -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) { diff --git a/llms.txt b/llms.txt index d2bda73..b4ed365 100644 --- a/llms.txt +++ b/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 diff --git a/lua/codetyper/adapters/nvim/autocmds.lua b/lua/codetyper/adapters/nvim/autocmds.lua index ec6aaea..fd55349 100644 --- a/lua/codetyper/adapters/nvim/autocmds.lua +++ b/lua/codetyper/adapters/nvim/autocmds.lua @@ -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 diff --git a/lua/codetyper/adapters/nvim/ui/logs_panel.lua b/lua/codetyper/adapters/nvim/ui/logs_panel.lua index 25cdfb4..5218bab 100644 --- a/lua/codetyper/adapters/nvim/ui/logs_panel.lua +++ b/lua/codetyper/adapters/nvim/ui/logs_panel.lua @@ -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", "", 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", "", 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", "", 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", "", 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 diff --git a/lua/codetyper/config/defaults.lua b/lua/codetyper/config/defaults.lua index fd0bfca..df3e434 100644 --- a/lua/codetyper/config/defaults.lua +++ b/lua/codetyper/config/defaults.lua @@ -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 diff --git a/lua/codetyper/config/preferences.lua b/lua/codetyper/config/preferences.lua index 0c3c0da..022a830 100644 --- a/lua/codetyper/config/preferences.lua +++ b/lua/codetyper/config/preferences.lua @@ -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", "", 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 diff --git a/lua/codetyper/core/cost/init.lua b/lua/codetyper/core/cost/init.lua index e304998..6a56d1f 100644 --- a/lua/codetyper/core/cost/init.lua +++ b/lua/codetyper/core/cost/init.lua @@ -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) diff --git a/lua/codetyper/core/diff/patch.lua b/lua/codetyper/core/diff/patch.lua index 41a4ff2..c6badbe 100644 --- a/lua/codetyper/core/diff/patch.lua +++ b/lua/codetyper/core/diff/patch.lua @@ -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") diff --git a/lua/codetyper/core/memory/storage.lua b/lua/codetyper/core/memory/storage.lua index 8f776dc..a903c31 100644 --- a/lua/codetyper/core/memory/storage.lua +++ b/lua/codetyper/core/memory/storage.lua @@ -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 diff --git a/lua/codetyper/core/scheduler/resume.lua b/lua/codetyper/core/scheduler/resume.lua index fcf3052..7a654d8 100644 --- a/lua/codetyper/core/scheduler/resume.lua +++ b/lua/codetyper/core/scheduler/resume.lua @@ -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 diff --git a/lua/codetyper/core/scheduler/scheduler.lua b/lua/codetyper/core/scheduler/scheduler.lua index df5721c..095946f 100644 --- a/lua/codetyper/core/scheduler/scheduler.lua +++ b/lua/codetyper/core/scheduler/scheduler.lua @@ -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) diff --git a/lua/codetyper/core/scheduler/worker.lua b/lua/codetyper/core/scheduler/worker.lua index a0ae87e..33b74fc 100644 --- a/lua/codetyper/core/scheduler/worker.lua +++ b/lua/codetyper/core/scheduler/worker.lua @@ -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 diff --git a/lua/codetyper/features/completion/inline.lua b/lua/codetyper/features/completion/inline.lua index a4fbf97..d9d4a61 100644 --- a/lua/codetyper/features/completion/inline.lua +++ b/lua/codetyper/features/completion/inline.lua @@ -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 style keybinding in insert mode - vim.keymap.set("i", "@", function() - M.show_file_completion() - end, { silent = true, desc = "Coder: Complete file reference" }) + -- Also allow manual trigger with style keybinding in insert mode + vim.keymap.set("i", "@", function() + M.show_file_completion() + end, { silent = true, desc = "Coder: Complete file reference" }) end return M diff --git a/lua/codetyper/features/indexer/init.lua b/lua/codetyper/features/indexer/init.lua index 4172b46..8570241 100644 --- a/lua/codetyper/features/indexer/init.lua +++ b/lua/codetyper/features/indexer/init.lua @@ -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() diff --git a/lua/codetyper/features/indexer/memory.lua b/lua/codetyper/features/indexer/memory.lua index e020623..bc65f23 100644 --- a/lua/codetyper/features/indexer/memory.lua +++ b/lua/codetyper/features/indexer/memory.lua @@ -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 diff --git a/lua/codetyper/features/indexer/scanner.lua b/lua/codetyper/features/indexer/scanner.lua index f1a767e..a789ecf 100644 --- a/lua/codetyper/features/indexer/scanner.lua +++ b/lua/codetyper/features/indexer/scanner.lua @@ -44,7 +44,7 @@ local DEFAULT_IGNORES = { "^node_modules$", "^__pycache__$", "^%.git$", - "^%.coder$", + "^%.codetyper$", "^dist$", "^build$", "^target$", diff --git a/lua/codetyper/init.lua b/lua/codetyper/init.lua index b7f9908..d0823d1 100644 --- a/lua/codetyper/init.lua +++ b/lua/codetyper/init.lua @@ -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 diff --git a/lua/codetyper/parser.lua b/lua/codetyper/parser.lua index ec91666..911f4ba 100644 --- a/lua/codetyper/parser.lua +++ b/lua/codetyper/parser.lua @@ -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 diff --git a/lua/codetyper/support/gitignore.lua b/lua/codetyper/support/gitignore.lua index 9105209..856f51d 100644 --- a/lua/codetyper/support/gitignore.lua +++ b/lua/codetyper/support/gitignore.lua @@ -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 diff --git a/lua/codetyper/support/tree.lua b/lua/codetyper/support/tree.lua index 4ad38d5..5fab0b0 100644 --- a/lua/codetyper/support/tree.lua +++ b/lua/codetyper/support/tree.lua @@ -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) diff --git a/lua/codetyper/support/utils.lua b/lua/codetyper/support/utils.lua index 01fa06a..5409626 100644 --- a/lua/codetyper/support/utils.lua +++ b/lua/codetyper/support/utils.lua @@ -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 diff --git a/lua/codetyper/types.lua b/lua/codetyper/types.lua index a7a7d40..b0c321a 100644 --- a/lua/codetyper/types.lua +++ b/lua/codetyper/types.lua @@ -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 diff --git a/plugin/codetyper.lua b/plugin/codetyper.lua index ca8de77..3a0a179 100644 --- a/plugin/codetyper.lua +++ b/plugin/codetyper.lua @@ -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") diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua deleted file mode 100644 index 5be0d81..0000000 --- a/tests/minimal_init.lua +++ /dev/null @@ -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, -}) diff --git a/tests/run_tests.sh b/tests/run_tests.sh deleted file mode 100755 index 25f1dd8..0000000 --- a/tests/run_tests.sh +++ /dev/null @@ -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}" diff --git a/tests/spec/brain_delta_spec.lua b/tests/spec/brain_delta_spec.lua deleted file mode 100644 index 58b89cf..0000000 --- a/tests/spec/brain_delta_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/brain_hash_spec.lua b/tests/spec/brain_hash_spec.lua deleted file mode 100644 index 3dc6d7c..0000000 --- a/tests/spec/brain_hash_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/brain_learners_spec.lua b/tests/spec/brain_learners_spec.lua deleted file mode 100644 index af9e774..0000000 --- a/tests/spec/brain_learners_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/brain_node_spec.lua b/tests/spec/brain_node_spec.lua deleted file mode 100644 index ed63a15..0000000 --- a/tests/spec/brain_node_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/brain_storage_spec.lua b/tests/spec/brain_storage_spec.lua deleted file mode 100644 index 9297d2a..0000000 --- a/tests/spec/brain_storage_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/coder_context_spec.lua b/tests/spec/coder_context_spec.lua deleted file mode 100644 index 60121c7..0000000 --- a/tests/spec/coder_context_spec.lua +++ /dev/null @@ -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("^ 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) diff --git a/tests/spec/config_spec.lua b/tests/spec/config_spec.lua deleted file mode 100644 index 4d0207f..0000000 --- a/tests/spec/config_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/indexer_spec.lua b/tests/spec/indexer_spec.lua deleted file mode 100644 index b1f6d59..0000000 --- a/tests/spec/indexer_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/inject_spec.lua b/tests/spec/inject_spec.lua deleted file mode 100644 index 9de3900..0000000 --- a/tests/spec/inject_spec.lua +++ /dev/null @@ -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
Hello
; -}]] - 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 -#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
Hello
; -}]] - - 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) diff --git a/tests/spec/intent_spec.lua b/tests/spec/intent_spec.lua deleted file mode 100644 index 1d85c90..0000000 --- a/tests/spec/intent_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/llm_selector_spec.lua b/tests/spec/llm_selector_spec.lua deleted file mode 100644 index 1a0fd41..0000000 --- a/tests/spec/llm_selector_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/llm_spec.lua b/tests/spec/llm_spec.lua deleted file mode 100644 index be9e90b..0000000 --- a/tests/spec/llm_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/logs_spec.lua b/tests/spec/logs_spec.lua deleted file mode 100644 index 0e19279..0000000 --- a/tests/spec/logs_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/memory_spec.lua b/tests/spec/memory_spec.lua deleted file mode 100644 index 7eafc70..0000000 --- a/tests/spec/memory_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/parser_spec.lua b/tests/spec/parser_spec.lua deleted file mode 100644 index 511328d..0000000 --- a/tests/spec/parser_spec.lua +++ /dev/null @@ -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 = "" - local prompts = parser.find_prompts(content, "") - - 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) diff --git a/tests/spec/patch_spec.lua b/tests/spec/patch_spec.lua deleted file mode 100644 index 418e11d..0000000 --- a/tests/spec/patch_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/preferences_spec.lua b/tests/spec/preferences_spec.lua deleted file mode 100644 index acaa7e1..0000000 --- a/tests/spec/preferences_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/queue_spec.lua b/tests/spec/queue_spec.lua deleted file mode 100644 index df9c08e..0000000 --- a/tests/spec/queue_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/scanner_spec.lua b/tests/spec/scanner_spec.lua deleted file mode 100644 index c046c35..0000000 --- a/tests/spec/scanner_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/utils_spec.lua b/tests/spec/utils_spec.lua deleted file mode 100644 index b13f391..0000000 --- a/tests/spec/utils_spec.lua +++ /dev/null @@ -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) diff --git a/tests/spec/worker_spec.lua b/tests/spec/worker_spec.lua deleted file mode 100644 index 58e61e8..0000000 --- a/tests/spec/worker_spec.lua +++ /dev/null @@ -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)