AI-powered coding partner for Neovim with LLM integration. Features: - Split view for coder files (*.coder.*) and target files - Tag-based prompts with /@ and @/ syntax - Claude API and Ollama (local) LLM support - Smart prompt detection (refactor, add, document, explain) - Automatic code injection into target files - Project tree logging (.coder/tree.log) - Auto .gitignore management Ask Panel (chat interface): - Fixed at 1/4 screen width - File attachment with @ key - Ctrl+n for new chat - Ctrl+Enter to submit - Proper window close behavior - Navigation with Ctrl+h/j/k/l Commands: Coder, CoderOpen, CoderClose, CoderToggle, CoderProcess, CoderAsk, CoderTree, CoderTreeView
867 lines
26 KiB
Lua
867 lines
26 KiB
Lua
---@mod codetyper.ask Ask window for Codetyper.nvim (similar to avante.nvim)
|
|
|
|
local M = {}
|
|
|
|
local utils = require("codetyper.utils")
|
|
|
|
---@class AskState
|
|
---@field input_buf number|nil Input buffer
|
|
---@field input_win number|nil Input window
|
|
---@field output_buf number|nil Output buffer
|
|
---@field output_win number|nil Output window
|
|
---@field is_open boolean Whether the ask panel is open
|
|
---@field history table Chat history
|
|
---@field referenced_files table Files referenced with @
|
|
|
|
---@type AskState
|
|
local state = {
|
|
input_buf = nil,
|
|
input_win = nil,
|
|
output_buf = nil,
|
|
output_win = nil,
|
|
is_open = false,
|
|
history = {},
|
|
referenced_files = {},
|
|
target_width = nil, -- Store the target width to maintain it
|
|
}
|
|
|
|
--- Get the ask window configuration
|
|
---@return table Config
|
|
local function get_config()
|
|
local ok, codetyper = pcall(require, "codetyper")
|
|
if ok and codetyper.is_initialized() then
|
|
return codetyper.get_config()
|
|
end
|
|
return {
|
|
window = { width = 0.4, border = "rounded" },
|
|
}
|
|
end
|
|
|
|
--- Create the output buffer (chat history)
|
|
---@return number Buffer number
|
|
local function create_output_buffer()
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
|
|
vim.bo[buf].buftype = "nofile"
|
|
vim.bo[buf].bufhidden = "hide"
|
|
vim.bo[buf].swapfile = false
|
|
vim.bo[buf].filetype = "markdown"
|
|
|
|
-- Set initial content
|
|
local header = {
|
|
"╔═══════════════════════════════════╗",
|
|
"║ 🤖 CODETYPER ASK ║",
|
|
"╠═══════════════════════════════════╣",
|
|
"║ Ask about code or concepts ║",
|
|
"║ ║",
|
|
"║ 💡 Keymaps: ║",
|
|
"║ @ → attach file ║",
|
|
"║ C-Enter → send ║",
|
|
"║ C-n → new chat ║",
|
|
"║ C-f → add current file ║",
|
|
"║ C-h/j/k/l → navigate ║",
|
|
"║ q → close │ K/J → jump ║",
|
|
"╚═══════════════════════════════════╝",
|
|
"",
|
|
}
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, header)
|
|
|
|
return buf
|
|
end
|
|
|
|
--- Create the input buffer
|
|
---@return number Buffer number
|
|
local function create_input_buffer()
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
|
|
vim.bo[buf].buftype = "nofile"
|
|
vim.bo[buf].bufhidden = "hide"
|
|
vim.bo[buf].swapfile = false
|
|
vim.bo[buf].filetype = "markdown"
|
|
|
|
-- Set placeholder text
|
|
local placeholder = {
|
|
"┌───────────────────────────────────┐",
|
|
"│ 💬 Type your question here... │",
|
|
"│ │",
|
|
"│ @ attach │ C-Enter send │ C-n new │",
|
|
"└───────────────────────────────────┘",
|
|
}
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, placeholder)
|
|
|
|
return buf
|
|
end
|
|
|
|
--- Setup keymaps for the input buffer
|
|
---@param buf number Buffer number
|
|
local function setup_input_keymaps(buf)
|
|
local opts = { buffer = buf, noremap = true, silent = true }
|
|
|
|
-- Submit with Ctrl+Enter
|
|
vim.keymap.set("i", "<C-CR>", function()
|
|
M.submit()
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<C-CR>", function()
|
|
M.submit()
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<CR>", function()
|
|
M.submit()
|
|
end, opts)
|
|
|
|
-- Include current file context with Ctrl+F
|
|
vim.keymap.set({ "n", "i" }, "<C-f>", function()
|
|
M.include_file_context()
|
|
end, opts)
|
|
|
|
-- File picker with @
|
|
vim.keymap.set("i", "@", function()
|
|
M.show_file_picker()
|
|
end, opts)
|
|
|
|
-- Close with q in normal mode
|
|
vim.keymap.set("n", "q", function()
|
|
M.close()
|
|
end, opts)
|
|
|
|
-- Clear input with Ctrl+c
|
|
vim.keymap.set("n", "<C-c>", function()
|
|
M.clear_input()
|
|
end, opts)
|
|
|
|
-- New chat with Ctrl+n (clears everything)
|
|
vim.keymap.set({ "n", "i" }, "<C-n>", function()
|
|
M.new_chat()
|
|
end, opts)
|
|
|
|
-- Window navigation (works in both normal and insert mode)
|
|
vim.keymap.set({ "n", "i" }, "<C-h>", function()
|
|
vim.cmd("wincmd h")
|
|
end, opts)
|
|
|
|
vim.keymap.set({ "n", "i" }, "<C-j>", function()
|
|
vim.cmd("wincmd j")
|
|
end, opts)
|
|
|
|
vim.keymap.set({ "n", "i" }, "<C-k>", function()
|
|
vim.cmd("wincmd k")
|
|
end, opts)
|
|
|
|
vim.keymap.set({ "n", "i" }, "<C-l>", function()
|
|
vim.cmd("wincmd l")
|
|
end, opts)
|
|
|
|
-- Jump to output window
|
|
vim.keymap.set("n", "K", function()
|
|
M.focus_output()
|
|
end, opts)
|
|
|
|
-- When entering insert mode, clear placeholder
|
|
vim.api.nvim_create_autocmd("InsertEnter", {
|
|
buffer = buf,
|
|
callback = function()
|
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
|
local content = table.concat(lines, "\n")
|
|
if content:match("Type your question here") then
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "" })
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
--- Setup keymaps for the output buffer
|
|
---@param buf number Buffer number
|
|
local function setup_output_keymaps(buf)
|
|
local opts = { buffer = buf, noremap = true, silent = true }
|
|
|
|
-- Close with q
|
|
vim.keymap.set("n", "q", function()
|
|
M.close()
|
|
end, opts)
|
|
|
|
-- Clear history with Ctrl+c
|
|
vim.keymap.set("n", "<C-c>", function()
|
|
M.clear_history()
|
|
end, opts)
|
|
|
|
-- New chat with Ctrl+n (clears everything)
|
|
vim.keymap.set("n", "<C-n>", function()
|
|
M.new_chat()
|
|
end, opts)
|
|
|
|
-- Copy last response with Y
|
|
vim.keymap.set("n", "Y", function()
|
|
M.copy_last_response()
|
|
end, opts)
|
|
|
|
-- Jump to input with i or J
|
|
vim.keymap.set("n", "i", function()
|
|
M.focus_input()
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "J", function()
|
|
M.focus_input()
|
|
end, opts)
|
|
|
|
-- Window navigation
|
|
vim.keymap.set("n", "<C-h>", function()
|
|
vim.cmd("wincmd h")
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<C-j>", function()
|
|
vim.cmd("wincmd j")
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<C-k>", function()
|
|
vim.cmd("wincmd k")
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "<C-l>", function()
|
|
vim.cmd("wincmd l")
|
|
end, opts)
|
|
|
|
end
|
|
|
|
--- Calculate window dimensions (always 1/4 of screen)
|
|
---@return table Dimensions
|
|
local function calculate_dimensions()
|
|
-- Always use 1/4 of the screen width
|
|
local width = math.floor(vim.o.columns * 0.25)
|
|
|
|
return {
|
|
width = math.max(width, 30), -- Minimum 30 columns
|
|
total_height = vim.o.lines - 4,
|
|
output_height = vim.o.lines - 14,
|
|
input_height = 8,
|
|
}
|
|
end
|
|
|
|
--- Autocmd group for maintaining width
|
|
local ask_augroup = nil
|
|
|
|
--- Setup autocmd to always maintain 1/4 window width
|
|
local function setup_width_autocmd()
|
|
-- Clear previous autocmd group if exists
|
|
if ask_augroup then
|
|
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
|
end
|
|
|
|
ask_augroup = vim.api.nvim_create_augroup("CodetypeAskWidth", { clear = true })
|
|
|
|
-- Always maintain 1/4 width on any window event
|
|
vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, {
|
|
group = ask_augroup,
|
|
callback = function()
|
|
if not state.is_open or not state.output_win then return end
|
|
if not vim.api.nvim_win_is_valid(state.output_win) then return end
|
|
|
|
vim.schedule(function()
|
|
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
|
-- Always calculate 1/4 of current screen width
|
|
local target_width = math.max(math.floor(vim.o.columns * 0.25), 30)
|
|
state.target_width = target_width
|
|
|
|
local current_width = vim.api.nvim_win_get_width(state.output_win)
|
|
if current_width ~= target_width then
|
|
pcall(vim.api.nvim_win_set_width, state.output_win, target_width)
|
|
end
|
|
end
|
|
end)
|
|
end,
|
|
desc = "Maintain Ask panel at 1/4 window width",
|
|
})
|
|
end
|
|
|
|
--- Open the ask panel
|
|
function M.open()
|
|
-- Use the is_open() function which validates window state
|
|
if M.is_open() then
|
|
M.focus_input()
|
|
return
|
|
end
|
|
|
|
local dims = calculate_dimensions()
|
|
|
|
-- Store the target width
|
|
state.target_width = dims.width
|
|
|
|
-- Create buffers if they don't exist
|
|
if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then
|
|
state.output_buf = create_output_buffer()
|
|
setup_output_keymaps(state.output_buf)
|
|
end
|
|
|
|
if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then
|
|
state.input_buf = create_input_buffer()
|
|
setup_input_keymaps(state.input_buf)
|
|
end
|
|
|
|
-- Save current window to return to it later
|
|
local current_win = vim.api.nvim_get_current_win()
|
|
|
|
-- Create output window (top-left)
|
|
vim.cmd("topleft vsplit")
|
|
state.output_win = vim.api.nvim_get_current_win()
|
|
vim.api.nvim_win_set_buf(state.output_win, state.output_buf)
|
|
vim.api.nvim_win_set_width(state.output_win, dims.width)
|
|
|
|
-- Window options for output
|
|
vim.wo[state.output_win].number = false
|
|
vim.wo[state.output_win].relativenumber = false
|
|
vim.wo[state.output_win].signcolumn = "no"
|
|
vim.wo[state.output_win].wrap = true
|
|
vim.wo[state.output_win].linebreak = true
|
|
vim.wo[state.output_win].cursorline = false
|
|
vim.wo[state.output_win].winfixwidth = true
|
|
|
|
-- Create input window (bottom of the left panel)
|
|
vim.cmd("belowright split")
|
|
state.input_win = vim.api.nvim_get_current_win()
|
|
vim.api.nvim_win_set_buf(state.input_win, state.input_buf)
|
|
vim.api.nvim_win_set_height(state.input_win, dims.input_height)
|
|
|
|
-- Window options for input
|
|
vim.wo[state.input_win].number = false
|
|
vim.wo[state.input_win].relativenumber = false
|
|
vim.wo[state.input_win].signcolumn = "no"
|
|
vim.wo[state.input_win].wrap = true
|
|
vim.wo[state.input_win].linebreak = true
|
|
vim.wo[state.input_win].winfixheight = true
|
|
vim.wo[state.input_win].winfixwidth = true
|
|
|
|
state.is_open = true
|
|
|
|
-- Setup autocmd to maintain width
|
|
setup_width_autocmd()
|
|
|
|
-- Setup autocmd to close both windows when one is closed
|
|
local close_group = vim.api.nvim_create_augroup("CodetypeAskClose", { clear = true })
|
|
|
|
vim.api.nvim_create_autocmd("WinClosed", {
|
|
group = close_group,
|
|
callback = function(args)
|
|
local closed_win = tonumber(args.match)
|
|
-- Check if one of our windows was closed
|
|
if closed_win == state.output_win or closed_win == state.input_win then
|
|
-- Defer to avoid issues during window close
|
|
vim.schedule(function()
|
|
-- Close the other window if it's still open
|
|
if closed_win == state.output_win then
|
|
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
|
pcall(vim.api.nvim_win_close, state.input_win, true)
|
|
end
|
|
elseif closed_win == state.input_win then
|
|
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
|
pcall(vim.api.nvim_win_close, state.output_win, true)
|
|
end
|
|
end
|
|
|
|
-- Reset state
|
|
state.input_win = nil
|
|
state.output_win = nil
|
|
state.is_open = false
|
|
state.target_width = nil
|
|
|
|
-- Clean up autocmd groups
|
|
pcall(vim.api.nvim_del_augroup_by_id, close_group)
|
|
if ask_augroup then
|
|
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
|
ask_augroup = nil
|
|
end
|
|
end)
|
|
end
|
|
end,
|
|
desc = "Close both Ask windows together",
|
|
})
|
|
|
|
-- Focus the input window and start insert mode
|
|
vim.api.nvim_set_current_win(state.input_win)
|
|
vim.cmd("startinsert")
|
|
end
|
|
|
|
--- Show file picker for @ mentions
|
|
function M.show_file_picker()
|
|
-- Check if telescope is available
|
|
local has_telescope, telescope = pcall(require, "telescope.builtin")
|
|
|
|
if has_telescope then
|
|
telescope.find_files({
|
|
prompt_title = "Select file to reference (@)",
|
|
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 filepath = selection.path or selection[1]
|
|
local filename = vim.fn.fnamemodify(filepath, ":t")
|
|
M.add_file_reference(filepath, filename)
|
|
end
|
|
end)
|
|
return true
|
|
end,
|
|
})
|
|
else
|
|
-- Fallback: simple input
|
|
vim.ui.input({ prompt = "Enter file path: " }, function(input)
|
|
if input and input ~= "" then
|
|
local filepath = vim.fn.fnamemodify(input, ":p")
|
|
local filename = vim.fn.fnamemodify(filepath, ":t")
|
|
M.add_file_reference(filepath, filename)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
--- Add a file reference to the input
|
|
---@param filepath string Full path to the file
|
|
---@param filename string Display name
|
|
function M.add_file_reference(filepath, filename)
|
|
-- Normalize filepath
|
|
filepath = vim.fn.fnamemodify(filepath, ":p")
|
|
|
|
-- Store the reference with full path
|
|
state.referenced_files[filename] = filepath
|
|
|
|
-- Read and validate file exists
|
|
local content = utils.read_file(filepath)
|
|
if not content then
|
|
utils.notify("Warning: Could not read file: " .. filename, vim.log.levels.WARN)
|
|
end
|
|
|
|
-- Add to input buffer
|
|
if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then
|
|
local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false)
|
|
local text = table.concat(lines, "\n")
|
|
|
|
-- Clear placeholder if present
|
|
if text:match("Type your question here") then
|
|
text = ""
|
|
end
|
|
|
|
-- Add file reference (with single @)
|
|
local reference = "[📎 " .. filename .. "] "
|
|
text = text .. reference
|
|
|
|
vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, vim.split(text, "\n"))
|
|
|
|
-- Move cursor to end
|
|
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
|
vim.api.nvim_set_current_win(state.input_win)
|
|
local line_count = vim.api.nvim_buf_line_count(state.input_buf)
|
|
local last_line = vim.api.nvim_buf_get_lines(state.input_buf, line_count - 1, line_count, false)[1] or ""
|
|
vim.api.nvim_win_set_cursor(state.input_win, { line_count, #last_line })
|
|
vim.cmd("startinsert!")
|
|
end
|
|
end
|
|
|
|
utils.notify("Added file: " .. filename .. " (" .. (content and #content or 0) .. " bytes)")
|
|
end
|
|
|
|
--- Close the ask panel
|
|
function M.close()
|
|
-- Remove the width maintenance autocmd first
|
|
if ask_augroup then
|
|
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
|
ask_augroup = nil
|
|
end
|
|
|
|
-- Find a window to focus after closing (not the ask windows)
|
|
local target_win = nil
|
|
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
|
local buf = vim.api.nvim_win_get_buf(win)
|
|
if win ~= state.input_win and win ~= state.output_win then
|
|
local buftype = vim.bo[buf].buftype
|
|
if buftype == "" or buftype == "acwrite" then
|
|
target_win = win
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Close input window
|
|
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
|
pcall(vim.api.nvim_win_close, state.input_win, true)
|
|
end
|
|
|
|
-- Close output window
|
|
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
|
pcall(vim.api.nvim_win_close, state.output_win, true)
|
|
end
|
|
|
|
-- Reset state
|
|
state.input_win = nil
|
|
state.output_win = nil
|
|
state.is_open = false
|
|
state.target_width = nil
|
|
|
|
-- Focus the target window if found, otherwise focus first available
|
|
if target_win and vim.api.nvim_win_is_valid(target_win) then
|
|
pcall(vim.api.nvim_set_current_win, target_win)
|
|
else
|
|
-- If no valid window, make sure we're not left with empty state
|
|
local wins = vim.api.nvim_list_wins()
|
|
if #wins > 0 then
|
|
pcall(vim.api.nvim_set_current_win, wins[1])
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Toggle the ask panel
|
|
function M.toggle()
|
|
if M.is_open() then
|
|
M.close()
|
|
else
|
|
M.open()
|
|
end
|
|
end
|
|
|
|
--- Focus the input window
|
|
function M.focus_input()
|
|
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
|
vim.api.nvim_set_current_win(state.input_win)
|
|
vim.cmd("startinsert")
|
|
end
|
|
end
|
|
|
|
--- Focus the output window
|
|
function M.focus_output()
|
|
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
|
vim.api.nvim_set_current_win(state.output_win)
|
|
end
|
|
end
|
|
|
|
--- Get input text
|
|
---@return string Input text
|
|
local function get_input_text()
|
|
if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then
|
|
return ""
|
|
end
|
|
|
|
local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false)
|
|
local content = table.concat(lines, "\n")
|
|
|
|
-- Ignore placeholder
|
|
if content:match("Type your question here") then
|
|
return ""
|
|
end
|
|
|
|
return content
|
|
end
|
|
|
|
--- Clear input buffer
|
|
function M.clear_input()
|
|
if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then
|
|
vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, { "" })
|
|
end
|
|
state.referenced_files = {}
|
|
end
|
|
|
|
--- Append text to output buffer
|
|
---@param text string Text to append
|
|
---@param is_user boolean Whether this is user message
|
|
local function append_to_output(text, is_user)
|
|
if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then
|
|
return
|
|
end
|
|
|
|
vim.bo[state.output_buf].modifiable = true
|
|
|
|
local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false)
|
|
|
|
local timestamp = os.date("%H:%M")
|
|
local header = is_user
|
|
and "┌─ 👤 You [" .. timestamp .. "] ────────"
|
|
or "┌─ 🤖 AI [" .. timestamp .. "] ──────────"
|
|
|
|
local new_lines = { "", header, "│" }
|
|
|
|
-- Add text lines with border
|
|
for _, line in ipairs(vim.split(text, "\n")) do
|
|
table.insert(new_lines, "│ " .. line)
|
|
end
|
|
|
|
table.insert(new_lines, "└─────────────────────────────────")
|
|
|
|
for _, line in ipairs(new_lines) do
|
|
table.insert(lines, line)
|
|
end
|
|
|
|
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines)
|
|
vim.bo[state.output_buf].modifiable = false
|
|
|
|
-- Scroll to bottom
|
|
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
|
local line_count = vim.api.nvim_buf_line_count(state.output_buf)
|
|
vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 })
|
|
end
|
|
end
|
|
|
|
--- Build context from referenced files
|
|
---@return string Context string, number File count
|
|
local function build_file_context()
|
|
local context = ""
|
|
local file_count = 0
|
|
|
|
for filename, filepath in pairs(state.referenced_files) do
|
|
local content = utils.read_file(filepath)
|
|
if content and content ~= "" then
|
|
-- Detect language from extension
|
|
local ext = vim.fn.fnamemodify(filepath, ":e")
|
|
local lang = ext or "text"
|
|
|
|
context = context .. "\n\n=== FILE: " .. filename .. " ===\n"
|
|
context = context .. "Path: " .. filepath .. "\n"
|
|
context = context .. "```" .. lang .. "\n" .. content .. "\n```\n"
|
|
file_count = file_count + 1
|
|
end
|
|
end
|
|
|
|
return context, file_count
|
|
end
|
|
|
|
--- Build context for the question
|
|
---@return table Context object
|
|
local function build_context()
|
|
local context = {
|
|
project_root = utils.get_project_root(),
|
|
current_file = nil,
|
|
current_content = nil,
|
|
language = nil,
|
|
referenced_files = state.referenced_files,
|
|
}
|
|
|
|
-- Try to get current file context from the non-ask window
|
|
local wins = vim.api.nvim_list_wins()
|
|
for _, win in ipairs(wins) do
|
|
if win ~= state.input_win and win ~= state.output_win then
|
|
local buf = vim.api.nvim_win_get_buf(win)
|
|
local filepath = vim.api.nvim_buf_get_name(buf)
|
|
|
|
if filepath and filepath ~= "" and not filepath:match("%.coder%.") then
|
|
context.current_file = filepath
|
|
context.current_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
|
|
context.language = vim.bo[buf].filetype
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return context
|
|
end
|
|
|
|
--- Submit the question to LLM
|
|
function M.submit()
|
|
local question = get_input_text()
|
|
|
|
if not question or question:match("^%s*$") then
|
|
utils.notify("Please enter a question", vim.log.levels.WARN)
|
|
M.focus_input()
|
|
return
|
|
end
|
|
|
|
-- Build context BEFORE clearing input (to preserve file references)
|
|
local context = build_context()
|
|
local file_context, file_count = build_file_context()
|
|
|
|
-- Build display message (without full file contents)
|
|
local display_question = question
|
|
if file_count > 0 then
|
|
display_question = question .. "\n📎 " .. file_count .. " file(s) attached"
|
|
end
|
|
|
|
-- Add user message to output
|
|
append_to_output(display_question, true)
|
|
|
|
-- Clear input and references AFTER building context
|
|
M.clear_input()
|
|
|
|
-- Build system prompt for ask mode using prompts module
|
|
local prompts = require("codetyper.prompts")
|
|
local system_prompt = prompts.system.ask
|
|
|
|
if context.current_file then
|
|
system_prompt = system_prompt .. "\n\nCurrent open file: " .. context.current_file
|
|
system_prompt = system_prompt .. "\nLanguage: " .. (context.language or "unknown")
|
|
end
|
|
|
|
-- Add to history
|
|
table.insert(state.history, { role = "user", content = question })
|
|
|
|
-- Show loading indicator
|
|
append_to_output("⏳ Thinking...", false)
|
|
|
|
-- Get LLM client and generate response
|
|
local ok, llm = pcall(require, "codetyper.llm")
|
|
if not ok then
|
|
append_to_output("❌ Error: LLM module not loaded", false)
|
|
return
|
|
end
|
|
|
|
local client = llm.get_client()
|
|
|
|
-- Build full prompt WITH file contents
|
|
local full_prompt = question
|
|
if file_context ~= "" then
|
|
full_prompt = "USER QUESTION: " .. question .. "\n\n" ..
|
|
"ATTACHED FILE CONTENTS (please analyze these):" .. file_context
|
|
end
|
|
|
|
-- Also add current file if no files were explicitly attached
|
|
if file_count == 0 and context.current_content and context.current_content ~= "" then
|
|
full_prompt = "USER QUESTION: " .. question .. "\n\n" ..
|
|
"CURRENT FILE (" .. (context.current_file or "unknown") .. "):\n```\n" ..
|
|
context.current_content .. "\n```"
|
|
end
|
|
|
|
local request_context = {
|
|
file_content = file_context ~= "" and file_context or context.current_content,
|
|
language = context.language,
|
|
prompt_type = "explain",
|
|
file_path = context.current_file,
|
|
}
|
|
|
|
client.generate(full_prompt, request_context, function(response, err)
|
|
-- Remove loading indicator
|
|
if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then
|
|
vim.bo[state.output_buf].modifiable = true
|
|
local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false)
|
|
-- Remove last few lines (the thinking message)
|
|
local to_remove = 0
|
|
for i = #lines, 1, -1 do
|
|
if lines[i]:match("Thinking") or lines[i]:match("^[│└┌─]") then
|
|
to_remove = to_remove + 1
|
|
if lines[i]:match("┌") then
|
|
break
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
for _ = 1, math.min(to_remove, 5) do
|
|
table.remove(lines)
|
|
end
|
|
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines)
|
|
vim.bo[state.output_buf].modifiable = false
|
|
end
|
|
|
|
if err then
|
|
append_to_output("❌ Error: " .. err, false)
|
|
return
|
|
end
|
|
|
|
if response then
|
|
-- Add to history
|
|
table.insert(state.history, { role = "assistant", content = response })
|
|
-- Display response
|
|
append_to_output(response, false)
|
|
else
|
|
append_to_output("❌ No response received", false)
|
|
end
|
|
|
|
-- Focus back to input
|
|
M.focus_input()
|
|
end)
|
|
end
|
|
|
|
--- Clear chat history
|
|
function M.clear_history()
|
|
state.history = {}
|
|
state.referenced_files = {}
|
|
|
|
if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then
|
|
local header = {
|
|
"╔═══════════════════════════════════╗",
|
|
"║ 🤖 CODETYPER ASK ║",
|
|
"╠═══════════════════════════════════╣",
|
|
"║ Ask about code or concepts ║",
|
|
"║ ║",
|
|
"║ 💡 Keymaps: ║",
|
|
"║ @ → attach file ║",
|
|
"║ C-Enter → send ║",
|
|
"║ C-n → new chat ║",
|
|
"║ C-f → add current file ║",
|
|
"║ C-h/j/k/l → navigate ║",
|
|
"║ q → close │ K/J → jump ║",
|
|
"╚═══════════════════════════════════╝",
|
|
"",
|
|
}
|
|
vim.bo[state.output_buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, header)
|
|
vim.bo[state.output_buf].modifiable = false
|
|
end
|
|
|
|
utils.notify("Chat history cleared")
|
|
end
|
|
|
|
--- Start a new chat (clears history and input)
|
|
function M.new_chat()
|
|
-- Clear the input
|
|
M.clear_input()
|
|
-- Clear the history
|
|
M.clear_history()
|
|
-- Focus the input
|
|
M.focus_input()
|
|
utils.notify("Started new chat")
|
|
end
|
|
|
|
--- Include current file context in input
|
|
function M.include_file_context()
|
|
local context = build_context()
|
|
|
|
if not context.current_file then
|
|
utils.notify("No file context available", vim.log.levels.WARN)
|
|
return
|
|
end
|
|
|
|
local filename = vim.fn.fnamemodify(context.current_file, ":t")
|
|
M.add_file_reference(context.current_file, filename)
|
|
end
|
|
|
|
--- Copy last assistant response to clipboard
|
|
function M.copy_last_response()
|
|
for i = #state.history, 1, -1 do
|
|
if state.history[i].role == "assistant" then
|
|
vim.fn.setreg("+", state.history[i].content)
|
|
utils.notify("Response copied to clipboard")
|
|
return
|
|
end
|
|
end
|
|
utils.notify("No response to copy", vim.log.levels.WARN)
|
|
end
|
|
--- Check if ask panel is open (validates window state)
|
|
---@return boolean
|
|
function M.is_open()
|
|
-- Verify windows are actually valid, not just the flag
|
|
if state.is_open then
|
|
local output_valid = state.output_win and vim.api.nvim_win_is_valid(state.output_win)
|
|
local input_valid = state.input_win and vim.api.nvim_win_is_valid(state.input_win)
|
|
|
|
-- If either window is invalid, reset the state
|
|
if not output_valid or not input_valid then
|
|
state.is_open = false
|
|
state.output_win = nil
|
|
state.input_win = nil
|
|
state.target_width = nil
|
|
-- Clean up autocmd
|
|
if ask_augroup then
|
|
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
|
ask_augroup = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
return state.is_open
|
|
end
|
|
|
|
--- Get chat history
|
|
---@return table History
|
|
function M.get_history()
|
|
return state.history
|
|
end
|
|
|
|
return M
|