### Added
- **Smart Scope Resolution** — Tree-sitter + indentation context for
selections
- `resolve_selection_context()` in `scope/init.lua` handles partial
functions,
whole functions, multi-function spans, indent blocks, and whole-file
selections
- Enclosing function automatically sent as context when selecting code
inside one
- Whole-file selection (>=80% of lines) triggers project tree as
context
- Indentation-based fallback when Tree-sitter is unavailable
- **Explain-to-Document Intent** — "explain" prompts generate
documentation
- Detects prompts like "explain this", "tell me about", "what does",
"question"
- Generates documentation comments and inserts them above selected
code
- Shows notification if nothing is selected
- Updated intent action from "none" to "insert" for explain intent
- **Granular LLM Status Notifications** — Real-time progress reporting
- Inline virtual text and floating status window show current stage
- Stages: "Reading context...", "Searching index...", "Gathering
context...",
"Recalling patterns...", "Building prompt...", "Sending to
[provider]...",
"Processing response...", "Generating patch...", "Applying code..."
- `update_inline_status()` in `thinking_placeholder.lua`
- `update_stage()` in `thinking.lua`
- **Thinking Placeholder Positioning** — "Implementing..." appears above
selection
- Uses `virt_lines_above = true` on extmark at selection start line
- Dynamic status text updates during LLM processing
### Changed
- **Providers reduced to Copilot and Ollama only**
- Removed Claude, OpenAI, and Gemini provider integrations
- Deleted `llm/openai.lua` and `llm/gemini.lua`
- Cleaned `llm/init.lua`, `config/defaults.lua`, `types.lua`,
`credentials.lua`,
`cost/init.lua`, and `events/queue.lua` of all references
- `valid_providers` now only includes "copilot" and "ollama"
- **Removed timer-based delayed processing** — Prompts are processed
instantly
- Removed `timer` field, `timeout_ms`, and timer setup/cancellation
from `worker.lua`
- **Removed chat/agent/split window UI**
- Deleted `ui/chat.lua`, `windows.lua`, `ui/switcher.lua`
- Removed `CoderOpen`, `CoderClose`, `CoderToggle` commands
- Removed window management from `autocmds.lua`, `inject.lua`,
`executor.lua`
- Removed auto-open companion file logic
- **Commands removed from menu** (code retained with TODOs for
re-enabling)
- `CoderAddApiKey`, `CoderRemoveApiKey`, `CoderBrain`,
`CoderFeedback`,
`CoderMemories`, `CoderForget`, `CoderProcess`
- Subcommands `process`, `status`, `memories`, `forget`,
`llm-feedback-good`,
`llm-feedback-bad`, `add-api-key`, `remove-api-key` removed from
completion
### Fixed
- Fixed `patch.lua` syntax error — missing `if` wrapper around
SEARCH/REPLACE block
- Fixed `CoderModel` require path typo
(`codetyper.adapters.config.credentials`
→ `codetyper.config.credentials`)
- Fixed `thinking_placeholder` extmark placement appearing after
selection
instead of above it
267 lines
8.1 KiB
Lua
267 lines
8.1 KiB
Lua
---@mod codetyper.inject Code injection for Codetyper.nvim
|
|
|
|
local M = {}
|
|
|
|
local utils = require("codetyper.support.utils")
|
|
|
|
--- Inject generated code into target file
|
|
---@param target_path string Path to target file
|
|
---@param code string Generated code
|
|
---@param prompt_type string Type of prompt (refactor, add, document, etc.)
|
|
function M.inject_code(target_path, code, prompt_type)
|
|
-- Normalize the target path
|
|
target_path = vim.fn.fnamemodify(target_path, ":p")
|
|
|
|
-- Try to find buffer by path
|
|
local target_buf = nil
|
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
|
local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":p")
|
|
if buf_name == target_path then
|
|
target_buf = buf
|
|
break
|
|
end
|
|
end
|
|
|
|
-- If still not found, open the file
|
|
if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then
|
|
-- Check if file exists
|
|
if utils.file_exists(target_path) then
|
|
vim.cmd("edit " .. vim.fn.fnameescape(target_path))
|
|
target_buf = vim.api.nvim_get_current_buf()
|
|
utils.notify("Opened target file: " .. vim.fn.fnamemodify(target_path, ":t"))
|
|
else
|
|
utils.notify("Target file not found: " .. target_path, vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
end
|
|
|
|
if not target_buf then
|
|
utils.notify("Target buffer not found", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
utils.notify("Injecting code into: " .. vim.fn.fnamemodify(target_path, ":t"))
|
|
|
|
-- Different injection strategies based on prompt type
|
|
if prompt_type == "refactor" then
|
|
M.inject_refactor(target_buf, code)
|
|
elseif prompt_type == "add" then
|
|
M.inject_add(target_buf, code)
|
|
elseif prompt_type == "document" then
|
|
M.inject_document(target_buf, code)
|
|
else
|
|
-- For generic, auto-add instead of prompting
|
|
M.inject_add(target_buf, code)
|
|
end
|
|
|
|
-- Mark buffer as modified and save
|
|
vim.bo[target_buf].modified = true
|
|
|
|
-- Auto-save the target file
|
|
vim.schedule(function()
|
|
if vim.api.nvim_buf_is_valid(target_buf) then
|
|
local wins = vim.fn.win_findbuf(target_buf)
|
|
if #wins > 0 then
|
|
vim.api.nvim_win_call(wins[1], function()
|
|
vim.cmd("silent! write")
|
|
end)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Inject code with strategy and range (used by patch system)
|
|
---@param bufnr number Buffer number
|
|
---@param code string Generated code
|
|
---@param opts table|nil { strategy = "replace"|"insert"|"append", range = { start_line, end_line } (1-based) }
|
|
---@return table { imports_added: number, body_lines: number, imports_merged: boolean }
|
|
function M.inject(bufnr, code, opts)
|
|
opts = opts or {}
|
|
local strategy = opts.strategy or "replace"
|
|
local range = opts.range
|
|
local lines = vim.split(code, "\n", { plain = true })
|
|
local body_lines = #lines
|
|
|
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
|
return { imports_added = 0, body_lines = 0, imports_merged = false }
|
|
end
|
|
|
|
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
|
|
|
if strategy == "replace" and range and range.start_line and range.end_line then
|
|
local start_0 = math.max(0, range.start_line - 1)
|
|
local end_0 = math.min(line_count, range.end_line)
|
|
if end_0 < start_0 then
|
|
end_0 = start_0
|
|
end
|
|
vim.api.nvim_buf_set_lines(bufnr, start_0, end_0, false, lines)
|
|
elseif strategy == "insert" and range and range.start_line then
|
|
local at_0 = math.max(0, math.min(range.start_line - 1, line_count))
|
|
vim.api.nvim_buf_set_lines(bufnr, at_0, at_0, false, lines)
|
|
else
|
|
-- append
|
|
vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, lines)
|
|
end
|
|
|
|
return { imports_added = 0, body_lines = body_lines, imports_merged = false }
|
|
end
|
|
|
|
--- Inject code for refactor (replace entire file)
|
|
---@param bufnr number Buffer number
|
|
---@param code string Generated code
|
|
function M.inject_refactor(bufnr, code)
|
|
local lines = vim.split(code, "\n", { plain = true })
|
|
|
|
-- Save cursor position
|
|
local cursor = nil
|
|
local wins = vim.fn.win_findbuf(bufnr)
|
|
if #wins > 0 then
|
|
cursor = vim.api.nvim_win_get_cursor(wins[1])
|
|
end
|
|
|
|
-- Replace buffer content
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
|
|
-- Restore cursor position if possible
|
|
if cursor then
|
|
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
|
cursor[1] = math.min(cursor[1], line_count)
|
|
pcall(vim.api.nvim_win_set_cursor, wins[1], cursor)
|
|
end
|
|
|
|
utils.notify("Code refactored", vim.log.levels.INFO)
|
|
end
|
|
|
|
--- Inject code for add (append at cursor or end)
|
|
---@param bufnr number Buffer number
|
|
---@param code string Generated code
|
|
function M.inject_add(bufnr, code)
|
|
local lines = vim.split(code, "\n", { plain = true })
|
|
|
|
-- Try to find a window displaying this buffer to get cursor position
|
|
local insert_line
|
|
local wins = vim.fn.win_findbuf(bufnr)
|
|
if #wins > 0 then
|
|
local cursor = vim.api.nvim_win_get_cursor(wins[1])
|
|
insert_line = cursor[1]
|
|
else
|
|
insert_line = vim.api.nvim_buf_line_count(bufnr)
|
|
end
|
|
|
|
-- Insert lines at position
|
|
vim.api.nvim_buf_set_lines(bufnr, insert_line, insert_line, false, lines)
|
|
|
|
utils.notify("Code added at line " .. (insert_line + 1), vim.log.levels.INFO)
|
|
end
|
|
|
|
--- Inject documentation
|
|
---@param bufnr number Buffer number
|
|
---@param code string Generated documentation
|
|
function M.inject_document(bufnr, code)
|
|
-- Documentation typically goes above the current function/class
|
|
-- For simplicity, insert at cursor position
|
|
M.inject_add(bufnr, code)
|
|
utils.notify("Documentation added", vim.log.levels.INFO)
|
|
end
|
|
|
|
--- Generic injection (prompt user for action)
|
|
---@param bufnr number Buffer number
|
|
---@param code string Generated code
|
|
function M.inject_generic(bufnr, code)
|
|
local actions = {
|
|
"Replace entire file",
|
|
"Insert at cursor",
|
|
"Append to end",
|
|
"Copy to clipboard",
|
|
"Cancel",
|
|
}
|
|
|
|
vim.ui.select(actions, {
|
|
prompt = "How to inject the generated code?",
|
|
}, function(choice)
|
|
if not choice then
|
|
return
|
|
end
|
|
|
|
if choice == "Replace entire file" then
|
|
M.inject_refactor(bufnr, code)
|
|
elseif choice == "Insert at cursor" then
|
|
M.inject_add(bufnr, code)
|
|
elseif choice == "Append to end" then
|
|
local lines = vim.split(code, "\n", { plain = true })
|
|
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
|
vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, lines)
|
|
utils.notify("Code appended to end", vim.log.levels.INFO)
|
|
elseif choice == "Copy to clipboard" then
|
|
vim.fn.setreg("+", code)
|
|
utils.notify("Code copied to clipboard", vim.log.levels.INFO)
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Preview code in a floating window before injection
|
|
---@param code string Generated code
|
|
---@param callback fun(action: string) Callback with selected action
|
|
function M.preview(code, callback)
|
|
local codetyper = require("codetyper")
|
|
local config = codetyper.get_config()
|
|
|
|
local lines = vim.split(code, "\n", { plain = true })
|
|
|
|
-- Create buffer for preview
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
|
|
|
-- Calculate window size
|
|
local width = math.min(80, vim.o.columns - 10)
|
|
local height = math.min(#lines + 2, vim.o.lines - 10)
|
|
|
|
-- Create floating window
|
|
local win = vim.api.nvim_open_win(buf, true, {
|
|
relative = "editor",
|
|
width = width,
|
|
height = height,
|
|
row = math.floor((vim.o.lines - height) / 2),
|
|
col = math.floor((vim.o.columns - width) / 2),
|
|
style = "minimal",
|
|
border = config.window.border,
|
|
title = " Generated Code Preview ",
|
|
title_pos = "center",
|
|
})
|
|
|
|
-- Set buffer options
|
|
vim.bo[buf].modifiable = false
|
|
vim.bo[buf].bufhidden = "wipe"
|
|
|
|
-- Add keymaps for actions
|
|
local opts = { buffer = buf, noremap = true, silent = true }
|
|
|
|
vim.keymap.set("n", "q", function()
|
|
vim.api.nvim_win_close(win, true)
|
|
callback("cancel")
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<CR>", function()
|
|
vim.api.nvim_win_close(win, true)
|
|
callback("inject")
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "y", function()
|
|
vim.fn.setreg("+", code)
|
|
utils.notify("Copied to clipboard")
|
|
end, opts)
|
|
|
|
-- Show help in command line
|
|
vim.api.nvim_echo({
|
|
{ "Press ", "Normal" },
|
|
{ "<CR>", "Keyword" },
|
|
{ " to inject, ", "Normal" },
|
|
{ "y", "Keyword" },
|
|
{ " to copy, ", "Normal" },
|
|
{ "q", "Keyword" },
|
|
{ " to cancel", "Normal" },
|
|
}, false, {})
|
|
end
|
|
|
|
return M
|