Files
codetyper.nvim/lua/codetyper/inject.lua
Carlos Gutierrez e57209a1f8 feat: adding multiple files
### 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
2026-03-18 23:05:26 -04:00

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