Adding the functionallity and refactoring
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 <c-v>
|
||||
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", "<CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<C-CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<C-Enter>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<leader>w", "<cmd>w<cr>", vim.tbl_extend("force", map_opts, { desc = "Submit prompt" }))
|
||||
-- Insert mode: Ctrl+Enter to submit
|
||||
vim.keymap.set("i", "<C-CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("i", "<C-Enter>", submit_prompt, map_opts)
|
||||
-- Close/cancel: Esc (in normal), q, or :q
|
||||
vim.keymap.set("n", "<Esc>", 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", "<leader>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", "<leader>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
|
||||
|
||||
|
||||
@@ -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 = "<Tab>", -- Accept suggestion
|
||||
next = "<M-]>", -- Next suggestion (Alt+])
|
||||
prev = "<M-[>", -- Previous suggestion (Alt+[)
|
||||
dismiss = "<C-]>", -- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
224
lua/codetyper/core/transform.lua
Normal file
224
lua/codetyper/core/transform.lua
Normal file
@@ -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", "<CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<C-CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<C-Enter>", submit_prompt, map_opts)
|
||||
vim.keymap.set("n", "<leader>w", "<cmd>w<cr>", vim.tbl_extend("force", map_opts, { desc = "Submit prompt" }))
|
||||
-- Insert mode: Ctrl+Enter to submit
|
||||
vim.keymap.set("i", "<C-CR>", submit_prompt, map_opts)
|
||||
vim.keymap.set("i", "<C-Enter>", submit_prompt, map_opts)
|
||||
-- Close/cancel: Esc (in normal), q, or :q
|
||||
vim.keymap.set("n", "<Esc>", close_prompt, map_opts)
|
||||
vim.keymap.set("n", "q", close_prompt, map_opts)
|
||||
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user