From 0a1429a823d921ef3c5c09e5866f51c728c6ed4c Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Tue, 17 Feb 2026 00:15:40 -0500 Subject: [PATCH] Adding the functionallity and refactoring --- lua/codetyper/adapters/nvim/autocmds.lua | 18 +- lua/codetyper/adapters/nvim/commands.lua | 616 +-------------------- lua/codetyper/config/defaults.lua | 206 +++---- lua/codetyper/core/diff/patch.lua | 30 +- lua/codetyper/core/scheduler/scheduler.lua | 32 ++ lua/codetyper/core/transform.lua | 224 ++++++++ lua/codetyper/init.lua | 78 +-- tests/spec/config_spec.lua | 12 - 8 files changed, 423 insertions(+), 793 deletions(-) create mode 100644 lua/codetyper/core/transform.lua diff --git a/lua/codetyper/adapters/nvim/autocmds.lua b/lua/codetyper/adapters/nvim/autocmds.lua index 3a70454..ec6aaea 100644 --- a/lua/codetyper/adapters/nvim/autocmds.lua +++ b/lua/codetyper/adapters/nvim/autocmds.lua @@ -322,22 +322,12 @@ local function create_injection_marks(target_bufnr, range) end_line = start_line end local marks = require("codetyper.core.marks") - local end_line_content = vim.api.nvim_buf_get_lines( - target_bufnr, - end_line - 1, - end_line, - false - ) + local end_line_content = vim.api.nvim_buf_get_lines(target_bufnr, end_line - 1, end_line, false) local end_col_0 = 0 if end_line_content and end_line_content[1] then end_col_0 = #end_line_content[1] end - local start_mark, end_mark = marks.mark_range( - target_bufnr, - start_line, - end_line, - end_col_0 - ) + local start_mark, end_mark = marks.mark_range(target_bufnr, start_line, end_line, end_col_0) if not start_mark.id or not end_mark.id then return nil end @@ -540,7 +530,9 @@ function M.check_for_closed_prompt() end -- Use captured injection range when provided, else prompt.start_line/end_line - local raw_start = (prompt.injection_range and prompt.injection_range.start_line) or prompt.start_line or 1 + local raw_start = (prompt.injection_range and prompt.injection_range.start_line) + or prompt.start_line + or 1 local raw_end = (prompt.injection_range and prompt.injection_range.end_line) or prompt.end_line or 1 local tc = vim.api.nvim_buf_line_count(target_bufnr) tc = math.max(1, tc) diff --git a/lua/codetyper/adapters/nvim/commands.lua b/lua/codetyper/adapters/nvim/commands.lua index 119b33f..f96c855 100644 --- a/lua/codetyper/adapters/nvim/commands.lua +++ b/lua/codetyper/adapters/nvim/commands.lua @@ -2,222 +2,8 @@ local M = {} +local transform = require("codetyper.core.transform") local utils = require("codetyper.support.utils") -local window = require("codetyper.adapters.nvim.windows") - ---- Open coder view for current file or select one ----@param opts? table Command options -local function cmd_open(opts) - opts = opts or {} - - local current_file = vim.fn.expand("%:p") - - -- If no file is open, prompt user to select one - if current_file == "" or vim.bo.buftype ~= "" then - -- Use telescope or vim.ui.select to pick a file - if pcall(require, "telescope") then - require("telescope.builtin").find_files({ - prompt_title = "Select file for Coder", - attach_mappings = function(prompt_bufnr, map) - local actions = require("telescope.actions") - local action_state = require("telescope.actions.state") - - actions.select_default:replace(function() - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - if selection then - local target_path = selection.path or selection[1] - local coder_path = utils.get_coder_path(target_path) - window.open_split(target_path, coder_path) - end - end) - return true - end, - }) - else - -- Fallback to input prompt - vim.ui.input({ prompt = "Enter file path: " }, function(input) - if input and input ~= "" then - local target_path = vim.fn.fnamemodify(input, ":p") - local coder_path = utils.get_coder_path(target_path) - window.open_split(target_path, coder_path) - end - end) - end - return - end - - local target_path, coder_path - - -- Check if current file is a coder file - if utils.is_coder_file(current_file) then - coder_path = current_file - target_path = utils.get_target_path(current_file) - else - target_path = current_file - coder_path = utils.get_coder_path(current_file) - end - - window.open_split(target_path, coder_path) -end - ---- Close coder view -local function cmd_close() - window.close_split() -end - ---- Toggle coder view -local function cmd_toggle() - local current_file = vim.fn.expand("%:p") - - if current_file == "" then - utils.notify("No file in current buffer", vim.log.levels.WARN) - return - end - - local target_path, coder_path - - if utils.is_coder_file(current_file) then - coder_path = current_file - target_path = utils.get_target_path(current_file) - else - target_path = current_file - coder_path = utils.get_coder_path(current_file) - end - - window.toggle_split(target_path, coder_path) -end - ---- Return editor dimensions (from UI, like 99 plugin) ----@return number width ----@return number height -local function get_ui_dimensions() - local ui = vim.api.nvim_list_uis()[1] - if ui then - return ui.width, ui.height - end - return vim.o.columns, vim.o.lines -end - ---- Centered floating window config for prompt (2/3 width, 1/3 height) ----@return table { width, height, row, col, border } -local function create_centered_window() - local width, height = get_ui_dimensions() - local win_width = math.floor(width * 2 / 3) - local win_height = math.floor(height / 3) - return { - width = win_width, - height = win_height, - row = math.floor((height - win_height) / 2), - col = math.floor((width - win_width) / 2), - border = "rounded", - } -end - ---- Build enhanced user prompt with context ----@param clean_prompt string The cleaned user prompt ----@param context table Context information ----@return string Enhanced prompt -local function build_user_prompt(clean_prompt, context) - local enhanced = "TASK: " .. clean_prompt .. "\n\n" - - enhanced = enhanced .. "REQUIREMENTS:\n" - enhanced = enhanced .. "- Generate ONLY " .. (context.language or "code") .. " code\n" - enhanced = enhanced .. "- NO markdown code blocks (no ```)\n" - enhanced = enhanced .. "- NO explanations or comments about what you did\n" - enhanced = enhanced .. "- Match the coding style of the existing file exactly\n" - enhanced = enhanced .. "- Output must be ready to insert directly into the file\n" - - return enhanced -end - ---- Process prompt at cursor and generate code -local function cmd_process() - local parser = require("codetyper.parser") - local llm = require("codetyper.core.llm") - - local bufnr = vim.api.nvim_get_current_buf() - local current_file = vim.fn.expand("%:p") - - if not utils.is_coder_file(current_file) then - utils.notify("Not a coder file. Use *.coder.* files", vim.log.levels.WARN) - return - end - - local prompt = parser.get_last_prompt(bufnr) - if not prompt then - utils.notify("No prompt found. Use /@ your prompt @/", vim.log.levels.WARN) - return - end - - local target_path = utils.get_target_path(current_file) - local prompt_type = parser.detect_prompt_type(prompt.content) - local context = llm.build_context(target_path, prompt_type) - local clean_prompt = parser.clean_prompt(prompt.content) - - -- Build enhanced prompt with explicit instructions - local enhanced_prompt = build_user_prompt(clean_prompt, context) - - utils.notify("Processing: " .. clean_prompt:sub(1, 50) .. "...", vim.log.levels.INFO) - - llm.generate(enhanced_prompt, context, function(response, err) - if err then - utils.notify("Generation failed: " .. err, vim.log.levels.ERROR) - return - end - - if response then - -- Inject code into target file - local inject = require("codetyper.inject") - inject.inject_code(target_path, response, prompt_type) - utils.notify("Code generated and injected!", vim.log.levels.INFO) - end - end) -end - ---- Show plugin status -local function cmd_status() - local codetyper = require("codetyper") - local config = codetyper.get_config() - local tree = require("codetyper.support.tree") - - local stats = tree.get_stats() - - local status = { - "Codetyper.nvim Status", - "====================", - "", - "Provider: " .. config.llm.provider, - } - - if config.llm.provider == "ollama" then - table.insert(status, "Ollama Host: " .. config.llm.ollama.host) - table.insert(status, "Ollama Model: " .. config.llm.ollama.model) - elseif config.llm.provider == "openai" then - local has_key = (config.llm.openai.api_key or vim.env.OPENAI_API_KEY) ~= nil - table.insert(status, "OpenAI API Key: " .. (has_key and "configured" or "NOT SET")) - table.insert(status, "OpenAI Model: " .. config.llm.openai.model) - elseif config.llm.provider == "gemini" then - local has_key = (config.llm.gemini.api_key or vim.env.GEMINI_API_KEY) ~= nil - table.insert(status, "Gemini API Key: " .. (has_key and "configured" or "NOT SET")) - table.insert(status, "Gemini Model: " .. config.llm.gemini.model) - elseif config.llm.provider == "copilot" then - table.insert(status, "Copilot Model: " .. config.llm.copilot.model) - end - - table.insert(status, "") - table.insert(status, "Window Position: " .. config.window.position) - table.insert(status, "Window Width: " .. tostring(config.window.width * 100) .. "%") - table.insert(status, "") - table.insert(status, "View Open: " .. (window.is_open() and "yes" or "no")) - table.insert(status, "") - table.insert(status, "Project Stats:") - table.insert(status, " Files: " .. stats.files) - table.insert(status, " Directories: " .. stats.directories) - table.insert(status, " Tree Log: " .. (tree.get_tree_log_path() or "N/A")) - - utils.notify(table.concat(status, "\n")) -end --- Refresh tree.log manually local function cmd_tree() @@ -260,248 +46,6 @@ local function cmd_gitignore() gitignore.force_update() end ---- Switch focus between coder and target windows -local function cmd_focus() - if not window.is_open() then - utils.notify("Coder view not open", vim.log.levels.WARN) - return - end - - local current_win = vim.api.nvim_get_current_win() - if current_win == window.get_coder_win() then - window.focus_target() - else - window.focus_coder() - end -end - ---- Transform prompts within a line range (for visual selection) ---- Uses the same processing logic as automatic mode for consistent results ----@param start_line number Start line (1-indexed) ----@param end_line number End line (1-indexed) -local function cmd_transform_range(start_line, end_line) - local parser = require("codetyper.parser") - local autocmds = require("codetyper.adapters.nvim.autocmds") - - local bufnr = vim.api.nvim_get_current_buf() - local filepath = vim.fn.expand("%:p") - - if filepath == "" then - utils.notify("No file in current buffer", vim.log.levels.WARN) - return - end - - -- Find all prompts in the current buffer - local all_prompts = parser.find_prompts_in_buffer(bufnr) - - -- Filter prompts that are within the selected range - local prompts = {} - for _, prompt in ipairs(all_prompts) do - if prompt.start_line >= start_line and prompt.end_line <= end_line then - table.insert(prompts, prompt) - end - end - - if #prompts == 0 then - utils.notify( - "No /@ @/ tags found in selection (lines " .. start_line .. "-" .. end_line .. ")", - vim.log.levels.INFO - ) - return - end - - utils.notify("Transforming " .. #prompts .. " prompt(s)...", vim.log.levels.INFO) - - -- Process each prompt using the same logic as automatic mode (skip processed check for manual mode) - for _, prompt in ipairs(prompts) do - autocmds.process_single_prompt(bufnr, prompt, filepath, true) - end -end - ---- Get visual selection text ----@return string|nil selected_text The selected text or nil -local function get_visual_selection() - local mode = vim.api.nvim_get_mode().mode - -- Third argument must be a Vim dictionary; empty Lua table can be treated as list - local opts = vim.empty_dict() - -- \22 is an escaped version of - if mode == "v" or mode == "V" or mode == "\22" then - opts = { type = mode } - end - return vim.fn.getregion(vim.fn.getpos("v"), vim.fn.getpos("."), opts) -end - ---- Transform visual selection with custom prompt input ---- Opens input window for prompt, processes selection on confirm. ---- When nothing is selected (e.g. from Normal mode), only the prompt is requested. -local function cmd_transform_selection() - local logger = require("codetyper.support.logger") - logger.func_entry("commands", "cmd_transform_selection", {}) - -- Get visual selection (getregion returns a table of lines); may be empty in Normal mode - local selection = get_visual_selection() - local selection_text = type(selection) == "table" and table.concat(selection, "\n") or tostring(selection or "") - local has_selection = selection_text and #selection_text >= 4 - - if has_selection then - logger.debug( - "commands", - "Visual selection: " .. selection_text:sub(1, 100) .. (#selection_text > 100 and "..." or "") - ) - logger.info("commands", "Selected " .. #selection_text .. " characters, opening prompt input...") - else - logger.info("commands", "No selection, opening prompt input only...") - end - - local bufnr = vim.api.nvim_get_current_buf() - local filepath = vim.fn.expand("%:p") - local line_count = vim.api.nvim_buf_line_count(bufnr) - line_count = math.max(1, line_count) - - -- Range for injection: selection, or whole file when no selection - local start_line, end_line - if has_selection then - start_line = vim.fn.line("'<") - -- Derive end_line from selection content so range matches selected lines (''>' can be wrong after UI changes) - local selection_lines = type(selection) == "table" and #selection or #vim.split(selection_text, "\n", { plain = true }) - selection_lines = math.max(1, selection_lines) - end_line = math.min(start_line + selection_lines - 1, line_count) - else - -- No selection: apply to whole file so the LLM works on the entire file - start_line = 1 - end_line = line_count - end - -- Clamp to valid 1-based range (avoid 0 or out-of-bounds) - start_line = math.max(1, math.min(start_line, line_count)) - end_line = math.max(1, math.min(end_line, line_count)) - if end_line < start_line then - end_line = start_line - end - - -- Capture injection range so we know exactly where to apply the generated code later - local injection_range = { start_line = start_line, end_line = end_line } - local range_line_count = end_line - start_line + 1 - logger.info("commands", string.format("Injection range: lines %d-%d (%d lines) – changes will replace this range", start_line, end_line, range_line_count)) - - -- Open centered prompt window (pattern from 99: acwrite + BufWriteCmd to submit, BufLeave to keep focus) - local prompt_buf = vim.api.nvim_create_buf(false, true) - vim.bo[prompt_buf].buftype = "acwrite" - vim.bo[prompt_buf].bufhidden = "wipe" - vim.bo[prompt_buf].filetype = "markdown" - vim.bo[prompt_buf].swapfile = false - vim.api.nvim_buf_set_name(prompt_buf, "codetyper-prompt") - - local win_opts = create_centered_window() - local prompt_win = vim.api.nvim_open_win(prompt_buf, true, { - relative = "editor", - row = win_opts.row, - col = win_opts.col, - width = win_opts.width, - height = win_opts.height, - style = "minimal", - border = win_opts.border, - title = has_selection and " Enter prompt for selection " or " Enter prompt ", - title_pos = "center", - }) - vim.wo[prompt_win].wrap = true - vim.api.nvim_set_current_win(prompt_win) - - local function close_prompt() - if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then - vim.api.nvim_win_close(prompt_win, true) - end - if prompt_buf and vim.api.nvim_buf_is_valid(prompt_buf) then - vim.api.nvim_buf_delete(prompt_buf, { force = true }) - end - prompt_win = nil - prompt_buf = nil - end - - local function submit_prompt() - if not prompt_buf or not vim.api.nvim_buf_is_valid(prompt_buf) then - close_prompt() - return - end - submitted = true - local lines = vim.api.nvim_buf_get_lines(prompt_buf, 0, -1, false) - local input = table.concat(lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "") - close_prompt() - if input == "" then - logger.info("commands", "User cancelled prompt input") - return - end - logger.info("commands", "Processing with prompt: " .. input:sub(1, 50) .. (#input > 50 and "..." or "")) - local content = has_selection and (input .. "\n\nCode:\n" .. selection_text) or input - -- Pass captured range so scheduler/patch know where to inject the generated code - local prompt = { - content = content, - start_line = injection_range.start_line, - end_line = injection_range.end_line, - start_col = 1, - end_col = 1, - selection = selection, - user_prompt = input, - -- Explicit injection range (same as start_line/end_line) for downstream - injection_range = injection_range, - } - local autocmds = require("codetyper.adapters.nvim.autocmds") - autocmds.process_single_prompt(bufnr, prompt, filepath, true) - logger.func_exit("commands", "cmd_transform_selection", "completed") - end - - local augroup = vim.api.nvim_create_augroup("CodetyperPrompt_" .. prompt_buf, { clear = true }) - local submitted = false - - -- Submit on :w (acwrite buffer triggers BufWriteCmd) - vim.api.nvim_create_autocmd("BufWriteCmd", { - group = augroup, - buffer = prompt_buf, - callback = function() - if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then - submitted = true - submit_prompt() - end - end, - }) - - -- Keep focus in prompt window (prevent leaving to other buffers) - vim.api.nvim_create_autocmd("BufLeave", { - group = augroup, - buffer = prompt_buf, - callback = function() - if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then - vim.api.nvim_set_current_win(prompt_win) - end - end, - }) - - -- Clean up when window is closed (e.g. :q or close button) - vim.api.nvim_create_autocmd("WinClosed", { - group = augroup, - pattern = tostring(prompt_win), - callback = function() - if not submitted then - logger.info("commands", "User cancelled prompt input") - end - close_prompt() - end, - }) - - local map_opts = { buffer = prompt_buf, noremap = true, silent = true } - -- Normal mode: Enter, :w, or Ctrl+Enter to submit - vim.keymap.set("n", "", submit_prompt, map_opts) - vim.keymap.set("n", "", submit_prompt, map_opts) - vim.keymap.set("n", "", submit_prompt, map_opts) - vim.keymap.set("n", "w", "w", vim.tbl_extend("force", map_opts, { desc = "Submit prompt" })) - -- Insert mode: Ctrl+Enter to submit - vim.keymap.set("i", "", submit_prompt, map_opts) - vim.keymap.set("i", "", submit_prompt, map_opts) - -- Close/cancel: Esc (in normal), q, or :q - vim.keymap.set("n", "", close_prompt, map_opts) - vim.keymap.set("n", "q", close_prompt, map_opts) - - vim.cmd("startinsert") -end - --- Index the entire project local function cmd_index_project() local indexer = require("codetyper.features.indexer") @@ -627,59 +171,6 @@ local function cmd_forget(pattern) end end ---- Transform a single prompt at cursor position -local function cmd_transform_at_cursor() - local parser = require("codetyper.parser") - local autocmds = require("codetyper.adapters.nvim.autocmds") - - local bufnr = vim.api.nvim_get_current_buf() - local filepath = vim.fn.expand("%:p") - - vim.notify( - "[codetyper] cmd_transform_at_cursor called: bufnr=" .. bufnr .. ", filepath=" .. tostring(filepath), - vim.log.levels.DEBUG - ) - - if filepath == "" then - vim.notify("[codetyper] No file in current buffer", vim.log.levels.DEBUG) - utils.notify("No file in current buffer", vim.log.levels.WARN) - return - end - - -- Find prompt at cursor - vim.notify("[codetyper] Calling parser.get_prompt_at_cursor...", vim.log.levels.DEBUG) - local prompt = parser.get_prompt_at_cursor(bufnr) - vim.notify( - "[codetyper] parser.get_prompt_at_cursor returned: " .. (prompt and "prompt found" or "nil"), - vim.log.levels.DEBUG - ) - - if not prompt then - vim.notify("[codetyper] No prompt found at cursor", vim.log.levels.DEBUG) - utils.notify("No /@ @/ tag at cursor position", vim.log.levels.WARN) - return - end - - vim.notify( - "[codetyper] Prompt found: start_line=" - .. prompt.start_line - .. ", end_line=" - .. prompt.end_line - .. ", content_length=" - .. #prompt.content, - vim.log.levels.DEBUG - ) - - local clean_prompt = parser.clean_prompt(prompt.content) - vim.notify("[codetyper] Clean prompt: " .. clean_prompt:sub(1, 50) .. "...", vim.log.levels.DEBUG) - utils.notify("Transforming: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO) - - -- Use the same processing logic as automatic mode (skip processed check for manual mode) - vim.notify("[codetyper] Calling autocmds.process_single_prompt...", vim.log.levels.DEBUG) - autocmds.process_single_prompt(bufnr, prompt, filepath, true) - vim.notify("[codetyper] autocmds.process_single_prompt completed", vim.log.levels.DEBUG) -end - --- Main command dispatcher ---@param args table Command arguments --- Show LLM accuracy statistics @@ -732,48 +223,17 @@ local function coder_cmd(args) local subcommand = args.fargs[1] or "toggle" local commands = { - open = cmd_open, - close = cmd_close, - toggle = cmd_toggle, - process = cmd_process, - status = cmd_status, - focus = cmd_focus, tree = cmd_tree, ["tree-view"] = cmd_tree_view, reset = cmd_reset, gitignore = cmd_gitignore, - - ["transform-selection"] = cmd_transform_selection, - + ["transform-selection"] = transform.cmd_transform_selection, ["index-project"] = cmd_index_project, ["index-status"] = cmd_index_status, memories = cmd_memories, forget = function(args) cmd_forget(args.fargs[2]) end, - ["auto-toggle"] = function() - local preferences = require("codetyper.config.preferences") - preferences.toggle_auto_process() - end, - ["auto-set"] = function(args) - local preferences = require("codetyper.config.preferences") - local arg = (args[1] or ""):lower() - if arg == "auto" or arg == "automatic" or arg == "on" then - preferences.set_auto_process(true) - utils.notify("Set to automatic mode", vim.log.levels.INFO) - elseif arg == "manual" or arg == "off" then - preferences.set_auto_process(false) - utils.notify("Set to manual mode", vim.log.levels.INFO) - else - local auto = preferences.is_auto_process_enabled() - if auto == nil then - utils.notify("Mode not set yet (will ask on first prompt)", vim.log.levels.INFO) - else - local mode = auto and "automatic" or "manual" - utils.notify("Currently in " .. mode .. " mode", vim.log.levels.INFO) - end - end - end, -- LLM smart selection commands ["llm-stats"] = cmd_llm_stats, ["llm-feedback-good"] = function() @@ -849,25 +309,17 @@ function M.setup() nargs = "?", complete = function() return { - "open", - "close", - "toggle", "process", "status", - "focus", "tree", "tree-view", "reset", "gitignore", - "transform", - "transform-cursor", "transform-selection", "index-project", "index-status", "memories", "forget", - "auto-toggle", - "auto-set", "llm-stats", "llm-feedback-good", "llm-feedback-bad", @@ -884,23 +336,6 @@ function M.setup() desc = "Codetyper.nvim commands", }) - -- Convenience aliases - vim.api.nvim_create_user_command("CoderOpen", function() - cmd_open() - end, { desc = "Open Coder view" }) - - vim.api.nvim_create_user_command("CoderClose", function() - cmd_close() - end, { desc = "Close Coder view" }) - - vim.api.nvim_create_user_command("CoderToggle", function() - cmd_toggle() - end, { desc = "Toggle Coder view" }) - - vim.api.nvim_create_user_command("CoderProcess", function() - cmd_process() - end, { desc = "Process prompt and generate code" }) - vim.api.nvim_create_user_command("CoderTree", function() cmd_tree() end, { desc = "Refresh tree.log" }) @@ -909,14 +344,8 @@ function M.setup() cmd_tree_view() end, { desc = "View tree.log" }) - vim.api.nvim_create_user_command("CoderTransformVisual", function(opts) - local start_line = opts.line1 - local end_line = opts.line2 - cmd_transform_range(start_line, end_line) - end, { range = true, desc = "Transform /@ @/ tags in visual selection" }) - vim.api.nvim_create_user_command("CoderTransformSelection", function() - cmd_transform_selection() + transform.cmd_transform_selection() end, { desc = "Transform visual selection with custom prompt input" }) -- Project indexer commands @@ -939,39 +368,6 @@ function M.setup() nargs = "?", }) - -- Preferences commands - vim.api.nvim_create_user_command("CoderAutoToggle", function() - local preferences = require("codetyper.config.preferences") - preferences.toggle_auto_process() - end, { desc = "Toggle automatic/manual prompt processing" }) - - vim.api.nvim_create_user_command("CoderAutoSet", function(opts) - local preferences = require("codetyper.config.preferences") - local arg = opts.args:lower() - if arg == "auto" or arg == "automatic" or arg == "on" then - preferences.set_auto_process(true) - vim.notify("Codetyper: Set to automatic mode", vim.log.levels.INFO) - elseif arg == "manual" or arg == "off" then - preferences.set_auto_process(false) - vim.notify("Codetyper: Set to manual mode", vim.log.levels.INFO) - else - -- Show current mode - local auto = preferences.is_auto_process_enabled() - if auto == nil then - vim.notify("Codetyper: Mode not set yet (will ask on first prompt)", vim.log.levels.INFO) - else - local mode = auto and "automatic" or "manual" - vim.notify("Codetyper: Currently in " .. mode .. " mode", vim.log.levels.INFO) - end - end - end, { - desc = "Set prompt processing mode (auto/manual)", - nargs = "?", - complete = function() - return { "auto", "manual" } - end, - }) - -- Brain feedback command - teach the brain from your experience vim.api.nvim_create_user_command("CoderFeedback", function(opts) local brain = require("codetyper.core.memory") @@ -1154,17 +550,17 @@ end function M.setup_keymaps() -- Visual mode: transform selection with custom prompt input vim.keymap.set("v", "ctt", function() - cmd_transform_selection() + transform.cmd_transform_selection() end, { silent = true, desc = "Coder: Transform selection with prompt", }) -- Normal mode: prompt only (no selection); request is entered in the prompt vim.keymap.set("n", "ctt", function() - cmd_transform_selection() + transform.cmd_transform_selection() end, { silent = true, - desc = "Coder: Transform with prompt (no selection)", + desc = "Coder: Open prompt window", }) end diff --git a/lua/codetyper/config/defaults.lua b/lua/codetyper/config/defaults.lua index 0ed61b2..fd0bfca 100644 --- a/lua/codetyper/config/defaults.lua +++ b/lua/codetyper/config/defaults.lua @@ -4,79 +4,57 @@ local M = {} ---@type CoderConfig local defaults = { - llm = { - provider = "ollama", -- Options: "ollama", "openai", "gemini", "copilot" - ollama = { - host = "http://localhost:11434", - model = "deepseek-coder:6.7b", - }, - openai = { - api_key = nil, -- Will use OPENAI_API_KEY env var if nil - model = "gpt-4o", - endpoint = nil, -- Custom endpoint (Azure, OpenRouter, etc.) - }, - gemini = { - api_key = nil, -- Will use GEMINI_API_KEY env var if nil - model = "gemini-2.0-flash", - }, - copilot = { - model = "claude-sonnet-4", -- Uses GitHub Copilot authentication - }, - }, - window = { - width = 25, -- 25% of screen width (1/4) - position = "left", - border = "rounded", - }, - patterns = { - open_tag = "/@", - close_tag = "@/", - file_pattern = "*.coder.*", - }, - auto_gitignore = true, - auto_index = false, -- Auto-create coder companion files on file open - indexer = { - enabled = true, -- Enable project indexing - 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" }, - index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" }, - memory = { - enabled = true, -- Enable memory persistence - max_memories = 1000, -- Maximum stored memories - prune_threshold = 0.1, -- Remove low-weight memories - }, - }, - brain = { - enabled = true, -- Enable brain learning system - auto_learn = true, -- Auto-learn from events - auto_commit = true, -- Auto-commit after threshold - commit_threshold = 10, -- Changes before auto-commit - max_nodes = 5000, -- Maximum nodes before pruning - max_deltas = 500, -- Maximum delta history - prune = { - enabled = true, -- Enable auto-pruning - threshold = 0.1, -- Remove nodes below this weight - unused_days = 90, -- Remove unused nodes after N days - }, - output = { - max_tokens = 4000, -- Token budget for LLM context - format = "compact", -- "compact"|"json"|"natural" - }, - }, - suggestion = { - enabled = true, -- Enable ghost text suggestions (Copilot-style) - auto_trigger = true, -- Auto-trigger on typing - debounce = 150, -- Debounce in milliseconds - use_copilot = true, -- Use copilot.lua suggestions when available, fallback to codetyper - keymap = { - accept = "", -- Accept suggestion - next = "", -- Next suggestion (Alt+]) - prev = "", -- Previous suggestion (Alt+[) - dismiss = "", -- Dismiss suggestion (Ctrl+]) - }, - }, + llm = { + provider = "ollama", -- Options: "ollama", "openai", "gemini", "copilot" + ollama = { + host = "http://localhost:11434", + model = "deepseek-coder:6.7b", + }, + openai = { + api_key = nil, -- Will use OPENAI_API_KEY env var if nil + model = "gpt-4o", + endpoint = nil, -- Custom endpoint (Azure, OpenRouter, etc.) + }, + gemini = { + api_key = nil, -- Will use GEMINI_API_KEY env var if nil + model = "gemini-2.0-flash", + }, + copilot = { + model = "claude-sonnet-4", -- Uses GitHub Copilot authentication + }, + }, + auto_gitignore = true, + auto_index = false, -- Auto-create coder companion files on file open + indexer = { + enabled = true, -- Enable project indexing + 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" }, + index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" }, + memory = { + enabled = true, -- Enable memory persistence + max_memories = 1000, -- Maximum stored memories + prune_threshold = 0.1, -- Remove low-weight memories + }, + }, + brain = { + enabled = true, -- Enable brain learning system + auto_learn = true, -- Auto-learn from events + auto_commit = true, -- Auto-commit after threshold + commit_threshold = 10, -- Changes before auto-commit + max_nodes = 5000, -- Maximum nodes before pruning + max_deltas = 500, -- Maximum delta history + prune = { + enabled = true, -- Enable auto-pruning + threshold = 0.1, -- Remove nodes below this weight + unused_days = 90, -- Remove unused nodes after N days + }, + output = { + max_tokens = 4000, -- Token budget for LLM context + format = "compact", -- "compact"|"json"|"natural" + }, + }, } --- Deep merge two tables @@ -84,68 +62,68 @@ local defaults = { ---@param t2 table Table to merge into base ---@return table Merged table local function deep_merge(t1, t2) - local result = vim.deepcopy(t1) - for k, v in pairs(t2) do - if type(v) == "table" and type(result[k]) == "table" then - result[k] = deep_merge(result[k], v) - else - result[k] = v - end - end - return result + local result = vim.deepcopy(t1) + for k, v in pairs(t2) do + if type(v) == "table" and type(result[k]) == "table" then + result[k] = deep_merge(result[k], v) + else + result[k] = v + end + end + return result end --- Setup configuration with user options ---@param opts? CoderConfig User configuration options ---@return CoderConfig Final configuration function M.setup(opts) - opts = opts or {} - return deep_merge(defaults, opts) + opts = opts or {} + return deep_merge(defaults, opts) end --- Get default configuration ---@return CoderConfig Default configuration function M.get_defaults() - return vim.deepcopy(defaults) + return vim.deepcopy(defaults) end --- Validate configuration ---@param config CoderConfig Configuration to validate ---@return boolean, string? Valid status and optional error message function M.validate(config) - if not config.llm then - return false, "Missing LLM configuration" - end + if not config.llm then + return false, "Missing LLM configuration" + end - local valid_providers = { "ollama", "openai", "gemini", "copilot" } - local is_valid_provider = false - for _, p in ipairs(valid_providers) do - if config.llm.provider == p then - is_valid_provider = true - break - end - end + local valid_providers = { "ollama", "openai", "gemini", "copilot" } + local is_valid_provider = false + for _, p in ipairs(valid_providers) do + if config.llm.provider == p then + is_valid_provider = true + break + end + end - if not is_valid_provider then - return false, "Invalid LLM provider. Must be one of: " .. table.concat(valid_providers, ", ") - end + if not is_valid_provider then + return false, "Invalid LLM provider. Must be one of: " .. table.concat(valid_providers, ", ") + end - -- Validate provider-specific configuration - if config.llm.provider == "openai" then - local api_key = config.llm.openai.api_key or vim.env.OPENAI_API_KEY - if not api_key or api_key == "" then - return false, "OpenAI API key not configured. Set llm.openai.api_key or OPENAI_API_KEY env var" - end - elseif config.llm.provider == "gemini" then - local api_key = config.llm.gemini.api_key or vim.env.GEMINI_API_KEY - if not api_key or api_key == "" then - return false, "Gemini API key not configured. Set llm.gemini.api_key or GEMINI_API_KEY env var" - end - end - -- Note: copilot uses OAuth from copilot.lua/copilot.vim, validated at runtime - -- Note: ollama doesn't require API key, just host configuration + -- Validate provider-specific configuration + if config.llm.provider == "openai" then + local api_key = config.llm.openai.api_key or vim.env.OPENAI_API_KEY + if not api_key or api_key == "" then + return false, "OpenAI API key not configured. Set llm.openai.api_key or OPENAI_API_KEY env var" + end + elseif config.llm.provider == "gemini" then + local api_key = config.llm.gemini.api_key or vim.env.GEMINI_API_KEY + if not api_key or api_key == "" then + return false, "Gemini API key not configured. Set llm.gemini.api_key or GEMINI_API_KEY env var" + end + end + -- Note: copilot uses OAuth from copilot.lua/copilot.vim, validated at runtime + -- Note: ollama doesn't require API key, just host configuration - return true + return true end return M diff --git a/lua/codetyper/core/diff/patch.lua b/lua/codetyper/core/diff/patch.lua index 2d0b477..41a4ff2 100644 --- a/lua/codetyper/core/diff/patch.lua +++ b/lua/codetyper/core/diff/patch.lua @@ -233,8 +233,24 @@ function M.create_from_event(event, generated_code, confidence, strategy) local injection_strategy = strategy local injection_range = nil + -- Handle intent_override from transform-selection (e.g., cursor insert mode) + if event.intent_override and event.intent_override.action then + injection_strategy = event.intent_override.action + -- Use injection_range from transform-selection, not event.range + injection_range = event.injection_range or (event.range and { + start_line = event.range.start_line, + end_line = event.range.end_line, + }) + pcall(function() + local logs = require("codetyper.adapters.nvim.ui.logs") + logs.add({ + type = "info", + message = string.format("Using override strategy: %s (%s)", injection_strategy, + injection_range and (injection_range.start_line .. "-" .. injection_range.end_line) or "nil"), + }) + end) -- If we have SEARCH/REPLACE blocks, use that strategy - if use_search_replace then + elseif use_search_replace then injection_strategy = "search_replace" pcall(function() local logs = require("codetyper.adapters.nvim.ui.logs") @@ -519,14 +535,18 @@ local function remove_prompt_tags(bufnr) return removed end ---- Check if it's safe to modify the buffer (not in insert mode) +--- Check if it's safe to modify the buffer (not in insert or visual mode) ---@return boolean local function is_safe_to_modify() local mode = vim.fn.mode() - -- Don't modify if in insert mode or completion is visible + -- Don't modify if in insert mode, visual mode, or completion is visible if mode == "i" or mode == "ic" or mode == "ix" then return false end + -- Visual modes: v (char), V (line), \22 (block) + if mode == "v" or mode == "V" or mode == "\22" then + return false + end if vim.fn.pumvisible() == 1 then return false end @@ -540,9 +560,9 @@ end function M.apply(patch) logger.info("patch", string.format("apply() entered: id=%s strategy=%s has_range=%s", patch.id, tostring(patch.injection_strategy), patch.injection_range and "yes" or "no")) - -- Check if safe to modify (not in insert mode) + -- Check if safe to modify (not in insert or visual mode) if not is_safe_to_modify() then - logger.info("patch", "apply aborted: user_typing (insert mode or pum visible)") + logger.info("patch", "apply aborted: not safe (insert/visual mode or pum visible)") return false, "user_typing" end diff --git a/lua/codetyper/core/scheduler/scheduler.lua b/lua/codetyper/core/scheduler/scheduler.lua index 3511f02..df5721c 100644 --- a/lua/codetyper/core/scheduler/scheduler.lua +++ b/lua/codetyper/core/scheduler/scheduler.lua @@ -59,6 +59,13 @@ function M.is_insert_mode() return mode == "i" or mode == "ic" or mode == "ix" end +--- Check if we're in visual mode +---@return boolean +function M.is_visual_mode() + local mode = vim.fn.mode() + return mode == "v" or mode == "V" or mode == "\22" +end + --- Check if it's safe to inject code ---@return boolean ---@return string|nil reason if not safe @@ -71,6 +78,10 @@ function M.is_safe_to_inject() return false, "insert_mode" end + if M.is_visual_mode() then + return false, "visual_mode" + end + return true, nil end @@ -503,6 +514,13 @@ function M.schedule_patch_flush() if not waiting_to_flush then waiting_to_flush = true logger.info("scheduler", "Waiting for user to finish typing before applying code...") + -- Notify user about the wait + local utils = require("codetyper.support.utils") + if reason == "visual_mode" then + utils.notify("Queue waiting: exit Visual mode to inject code", vim.log.levels.INFO) + elseif reason == "insert_mode" then + utils.notify("Queue waiting: exit Insert mode to inject code", vim.log.levels.INFO) + end end -- Retry after a delay - keep waiting for user to finish typing M.schedule_patch_flush() @@ -549,6 +567,20 @@ local function setup_autocmds() desc = "Flush pending patches on InsertLeave", }) + -- Flush patches when leaving visual mode + vim.api.nvim_create_autocmd("ModeChanged", { + group = augroup, + pattern = "[vV\x16]*:*", -- visual mode to any other mode + callback = function() + vim.defer_fn(function() + if not M.is_insert_mode() and not M.is_completion_visible() then + patch.flush_pending_smart() + end + end, state.config.completion_delay_ms) + end, + desc = "Flush pending patches on VisualLeave", + }) + -- Flush patches on cursor hold vim.api.nvim_create_autocmd("CursorHold", { group = augroup, diff --git a/lua/codetyper/core/transform.lua b/lua/codetyper/core/transform.lua new file mode 100644 index 0000000..5cf16d5 --- /dev/null +++ b/lua/codetyper/core/transform.lua @@ -0,0 +1,224 @@ +local M = {} + +--- Return editor dimensions (from UI, like 99 plugin) +---@return number width +---@return number height +local function get_ui_dimensions() + local ui = vim.api.nvim_list_uis()[1] + if ui then + return ui.width, ui.height + end + return vim.o.columns, vim.o.lines +end + +--- Centered floating window config for prompt (2/3 width, 1/3 height) +---@return table { width, height, row, col, border } +local function create_centered_window() + local width, height = get_ui_dimensions() + local win_width = math.floor(width * 2 / 3) + local win_height = math.floor(height / 3) + return { + width = win_width, + height = win_height, + row = math.floor((height - win_height) / 2), + col = math.floor((width - win_width) / 2), + border = "rounded", + } +end + +--- Get visual selection text and range +---@return table|nil { text: string, start_line: number, end_line: number } +local function get_visual_selection() + local mode = vim.api.nvim_get_mode().mode + -- Check if in visual mode + local is_visual = mode == "v" or mode == "V" or mode == "\22" + if not is_visual then + return nil + end + -- Get selection range BEFORE any mode changes + local start_line = vim.fn.line("'<") + local end_line = vim.fn.line("'>") + -- Check if marks are valid (might be 0 if not in visual mode) + if start_line <= 0 or end_line <= 0 then + return nil + end + -- Third argument must be a Vim dictionary; empty Lua table can be treated as list + local opts = { type = mode } + local selection = vim.fn.getregion(vim.fn.getpos("'<"), vim.fn.getpos("'>"), opts) + local text = type(selection) == "table" and table.concat(selection, "\n") or tostring(selection or "") + return { + text = text, + start_line = start_line, + end_line = end_line, + } +end + +--- Transform visual selection with custom prompt input +--- Opens input window for prompt, processes selection on confirm. +--- When nothing is selected (e.g. from Normal mode), only the prompt is requested. +function M.cmd_transform_selection() + local logger = require("codetyper.support.logger") + logger.func_entry("commands", "cmd_transform_selection", {}) + -- Get visual selection (returns table with text, start_line, end_line or nil) + local selection_data = get_visual_selection() + local selection_text = selection_data and selection_data.text or "" + local has_selection = selection_text and #selection_text >= 4 + + local bufnr = vim.api.nvim_get_current_buf() + local filepath = vim.fn.expand("%:p") + local line_count = vim.api.nvim_buf_line_count(bufnr) + line_count = math.max(1, line_count) + + -- Range for injection: selection, cursor line when no selection + local start_line, end_line + local is_cursor_insert = false + if has_selection and selection_data then + start_line = selection_data.start_line + end_line = selection_data.end_line + logger.info("commands", string.format("Visual selection: start=%d end=%d selected_text_lines=%d", + start_line, end_line, #vim.split(selection_text, "\n", { plain = true }))) + else + -- No selection: insert at current cursor line (not replace whole file) + start_line = vim.fn.line(".") + end_line = start_line + is_cursor_insert = true + end + -- Clamp to valid 1-based range (avoid 0 or out-of-bounds) + start_line = math.max(1, math.min(start_line, line_count)) + end_line = math.max(1, math.min(end_line, line_count)) + if end_line < start_line then + end_line = start_line + end + + -- Capture injection range so we know exactly where to apply the generated code later + local injection_range = { start_line = start_line, end_line = end_line } + local range_line_count = end_line - start_line + 1 + + -- Open centered prompt window (pattern from 99: acwrite + BufWriteCmd to submit, BufLeave to keep focus) + local prompt_buf = vim.api.nvim_create_buf(false, true) + vim.bo[prompt_buf].buftype = "acwrite" + vim.bo[prompt_buf].bufhidden = "wipe" + vim.bo[prompt_buf].filetype = "markdown" + vim.bo[prompt_buf].swapfile = false + vim.api.nvim_buf_set_name(prompt_buf, "codetyper-prompt") + + local win_opts = create_centered_window() + local prompt_win = vim.api.nvim_open_win(prompt_buf, true, { + relative = "editor", + row = win_opts.row, + col = win_opts.col, + width = win_opts.width, + height = win_opts.height, + style = "minimal", + border = win_opts.border, + title = has_selection and " Enter prompt for selection " or " Enter prompt ", + title_pos = "center", + }) + vim.wo[prompt_win].wrap = true + vim.api.nvim_set_current_win(prompt_win) + + local function close_prompt() + if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then + vim.api.nvim_win_close(prompt_win, true) + end + if prompt_buf and vim.api.nvim_buf_is_valid(prompt_buf) then + vim.api.nvim_buf_delete(prompt_buf, { force = true }) + end + prompt_win = nil + prompt_buf = nil + end + + local submitted = false + + local function submit_prompt() + if not prompt_buf or not vim.api.nvim_buf_is_valid(prompt_buf) then + close_prompt() + return + end + submitted = true + local lines = vim.api.nvim_buf_get_lines(prompt_buf, 0, -1, false) + local input = table.concat(lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "") + close_prompt() + if input == "" then + logger.info("commands", "User cancelled prompt input") + return + end + local content + if has_selection then + content = input .. "\n\nCode to replace (replace this code):\n" .. selection_text + elseif is_cursor_insert then + content = "Insert at line " .. start_line .. ":\n" .. input + else + content = input + end + -- Pass captured range so scheduler/patch know where to inject the generated code + local prompt = { + content = content, + start_line = injection_range.start_line, + end_line = injection_range.end_line, + start_col = 1, + end_col = 1, + user_prompt = input, + -- Explicit injection range (same as start_line/end_line) for downstream + injection_range = injection_range, + -- When there's a selection, force replace; when no selection, insert at cursor + intent_override = has_selection and { action = "replace" } or (is_cursor_insert and { action = "insert" } or nil), + } + local autocmds = require("codetyper.adapters.nvim.autocmds") + autocmds.process_single_prompt(bufnr, prompt, filepath, true) + end + + local augroup = vim.api.nvim_create_augroup("CodetyperPrompt_" .. prompt_buf, { clear = true }) + + -- Submit on :w (acwrite buffer triggers BufWriteCmd) + vim.api.nvim_create_autocmd("BufWriteCmd", { + group = augroup, + buffer = prompt_buf, + callback = function() + if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then + submitted = true + submit_prompt() + end + end, + }) + + -- Keep focus in prompt window (prevent leaving to other buffers) + vim.api.nvim_create_autocmd("BufLeave", { + group = augroup, + buffer = prompt_buf, + callback = function() + if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then + vim.api.nvim_set_current_win(prompt_win) + end + end, + }) + + -- Clean up when window is closed (e.g. :q or close button) + vim.api.nvim_create_autocmd("WinClosed", { + group = augroup, + pattern = tostring(prompt_win), + callback = function() + if not submitted then + logger.info("commands", "User cancelled prompt input") + end + close_prompt() + end, + }) + + local map_opts = { buffer = prompt_buf, noremap = true, silent = true } + -- Normal mode: Enter, :w, or Ctrl+Enter to submit + vim.keymap.set("n", "", submit_prompt, map_opts) + vim.keymap.set("n", "", submit_prompt, map_opts) + vim.keymap.set("n", "", submit_prompt, map_opts) + vim.keymap.set("n", "w", "w", vim.tbl_extend("force", map_opts, { desc = "Submit prompt" })) + -- Insert mode: Ctrl+Enter to submit + vim.keymap.set("i", "", submit_prompt, map_opts) + vim.keymap.set("i", "", submit_prompt, map_opts) + -- Close/cancel: Esc (in normal), q, or :q + vim.keymap.set("n", "", close_prompt, map_opts) + vim.keymap.set("n", "q", close_prompt, map_opts) + + vim.cmd("startinsert") +end + +return M diff --git a/lua/codetyper/init.lua b/lua/codetyper/init.lua index 5d9d32c..b7f9908 100644 --- a/lua/codetyper/init.lua +++ b/lua/codetyper/init.lua @@ -18,66 +18,66 @@ M._initialized = false --- Setup the plugin with user configuration ---@param opts? CoderConfig User configuration options function M.setup(opts) - if M._initialized then - return - end + if M._initialized then + return + end - local config = require("codetyper.config.defaults") - M.config = config.setup(opts) + local config = require("codetyper.config.defaults") + M.config = config.setup(opts) - -- Initialize modules - local commands = require("codetyper.adapters.nvim.commands") - local gitignore = require("codetyper.support.gitignore") - local autocmds = require("codetyper.adapters.nvim.autocmds") - local tree = require("codetyper.support.tree") - local completion = require("codetyper.features.completion.inline") + -- Initialize modules + local commands = require("codetyper.adapters.nvim.commands") + local gitignore = require("codetyper.support.gitignore") + local autocmds = require("codetyper.adapters.nvim.autocmds") + local tree = require("codetyper.support.tree") + local completion = require("codetyper.features.completion.inline") - -- Register commands - commands.setup() + -- Register commands + commands.setup() - -- Setup autocommands - autocmds.setup() + -- Setup autocommands + autocmds.setup() - -- Setup file reference completion - completion.setup() + -- Setup file reference completion + completion.setup() - -- Ensure .gitignore has coder files excluded - gitignore.ensure_ignored() + -- Ensure .gitignore has coder files excluded + gitignore.ensure_ignored() - -- Initialize tree logging (creates .coder folder and initial tree.log) - tree.setup() + -- Initialize tree logging (creates .coder folder and initial tree.log) + tree.setup() - -- Initialize project indexer if enabled - if M.config.indexer and M.config.indexer.enabled then - local indexer = require("codetyper.features.indexer") - indexer.setup(M.config.indexer) - end + -- Initialize project indexer if enabled + if M.config.indexer and M.config.indexer.enabled then + local indexer = require("codetyper.features.indexer") + indexer.setup(M.config.indexer) + end - -- Initialize brain learning system if enabled - if M.config.brain and M.config.brain.enabled then - local brain = require("codetyper.core.memory") - brain.setup(M.config.brain) - end + -- Initialize brain learning system if enabled + if M.config.brain and M.config.brain.enabled then + local brain = require("codetyper.core.memory") + brain.setup(M.config.brain) + end - -- Setup inline ghost text suggestions (Copilot-style) - if M.config.suggestion and M.config.suggestion.enabled then - local suggestion = require("codetyper.features.completion.suggestion") - suggestion.setup(M.config.suggestion) - end + -- Setup inline ghost text suggestions (Copilot-style) + if M.config.suggestion and M.config.suggestion.enabled then + local suggestion = require("codetyper.features.completion.suggestion") + suggestion.setup(M.config.suggestion) + end - M._initialized = true + M._initialized = true end --- Get current configuration ---@return CoderConfig function M.get_config() - return M.config + return M.config end --- Check if plugin is initialized ---@return boolean function M.is_initialized() - return M._initialized + return M._initialized end return M diff --git a/tests/spec/config_spec.lua b/tests/spec/config_spec.lua index 24606b6..4d0207f 100644 --- a/tests/spec/config_spec.lua +++ b/tests/spec/config_spec.lua @@ -12,18 +12,6 @@ describe("config", function() assert.equals("claude", defaults.llm.provider) end) - it("should have window configuration", function() - assert.is_table(defaults.window) - assert.equals(25, defaults.window.width) - assert.equals("left", defaults.window.position) - end) - - it("should have pattern configuration", function() - assert.is_table(defaults.patterns) - assert.equals("/@", defaults.patterns.open_tag) - assert.equals("@/", defaults.patterns.close_tag) - end) - it("should have scheduler configuration", function() assert.is_table(defaults.scheduler) assert.is_boolean(defaults.scheduler.enabled)