feat: initial release of codetyper.nvim v0.2.0
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
This commit is contained in:
866
lua/codetyper/ask.lua
Normal file
866
lua/codetyper/ask.lua
Normal file
@@ -0,0 +1,866 @@
|
||||
---@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
|
||||
349
lua/codetyper/autocmds.lua
Normal file
349
lua/codetyper/autocmds.lua
Normal file
@@ -0,0 +1,349 @@
|
||||
---@mod codetyper.autocmds Autocommands for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Autocommand group name
|
||||
local AUGROUP = "Codetyper"
|
||||
|
||||
--- Debounce timer for tree updates
|
||||
local tree_update_timer = nil
|
||||
local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce
|
||||
|
||||
--- Track processed prompts to avoid re-processing
|
||||
---@type table<string, boolean>
|
||||
local processed_prompts = {}
|
||||
|
||||
--- Generate a unique key for a prompt
|
||||
---@param bufnr number Buffer number
|
||||
---@param prompt table Prompt object
|
||||
---@return string Unique key
|
||||
local function get_prompt_key(bufnr, prompt)
|
||||
return string.format("%d:%d:%d:%s", bufnr, prompt.start_line, prompt.end_line, prompt.content:sub(1, 50))
|
||||
end
|
||||
|
||||
--- Schedule tree update with debounce
|
||||
local function schedule_tree_update()
|
||||
if tree_update_timer then
|
||||
tree_update_timer:stop()
|
||||
end
|
||||
|
||||
tree_update_timer = vim.defer_fn(function()
|
||||
local tree = require("codetyper.tree")
|
||||
tree.update_tree_log()
|
||||
tree_update_timer = nil
|
||||
end, TREE_UPDATE_DEBOUNCE_MS)
|
||||
end
|
||||
|
||||
--- Setup autocommands
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true })
|
||||
|
||||
-- Auto-save coder file when leaving insert mode
|
||||
vim.api.nvim_create_autocmd("InsertLeave", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
-- Auto-save the coder file
|
||||
if vim.bo.modified then
|
||||
vim.cmd("silent! write")
|
||||
end
|
||||
-- Check for closed prompts and auto-process
|
||||
M.check_for_closed_prompt()
|
||||
end,
|
||||
desc = "Auto-save and check for closed prompt tags",
|
||||
})
|
||||
|
||||
-- Auto-set filetype for coder files based on extension
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
M.set_coder_filetype()
|
||||
end,
|
||||
desc = "Set filetype for coder files",
|
||||
})
|
||||
|
||||
-- Auto-open split view when opening a coder file directly (e.g., from nvim-tree)
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
-- Delay slightly to ensure buffer is fully loaded
|
||||
vim.defer_fn(function()
|
||||
M.auto_open_target_file()
|
||||
end, 50)
|
||||
end,
|
||||
desc = "Auto-open target file when coder file is opened",
|
||||
})
|
||||
|
||||
-- Cleanup on buffer close
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function(ev)
|
||||
local window = require("codetyper.window")
|
||||
if window.is_open() then
|
||||
window.close_split()
|
||||
end
|
||||
-- Clear processed prompts for this buffer
|
||||
local bufnr = ev.buf
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
-- Clear auto-opened tracking
|
||||
M.clear_auto_opened(bufnr)
|
||||
end,
|
||||
desc = "Cleanup on coder buffer close",
|
||||
})
|
||||
|
||||
-- Update tree.log when files are created/written
|
||||
vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
-- Skip coder files and tree.log itself
|
||||
local filepath = ev.file or vim.fn.expand("%:p")
|
||||
if filepath:match("%.coder%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
-- Schedule tree update with debounce
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on file creation/save",
|
||||
})
|
||||
|
||||
-- Update tree.log when files are deleted (via netrw or file explorer)
|
||||
vim.api.nvim_create_autocmd("BufDelete", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
local filepath = ev.file or ""
|
||||
-- Skip special buffers and coder files
|
||||
if filepath == "" or filepath:match("%.coder%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on file deletion",
|
||||
})
|
||||
|
||||
-- Update tree on directory change
|
||||
vim.api.nvim_create_autocmd("DirChanged", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on directory change",
|
||||
})
|
||||
end
|
||||
|
||||
--- Check if the buffer has a newly closed prompt and auto-process
|
||||
function M.check_for_closed_prompt()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local parser = require("codetyper.parser")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Get current line
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local line = cursor[1]
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)
|
||||
|
||||
if #lines == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = lines[1]
|
||||
|
||||
-- Check if line contains closing tag
|
||||
if parser.has_closing_tag(current_line, config.patterns.close_tag) then
|
||||
-- Find the complete prompt
|
||||
local prompt = parser.get_last_prompt(bufnr)
|
||||
if prompt and prompt.content and prompt.content ~= "" then
|
||||
-- Generate unique key for this prompt
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
-- Check if already processed
|
||||
if processed_prompts[prompt_key] then
|
||||
return
|
||||
end
|
||||
|
||||
-- Mark as processed
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
-- Auto-process the prompt (no confirmation needed)
|
||||
utils.notify("Processing prompt...", vim.log.levels.INFO)
|
||||
vim.schedule(function()
|
||||
vim.cmd("CoderProcess")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset processed prompts for a buffer (useful for re-processing)
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
function M.reset_processed(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
utils.notify("Prompt history cleared - prompts can be re-processed")
|
||||
end
|
||||
|
||||
--- Track if we already opened the split for this buffer
|
||||
---@type table<number, boolean>
|
||||
local auto_opened_buffers = {}
|
||||
|
||||
--- Auto-open target file when a coder file is opened directly
|
||||
function M.auto_open_target_file()
|
||||
local window = require("codetyper.window")
|
||||
|
||||
-- Skip if split is already open
|
||||
if window.is_open() then
|
||||
return
|
||||
end
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Skip if we already handled this buffer
|
||||
if auto_opened_buffers[bufnr] then
|
||||
return
|
||||
end
|
||||
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
-- Skip empty paths
|
||||
if not current_file or current_file == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify it's a coder file
|
||||
if not utils.is_coder_file(current_file) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Skip if we're in a special buffer (nvim-tree, etc.)
|
||||
local buftype = vim.bo[bufnr].buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Mark as handled
|
||||
auto_opened_buffers[bufnr] = true
|
||||
|
||||
-- Get the target file path
|
||||
local target_path = utils.get_target_path(current_file)
|
||||
|
||||
-- Check if target file exists
|
||||
if not utils.file_exists(target_path) then
|
||||
utils.notify("Target file not found: " .. vim.fn.fnamemodify(target_path, ":t"), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get config with fallback defaults
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
-- Fallback width if config not fully loaded
|
||||
local width = (config and config.window and config.window.width) or 0.4
|
||||
if width <= 1 then
|
||||
width = math.floor(vim.o.columns * width)
|
||||
end
|
||||
|
||||
-- Store current coder window
|
||||
local coder_win = vim.api.nvim_get_current_win()
|
||||
local coder_buf = bufnr
|
||||
|
||||
-- Open target file in a vertical split on the right
|
||||
local ok, err = pcall(function()
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(target_path))
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
utils.notify("Failed to open target file: " .. tostring(err), vim.log.levels.ERROR)
|
||||
auto_opened_buffers[bufnr] = nil -- Allow retry
|
||||
return
|
||||
end
|
||||
|
||||
-- Now we're in the target window (right side)
|
||||
local target_win = vim.api.nvim_get_current_win()
|
||||
local target_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Set the coder window width (left side)
|
||||
pcall(vim.api.nvim_win_set_width, coder_win, width)
|
||||
|
||||
-- Update window module state
|
||||
window._coder_win = coder_win
|
||||
window._coder_buf = coder_buf
|
||||
window._target_win = target_win
|
||||
window._target_buf = target_buf
|
||||
|
||||
-- Set up window options for coder window
|
||||
pcall(function()
|
||||
vim.wo[coder_win].number = true
|
||||
vim.wo[coder_win].relativenumber = true
|
||||
vim.wo[coder_win].signcolumn = "yes"
|
||||
end)
|
||||
|
||||
utils.notify("Opened target: " .. vim.fn.fnamemodify(target_path, ":t"))
|
||||
end
|
||||
|
||||
--- Clear auto-opened tracking for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
function M.clear_auto_opened(bufnr)
|
||||
auto_opened_buffers[bufnr] = nil
|
||||
end
|
||||
|
||||
--- Set appropriate filetype for coder files
|
||||
function M.set_coder_filetype()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
|
||||
-- Extract the actual extension (e.g., index.coder.ts -> ts)
|
||||
local ext = filepath:match("%.coder%.(%w+)$")
|
||||
|
||||
if ext then
|
||||
-- Map extension to filetype
|
||||
local ft_map = {
|
||||
ts = "typescript",
|
||||
tsx = "typescriptreact",
|
||||
js = "javascript",
|
||||
jsx = "javascriptreact",
|
||||
py = "python",
|
||||
lua = "lua",
|
||||
go = "go",
|
||||
rs = "rust",
|
||||
rb = "ruby",
|
||||
java = "java",
|
||||
c = "c",
|
||||
cpp = "cpp",
|
||||
cs = "cs",
|
||||
json = "json",
|
||||
yaml = "yaml",
|
||||
yml = "yaml",
|
||||
md = "markdown",
|
||||
html = "html",
|
||||
css = "css",
|
||||
scss = "scss",
|
||||
vue = "vue",
|
||||
svelte = "svelte",
|
||||
}
|
||||
|
||||
local filetype = ft_map[ext] or ext
|
||||
vim.bo.filetype = filetype
|
||||
end
|
||||
end
|
||||
|
||||
--- Clear all autocommands
|
||||
function M.clear()
|
||||
vim.api.nvim_del_augroup_by_name(AUGROUP)
|
||||
end
|
||||
|
||||
return M
|
||||
330
lua/codetyper/commands.lua
Normal file
330
lua/codetyper/commands.lua
Normal file
@@ -0,0 +1,330 @@
|
||||
---@mod codetyper.commands Command definitions for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local window = require("codetyper.window")
|
||||
|
||||
--- 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
|
||||
|
||||
--- Process prompt at cursor and generate code
|
||||
local function cmd_process()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.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)
|
||||
|
||||
llm.generate(clean_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)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Show plugin status
|
||||
local function cmd_status()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local tree = require("codetyper.tree")
|
||||
|
||||
local stats = tree.get_stats()
|
||||
|
||||
local status = {
|
||||
"Codetyper.nvim Status",
|
||||
"====================",
|
||||
"",
|
||||
"Provider: " .. config.llm.provider,
|
||||
}
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local has_key = (config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY) ~= nil
|
||||
table.insert(status, "Claude API Key: " .. (has_key and "configured" or "NOT SET"))
|
||||
table.insert(status, "Claude Model: " .. config.llm.claude.model)
|
||||
else
|
||||
table.insert(status, "Ollama Host: " .. config.llm.ollama.host)
|
||||
table.insert(status, "Ollama Model: " .. config.llm.ollama.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()
|
||||
local tree = require("codetyper.tree")
|
||||
if tree.update_tree_log() then
|
||||
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
|
||||
else
|
||||
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Open tree.log file
|
||||
local function cmd_tree_view()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree_log_path = tree.get_tree_log_path()
|
||||
|
||||
if not tree_log_path then
|
||||
utils.notify("Could not find tree.log", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure tree is up to date
|
||||
tree.update_tree_log()
|
||||
|
||||
-- Open in a new split
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
|
||||
vim.bo.readonly = true
|
||||
vim.bo.modifiable = false
|
||||
end
|
||||
|
||||
--- Reset processed prompts to allow re-processing
|
||||
local function cmd_reset()
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
autocmds.reset_processed()
|
||||
end
|
||||
|
||||
--- Force update gitignore
|
||||
local function cmd_gitignore()
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.force_update()
|
||||
end
|
||||
|
||||
--- Open ask panel
|
||||
local function cmd_ask()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.open()
|
||||
end
|
||||
|
||||
--- Close ask panel
|
||||
local function cmd_ask_close()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.close()
|
||||
end
|
||||
|
||||
--- Toggle ask panel
|
||||
local function cmd_ask_toggle()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.toggle()
|
||||
end
|
||||
|
||||
--- Clear ask history
|
||||
local function cmd_ask_clear()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.clear_history()
|
||||
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
|
||||
|
||||
--- Main command dispatcher
|
||||
---@param args table Command arguments
|
||||
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,
|
||||
ask = cmd_ask,
|
||||
["ask-close"] = cmd_ask_close,
|
||||
["ask-toggle"] = cmd_ask_toggle,
|
||||
["ask-clear"] = cmd_ask_clear,
|
||||
gitignore = cmd_gitignore,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
if cmd_fn then
|
||||
cmd_fn(args)
|
||||
else
|
||||
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup all commands
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command("Coder", coder_cmd, {
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return {
|
||||
"open", "close", "toggle", "process", "status", "focus",
|
||||
"tree", "tree-view", "reset", "gitignore",
|
||||
"ask", "ask-close", "ask-toggle", "ask-clear",
|
||||
}
|
||||
end,
|
||||
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" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTreeView", function()
|
||||
cmd_tree_view()
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
-- Ask panel commands
|
||||
vim.api.nvim_create_user_command("CoderAsk", function()
|
||||
cmd_ask()
|
||||
end, { desc = "Open Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskToggle", function()
|
||||
cmd_ask_toggle()
|
||||
end, { desc = "Toggle Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskClear", function()
|
||||
cmd_ask_clear()
|
||||
end, { desc = "Clear Ask history" })
|
||||
end
|
||||
|
||||
return M
|
||||
84
lua/codetyper/config.lua
Normal file
84
lua/codetyper/config.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
---@mod codetyper.config Configuration module for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type CoderConfig
|
||||
local defaults = {
|
||||
llm = {
|
||||
provider = "claude",
|
||||
claude = {
|
||||
api_key = nil, -- Will use ANTHROPIC_API_KEY env var if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "codellama",
|
||||
},
|
||||
},
|
||||
window = {
|
||||
width = 0.25, -- 25% of screen width (1/4)
|
||||
position = "left",
|
||||
border = "rounded",
|
||||
},
|
||||
patterns = {
|
||||
open_tag = "/@",
|
||||
close_tag = "@/",
|
||||
file_pattern = "*.coder.*",
|
||||
},
|
||||
auto_gitignore = true,
|
||||
auto_open_ask = true, -- Auto-open Ask panel on startup
|
||||
}
|
||||
|
||||
--- Deep merge two tables
|
||||
---@param t1 table Base table
|
||||
---@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
|
||||
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)
|
||||
end
|
||||
|
||||
--- Get default configuration
|
||||
---@return CoderConfig Default configuration
|
||||
function M.get_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 config.llm.provider ~= "claude" and config.llm.provider ~= "ollama" then
|
||||
return false, "Invalid LLM provider. Must be 'claude' or 'ollama'"
|
||||
end
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
if not api_key or api_key == "" then
|
||||
return false, "Claude API key not configured. Set llm.claude.api_key or ANTHROPIC_API_KEY env var"
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
190
lua/codetyper/gitignore.lua
Normal file
190
lua/codetyper/gitignore.lua
Normal file
@@ -0,0 +1,190 @@
|
||||
---@mod codetyper.gitignore Gitignore management for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Patterns to add to .gitignore
|
||||
local IGNORE_PATTERNS = {
|
||||
"*.coder.*",
|
||||
".coder/",
|
||||
}
|
||||
|
||||
--- Comment to identify codetyper entries
|
||||
local CODER_COMMENT = "# Codetyper.nvim - AI coding partner files"
|
||||
|
||||
--- Check if pattern exists in gitignore content
|
||||
---@param content string Gitignore content
|
||||
---@param pattern string Pattern to check
|
||||
---@return boolean
|
||||
local function pattern_exists(content, pattern)
|
||||
local escaped = utils.escape_pattern(pattern)
|
||||
return content:match("\n" .. escaped .. "\n") ~= nil
|
||||
or content:match("^" .. escaped .. "\n") ~= nil
|
||||
or content:match("\n" .. escaped .. "$") ~= nil
|
||||
or content == pattern
|
||||
end
|
||||
|
||||
--- Check if all patterns exist in gitignore content
|
||||
---@param content string Gitignore content
|
||||
---@return boolean, string[] All exist status and list of missing patterns
|
||||
local function all_patterns_exist(content)
|
||||
local missing = {}
|
||||
for _, pattern in ipairs(IGNORE_PATTERNS) do
|
||||
if not pattern_exists(content, pattern) then
|
||||
table.insert(missing, pattern)
|
||||
end
|
||||
end
|
||||
return #missing == 0, missing
|
||||
end
|
||||
|
||||
--- Get the path to .gitignore in project root
|
||||
---@return string|nil Path to .gitignore or nil
|
||||
function M.get_gitignore_path()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return nil
|
||||
end
|
||||
return root .. "/.gitignore"
|
||||
end
|
||||
|
||||
--- Check if coder files are already ignored
|
||||
---@return boolean
|
||||
function M.is_ignored()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
|
||||
local all_exist, _ = all_patterns_exist(content)
|
||||
return all_exist
|
||||
end
|
||||
|
||||
--- Add coder patterns to .gitignore
|
||||
---@return boolean Success status
|
||||
function M.add_to_gitignore()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
utils.notify("Could not determine project root", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
local patterns_to_add = {}
|
||||
|
||||
if content then
|
||||
-- File exists, check which patterns are missing
|
||||
local _, missing = all_patterns_exist(content)
|
||||
if #missing == 0 then
|
||||
return true -- All already ignored
|
||||
end
|
||||
patterns_to_add = missing
|
||||
else
|
||||
-- Create new .gitignore with all patterns
|
||||
content = ""
|
||||
patterns_to_add = IGNORE_PATTERNS
|
||||
end
|
||||
|
||||
-- Build the patterns string
|
||||
local patterns_str = table.concat(patterns_to_add, "\n")
|
||||
|
||||
if content == "" then
|
||||
-- New file
|
||||
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
|
||||
else
|
||||
-- Append to existing
|
||||
local newline = content:sub(-1) == "\n" and "" or "\n"
|
||||
-- Check if comment already exists
|
||||
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
|
||||
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
|
||||
else
|
||||
content = content .. newline .. patterns_str .. "\n"
|
||||
end
|
||||
end
|
||||
|
||||
if utils.write_file(gitignore_path, content) then
|
||||
utils.notify("Added coder patterns to .gitignore")
|
||||
return true
|
||||
else
|
||||
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Ensure coder files are in .gitignore (called on setup)
|
||||
---@param auto_gitignore? boolean Override auto_gitignore setting (default: true)
|
||||
---@return boolean Success status
|
||||
function M.ensure_ignored(auto_gitignore)
|
||||
-- Default to true if not specified
|
||||
if auto_gitignore == nil then
|
||||
-- Try to get from config if available
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok and codetyper.is_initialized and codetyper.is_initialized() then
|
||||
local config = codetyper.get_config()
|
||||
auto_gitignore = config and config.auto_gitignore
|
||||
else
|
||||
auto_gitignore = true -- Default to true
|
||||
end
|
||||
end
|
||||
|
||||
if not auto_gitignore then
|
||||
return true
|
||||
end
|
||||
|
||||
if M.is_ignored() then
|
||||
return true
|
||||
end
|
||||
|
||||
return M.add_to_gitignore()
|
||||
end
|
||||
|
||||
--- Remove coder patterns from .gitignore
|
||||
---@return boolean Success status
|
||||
function M.remove_from_gitignore()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Remove the comment and all patterns
|
||||
content = content:gsub(CODER_COMMENT .. "\n", "")
|
||||
for _, pattern in ipairs(IGNORE_PATTERNS) do
|
||||
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
|
||||
end
|
||||
|
||||
-- Clean up extra newlines
|
||||
content = content:gsub("\n\n\n+", "\n\n")
|
||||
|
||||
return utils.write_file(gitignore_path, content)
|
||||
end
|
||||
|
||||
--- Get list of patterns being ignored
|
||||
---@return string[] List of patterns
|
||||
function M.get_ignore_patterns()
|
||||
return vim.deepcopy(IGNORE_PATTERNS)
|
||||
end
|
||||
|
||||
--- Force update gitignore (manual trigger)
|
||||
---@return boolean Success status
|
||||
function M.force_update()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
utils.notify("Updating .gitignore at: " .. gitignore_path)
|
||||
return M.add_to_gitignore()
|
||||
end
|
||||
|
||||
return M
|
||||
91
lua/codetyper/health.lua
Normal file
91
lua/codetyper/health.lua
Normal file
@@ -0,0 +1,91 @@
|
||||
---@mod codetyper.health Health check for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local health = vim.health or require("health")
|
||||
|
||||
--- Run health checks
|
||||
function M.check()
|
||||
health.start("Codetyper.nvim")
|
||||
|
||||
-- Check Neovim version
|
||||
if vim.fn.has("nvim-0.8.0") == 1 then
|
||||
health.ok("Neovim version >= 0.8.0")
|
||||
else
|
||||
health.error("Neovim 0.8.0+ required")
|
||||
end
|
||||
|
||||
-- Check if plugin is initialized
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok and codetyper.is_initialized() then
|
||||
health.ok("Plugin initialized")
|
||||
else
|
||||
health.info("Plugin not yet initialized (call setup() first)")
|
||||
end
|
||||
|
||||
-- Check curl availability
|
||||
if vim.fn.executable("curl") == 1 then
|
||||
health.ok("curl is available")
|
||||
else
|
||||
health.error("curl is required for LLM API calls")
|
||||
end
|
||||
|
||||
-- Check LLM configuration
|
||||
if ok and codetyper.is_initialized() then
|
||||
local config = codetyper.get_config()
|
||||
|
||||
health.info("LLM Provider: " .. config.llm.provider)
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
if api_key and api_key ~= "" then
|
||||
health.ok("Claude API key configured")
|
||||
else
|
||||
health.warn("Claude API key not set. Set ANTHROPIC_API_KEY or llm.claude.api_key")
|
||||
end
|
||||
health.info("Claude model: " .. config.llm.claude.model)
|
||||
elseif config.llm.provider == "ollama" then
|
||||
health.info("Ollama host: " .. config.llm.ollama.host)
|
||||
health.info("Ollama model: " .. config.llm.ollama.model)
|
||||
|
||||
-- Try to check Ollama connectivity
|
||||
local ollama = require("codetyper.llm.ollama")
|
||||
ollama.health_check(function(is_ok, err)
|
||||
if is_ok then
|
||||
vim.schedule(function()
|
||||
health.ok("Ollama is reachable")
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
health.warn("Cannot connect to Ollama: " .. (err or "unknown error"))
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check optional dependencies
|
||||
if pcall(require, "telescope") then
|
||||
health.ok("telescope.nvim is available (enhanced file picker)")
|
||||
else
|
||||
health.info("telescope.nvim not found (using basic file picker)")
|
||||
end
|
||||
|
||||
-- Check .gitignore configuration
|
||||
local utils = require("codetyper.utils")
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
|
||||
local root = utils.get_project_root()
|
||||
if root then
|
||||
health.info("Project root: " .. root)
|
||||
if gitignore.is_ignored() then
|
||||
health.ok("Coder files are in .gitignore")
|
||||
else
|
||||
health.warn("Coder files not in .gitignore (will be added on setup)")
|
||||
end
|
||||
else
|
||||
health.info("No project root detected")
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
69
lua/codetyper/init.lua
Normal file
69
lua/codetyper/init.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
---@mod codetyper Codetyper.nvim - AI-powered coding partner
|
||||
---@brief [[
|
||||
--- Codetyper.nvim is a Neovim plugin that acts as your coding partner.
|
||||
--- It uses LLM APIs (Claude, Ollama) to help you write code faster
|
||||
--- using special `.coder.*` files and inline prompt tags.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type CoderConfig
|
||||
M.config = {}
|
||||
|
||||
---@type boolean
|
||||
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
|
||||
|
||||
local config = require("codetyper.config")
|
||||
M.config = config.setup(opts)
|
||||
|
||||
-- Initialize modules
|
||||
local commands = require("codetyper.commands")
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local tree = require("codetyper.tree")
|
||||
|
||||
-- Register commands
|
||||
commands.setup()
|
||||
|
||||
-- Setup autocommands
|
||||
autocmds.setup()
|
||||
|
||||
-- Ensure .gitignore has coder files excluded
|
||||
gitignore.ensure_ignored()
|
||||
|
||||
-- Initialize tree logging (creates .coder folder and initial tree.log)
|
||||
tree.setup()
|
||||
|
||||
M._initialized = true
|
||||
|
||||
-- Auto-open Ask panel after a short delay (to let UI settle)
|
||||
if M.config.auto_open_ask then
|
||||
vim.defer_fn(function()
|
||||
local ask = require("codetyper.ask")
|
||||
if not ask.is_open() then
|
||||
ask.open()
|
||||
end
|
||||
end, 300)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current configuration
|
||||
---@return CoderConfig
|
||||
function M.get_config()
|
||||
return M.config
|
||||
end
|
||||
|
||||
--- Check if plugin is initialized
|
||||
---@return boolean
|
||||
function M.is_initialized()
|
||||
return M._initialized
|
||||
end
|
||||
|
||||
return M
|
||||
239
lua/codetyper/inject.lua
Normal file
239
lua/codetyper/inject.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
---@mod codetyper.inject Code injection for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.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)
|
||||
local window = require("codetyper.window")
|
||||
|
||||
-- Normalize the target path
|
||||
target_path = vim.fn.fnamemodify(target_path, ":p")
|
||||
|
||||
-- Get target buffer
|
||||
local target_buf = window.get_target_buf()
|
||||
|
||||
if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then
|
||||
-- Try to find buffer by path
|
||||
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
|
||||
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 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 })
|
||||
|
||||
-- Get cursor position in target window
|
||||
local window = require("codetyper.window")
|
||||
local target_win = window.get_target_win()
|
||||
|
||||
local insert_line
|
||||
if target_win and vim.api.nvim_win_is_valid(target_win) then
|
||||
local cursor = vim.api.nvim_win_get_cursor(target_win)
|
||||
insert_line = cursor[1]
|
||||
else
|
||||
-- Append at end
|
||||
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
|
||||
154
lua/codetyper/llm/claude.lua
Normal file
154
lua/codetyper/llm/claude.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
---@mod codetyper.llm.claude Claude API client for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
|
||||
--- Claude API endpoint
|
||||
local API_URL = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
--- Get API key from config or environment
|
||||
---@return string|nil API key
|
||||
local function get_api_key()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
end
|
||||
|
||||
--- Get model from config
|
||||
---@return string Model name
|
||||
local function get_model()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.claude.model
|
||||
end
|
||||
|
||||
--- Build request body for Claude API
|
||||
---@param prompt string User prompt
|
||||
---@param context table Context information
|
||||
---@return table Request body
|
||||
local function build_request_body(prompt, context)
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
|
||||
return {
|
||||
model = get_model(),
|
||||
max_tokens = 4096,
|
||||
system = system_prompt,
|
||||
messages = {
|
||||
{
|
||||
role = "user",
|
||||
content = prompt,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Make HTTP request to Claude API
|
||||
---@param body table Request body
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
local function make_request(body, callback)
|
||||
local api_key = get_api_key()
|
||||
if not api_key then
|
||||
callback(nil, "Claude API key not configured")
|
||||
return
|
||||
end
|
||||
|
||||
local json_body = vim.json.encode(body)
|
||||
|
||||
-- Use curl for HTTP request (plenary.curl alternative)
|
||||
local cmd = {
|
||||
"curl",
|
||||
"-s",
|
||||
"-X", "POST",
|
||||
API_URL,
|
||||
"-H", "Content-Type: application/json",
|
||||
"-H", "x-api-key: " .. api_key,
|
||||
"-H", "anthropic-version: 2023-06-01",
|
||||
"-d", json_body,
|
||||
}
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if not data or #data == 0 or (data[1] == "" and #data == 1) then
|
||||
return
|
||||
end
|
||||
|
||||
local response_text = table.concat(data, "\n")
|
||||
local ok, response = pcall(vim.json.decode, response_text)
|
||||
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Failed to parse Claude response")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.error then
|
||||
vim.schedule(function()
|
||||
callback(nil, response.error.message or "Claude API error")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.content and response.content[1] and response.content[1].text then
|
||||
local code = llm.extract_code(response.content[1].text)
|
||||
vim.schedule(function()
|
||||
callback(code, nil)
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
callback(nil, "No content in Claude response")
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"))
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Claude API request failed with code: " .. code)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Generate code using Claude API
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
utils.notify("Sending request to Claude...", vim.log.levels.INFO)
|
||||
|
||||
local body = build_request_body(prompt, context)
|
||||
make_request(body, function(response, err)
|
||||
if err then
|
||||
utils.notify(err, vim.log.levels.ERROR)
|
||||
callback(nil, err)
|
||||
else
|
||||
utils.notify("Code generated successfully", vim.log.levels.INFO)
|
||||
callback(response, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check if Claude is properly configured
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate()
|
||||
local api_key = get_api_key()
|
||||
if not api_key or api_key == "" then
|
||||
return false, "Claude API key not configured"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
101
lua/codetyper/llm/init.lua
Normal file
101
lua/codetyper/llm/init.lua
Normal file
@@ -0,0 +1,101 @@
|
||||
---@mod codetyper.llm LLM interface for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Get the appropriate LLM client based on configuration
|
||||
---@return table LLM client module
|
||||
function M.get_client()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
return require("codetyper.llm.claude")
|
||||
elseif config.llm.provider == "ollama" then
|
||||
return require("codetyper.llm.ollama")
|
||||
else
|
||||
error("Unknown LLM provider: " .. config.llm.provider)
|
||||
end
|
||||
end
|
||||
|
||||
--- Generate code from a prompt
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information (file content, language, etc.)
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
local client = M.get_client()
|
||||
client.generate(prompt, context, callback)
|
||||
end
|
||||
|
||||
--- Build the system prompt for code generation
|
||||
---@param context table Context information
|
||||
---@return string System prompt
|
||||
function M.build_system_prompt(context)
|
||||
local prompts = require("codetyper.prompts")
|
||||
|
||||
-- Select appropriate system prompt based on context
|
||||
local prompt_type = context.prompt_type or "code_generation"
|
||||
local system_prompts = prompts.system
|
||||
|
||||
local system = system_prompts[prompt_type] or system_prompts.code_generation
|
||||
|
||||
-- Substitute variables
|
||||
system = system:gsub("{{language}}", context.language or "unknown")
|
||||
system = system:gsub("{{filepath}}", context.file_path or "unknown")
|
||||
|
||||
if context.file_content then
|
||||
system = system .. "\n\nExisting file content:\n```\n" .. context.file_content .. "\n```"
|
||||
end
|
||||
|
||||
return system
|
||||
end
|
||||
|
||||
--- Build context for LLM request
|
||||
---@param target_path string Path to target file
|
||||
---@param prompt_type string Type of prompt
|
||||
---@return table Context object
|
||||
function M.build_context(target_path, prompt_type)
|
||||
local content = utils.read_file(target_path)
|
||||
local ext = vim.fn.fnamemodify(target_path, ":e")
|
||||
|
||||
-- Map extension to language
|
||||
local lang_map = {
|
||||
ts = "TypeScript",
|
||||
tsx = "TypeScript React",
|
||||
js = "JavaScript",
|
||||
jsx = "JavaScript React",
|
||||
py = "Python",
|
||||
lua = "Lua",
|
||||
go = "Go",
|
||||
rs = "Rust",
|
||||
rb = "Ruby",
|
||||
java = "Java",
|
||||
c = "C",
|
||||
cpp = "C++",
|
||||
cs = "C#",
|
||||
}
|
||||
|
||||
return {
|
||||
file_content = content,
|
||||
language = lang_map[ext] or ext,
|
||||
extension = ext,
|
||||
prompt_type = prompt_type,
|
||||
file_path = target_path,
|
||||
}
|
||||
end
|
||||
|
||||
--- Parse LLM response and extract code
|
||||
---@param response string Raw LLM response
|
||||
---@return string Extracted code
|
||||
function M.extract_code(response)
|
||||
-- Remove markdown code blocks if present
|
||||
local code = response:gsub("```%w*\n?", ""):gsub("\n?```", "")
|
||||
|
||||
-- Trim whitespace
|
||||
code = code:match("^%s*(.-)%s*$")
|
||||
|
||||
return code
|
||||
end
|
||||
|
||||
return M
|
||||
173
lua/codetyper/llm/ollama.lua
Normal file
173
lua/codetyper/llm/ollama.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
---@mod codetyper.llm.ollama Ollama API client for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
|
||||
--- Get Ollama host from config
|
||||
---@return string Host URL
|
||||
local function get_host()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.ollama.host
|
||||
end
|
||||
|
||||
--- Get model from config
|
||||
---@return string Model name
|
||||
local function get_model()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.ollama.model
|
||||
end
|
||||
|
||||
--- Build request body for Ollama API
|
||||
---@param prompt string User prompt
|
||||
---@param context table Context information
|
||||
---@return table Request body
|
||||
local function build_request_body(prompt, context)
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
|
||||
return {
|
||||
model = get_model(),
|
||||
system = system_prompt,
|
||||
prompt = prompt,
|
||||
stream = false,
|
||||
options = {
|
||||
temperature = 0.2,
|
||||
num_predict = 4096,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Make HTTP request to Ollama API
|
||||
---@param body table Request body
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
local function make_request(body, callback)
|
||||
local host = get_host()
|
||||
local url = host .. "/api/generate"
|
||||
local json_body = vim.json.encode(body)
|
||||
|
||||
local cmd = {
|
||||
"curl",
|
||||
"-s",
|
||||
"-X", "POST",
|
||||
url,
|
||||
"-H", "Content-Type: application/json",
|
||||
"-d", json_body,
|
||||
}
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if not data or #data == 0 or (data[1] == "" and #data == 1) then
|
||||
return
|
||||
end
|
||||
|
||||
local response_text = table.concat(data, "\n")
|
||||
local ok, response = pcall(vim.json.decode, response_text)
|
||||
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Failed to parse Ollama response")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.error then
|
||||
vim.schedule(function()
|
||||
callback(nil, response.error or "Ollama API error")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.response then
|
||||
local code = llm.extract_code(response.response)
|
||||
vim.schedule(function()
|
||||
callback(code, nil)
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
callback(nil, "No response from Ollama")
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Ollama API request failed: " .. table.concat(data, "\n"))
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Ollama API request failed with code: " .. code)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Generate code using Ollama API
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
utils.notify("Sending request to Ollama...", vim.log.levels.INFO)
|
||||
|
||||
local body = build_request_body(prompt, context)
|
||||
make_request(body, function(response, err)
|
||||
if err then
|
||||
utils.notify(err, vim.log.levels.ERROR)
|
||||
callback(nil, err)
|
||||
else
|
||||
utils.notify("Code generated successfully", vim.log.levels.INFO)
|
||||
callback(response, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check if Ollama is reachable
|
||||
---@param callback fun(ok: boolean, error: string|nil) Callback function
|
||||
function M.health_check(callback)
|
||||
local host = get_host()
|
||||
|
||||
local cmd = { "curl", "-s", host .. "/api/tags" }
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(true, nil)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(false, "Cannot connect to Ollama at " .. host)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Check if Ollama is properly configured
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate()
|
||||
local host = get_host()
|
||||
if not host or host == "" then
|
||||
return false, "Ollama host not configured"
|
||||
end
|
||||
local model = get_model()
|
||||
if not model or model == "" then
|
||||
return false, "Ollama model not configured"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
183
lua/codetyper/parser.lua
Normal file
183
lua/codetyper/parser.lua
Normal file
@@ -0,0 +1,183 @@
|
||||
---@mod codetyper.parser Parser for /@ @/ prompt tags
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Find all prompts in buffer content
|
||||
---@param content string Buffer content
|
||||
---@param open_tag string Opening tag
|
||||
---@param close_tag string Closing tag
|
||||
---@return CoderPrompt[] List of found prompts
|
||||
function M.find_prompts(content, open_tag, close_tag)
|
||||
local prompts = {}
|
||||
local escaped_open = utils.escape_pattern(open_tag)
|
||||
local escaped_close = utils.escape_pattern(close_tag)
|
||||
|
||||
local lines = vim.split(content, "\n", { plain = true })
|
||||
local in_prompt = false
|
||||
local current_prompt = nil
|
||||
local prompt_content = {}
|
||||
|
||||
for line_num, line in ipairs(lines) do
|
||||
if not in_prompt then
|
||||
-- Look for opening tag
|
||||
local start_col = line:find(escaped_open)
|
||||
if start_col then
|
||||
in_prompt = true
|
||||
current_prompt = {
|
||||
start_line = line_num,
|
||||
start_col = start_col,
|
||||
content = "",
|
||||
}
|
||||
-- Get content after opening tag on same line
|
||||
local after_tag = line:sub(start_col + #open_tag)
|
||||
local end_col = after_tag:find(escaped_close)
|
||||
if end_col then
|
||||
-- Single line prompt
|
||||
current_prompt.content = after_tag:sub(1, end_col - 1)
|
||||
current_prompt.end_line = line_num
|
||||
current_prompt.end_col = start_col + #open_tag + end_col + #close_tag - 2
|
||||
table.insert(prompts, current_prompt)
|
||||
in_prompt = false
|
||||
current_prompt = nil
|
||||
else
|
||||
table.insert(prompt_content, after_tag)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Look for closing tag
|
||||
local end_col = line:find(escaped_close)
|
||||
if end_col then
|
||||
-- Found closing tag
|
||||
local before_tag = line:sub(1, end_col - 1)
|
||||
table.insert(prompt_content, before_tag)
|
||||
current_prompt.content = table.concat(prompt_content, "\n")
|
||||
current_prompt.end_line = line_num
|
||||
current_prompt.end_col = end_col + #close_tag - 1
|
||||
table.insert(prompts, current_prompt)
|
||||
in_prompt = false
|
||||
current_prompt = nil
|
||||
prompt_content = {}
|
||||
else
|
||||
table.insert(prompt_content, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return prompts
|
||||
end
|
||||
|
||||
--- Find prompts in a buffer
|
||||
---@param bufnr number Buffer number
|
||||
---@return CoderPrompt[] List of found prompts
|
||||
function M.find_prompts_in_buffer(bufnr)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
return M.find_prompts(content, config.patterns.open_tag, config.patterns.close_tag)
|
||||
end
|
||||
|
||||
--- Get prompt at cursor position
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return CoderPrompt|nil Prompt at cursor or nil
|
||||
function M.get_prompt_at_cursor(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local line = cursor[1]
|
||||
local col = cursor[2] + 1 -- Convert to 1-indexed
|
||||
|
||||
local prompts = M.find_prompts_in_buffer(bufnr)
|
||||
|
||||
for _, prompt in ipairs(prompts) do
|
||||
if line >= prompt.start_line and line <= prompt.end_line then
|
||||
if line == prompt.start_line and col < prompt.start_col then
|
||||
goto continue
|
||||
end
|
||||
if line == prompt.end_line and col > prompt.end_col then
|
||||
goto continue
|
||||
end
|
||||
return prompt
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get the last closed prompt in buffer
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return CoderPrompt|nil Last prompt or nil
|
||||
function M.get_last_prompt(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local prompts = M.find_prompts_in_buffer(bufnr)
|
||||
|
||||
if #prompts > 0 then
|
||||
return prompts[#prompts]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Extract the prompt type from content
|
||||
---@param content string Prompt content
|
||||
---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type
|
||||
function M.detect_prompt_type(content)
|
||||
local lower = content:lower()
|
||||
|
||||
if lower:match("refactor") then
|
||||
return "refactor"
|
||||
elseif lower:match("add") or lower:match("create") or lower:match("implement") then
|
||||
return "add"
|
||||
elseif lower:match("document") or lower:match("comment") or lower:match("jsdoc") then
|
||||
return "document"
|
||||
elseif lower:match("explain") or lower:match("what") or lower:match("how") then
|
||||
return "explain"
|
||||
end
|
||||
|
||||
return "generic"
|
||||
end
|
||||
|
||||
--- Clean prompt content (trim whitespace, normalize newlines)
|
||||
---@param content string Raw prompt content
|
||||
---@return string Cleaned content
|
||||
function M.clean_prompt(content)
|
||||
-- Trim leading/trailing whitespace
|
||||
content = content:match("^%s*(.-)%s*$")
|
||||
-- Normalize multiple newlines
|
||||
content = content:gsub("\n\n\n+", "\n\n")
|
||||
return content
|
||||
end
|
||||
|
||||
--- Check if line contains a closing tag
|
||||
---@param line string Line to check
|
||||
---@param close_tag string Closing tag
|
||||
---@return boolean
|
||||
function M.has_closing_tag(line, close_tag)
|
||||
return line:find(utils.escape_pattern(close_tag)) ~= nil
|
||||
end
|
||||
|
||||
--- Check if buffer has any unclosed prompts
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return boolean
|
||||
function M.has_unclosed_prompts(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
|
||||
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
|
||||
|
||||
local _, open_count = content:gsub(escaped_open, "")
|
||||
local _, close_count = content:gsub(escaped_close, "")
|
||||
|
||||
return open_count > close_count
|
||||
end
|
||||
|
||||
return M
|
||||
128
lua/codetyper/prompts/ask.lua
Normal file
128
lua/codetyper/prompts/ask.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
---@mod codetyper.prompts.ask Ask/explanation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for the Ask panel and code explanations.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for explaining code
|
||||
M.explain_code = [[Please explain the following code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Provide:
|
||||
1. A high-level overview of what it does
|
||||
2. Explanation of key parts
|
||||
3. Any potential issues or improvements
|
||||
]]
|
||||
|
||||
--- Prompt for explaining a specific function
|
||||
M.explain_function = [[Explain this function in detail:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include:
|
||||
1. What the function does
|
||||
2. Parameters and their purposes
|
||||
3. Return value
|
||||
4. Any side effects
|
||||
5. Usage examples
|
||||
]]
|
||||
|
||||
--- Prompt for explaining an error
|
||||
M.explain_error = [[I'm getting this error:
|
||||
|
||||
{{error}}
|
||||
|
||||
In this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Please explain:
|
||||
1. What the error means
|
||||
2. Why it's happening
|
||||
3. How to fix it
|
||||
]]
|
||||
|
||||
--- Prompt for code review
|
||||
M.code_review = [[Please review this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Provide feedback on:
|
||||
1. Code quality and readability
|
||||
2. Potential bugs or issues
|
||||
3. Performance considerations
|
||||
4. Security concerns (if applicable)
|
||||
5. Suggested improvements
|
||||
]]
|
||||
|
||||
--- Prompt for explaining a concept
|
||||
M.explain_concept = [[Explain the following programming concept:
|
||||
|
||||
{{concept}}
|
||||
|
||||
Include:
|
||||
1. Definition and purpose
|
||||
2. When and why to use it
|
||||
3. Simple code examples
|
||||
4. Common pitfalls to avoid
|
||||
]]
|
||||
|
||||
--- Prompt for comparing approaches
|
||||
M.compare_approaches = [[Compare these different approaches:
|
||||
|
||||
{{approaches}}
|
||||
|
||||
Analyze:
|
||||
1. Pros and cons of each
|
||||
2. Performance implications
|
||||
3. Maintainability
|
||||
4. When to use each approach
|
||||
]]
|
||||
|
||||
--- Prompt for debugging help
|
||||
M.debug_help = [[Help me debug this issue:
|
||||
|
||||
Problem: {{problem}}
|
||||
|
||||
Code:
|
||||
{{code}}
|
||||
|
||||
What I've tried:
|
||||
{{attempts}}
|
||||
|
||||
Please help identify the issue and suggest a solution.
|
||||
]]
|
||||
|
||||
--- Prompt for architecture advice
|
||||
M.architecture_advice = [[I need advice on this architecture decision:
|
||||
|
||||
{{question}}
|
||||
|
||||
Context:
|
||||
{{context}}
|
||||
|
||||
Please provide:
|
||||
1. Recommended approach
|
||||
2. Reasoning
|
||||
3. Potential alternatives
|
||||
4. Things to consider
|
||||
]]
|
||||
|
||||
--- Generic ask prompt
|
||||
M.generic = [[USER QUESTION: {{question}}
|
||||
|
||||
{{#if files}}
|
||||
ATTACHED FILE CONTENTS:
|
||||
{{files}}
|
||||
{{/if}}
|
||||
|
||||
{{#if context}}
|
||||
ADDITIONAL CONTEXT:
|
||||
{{context}}
|
||||
{{/if}}
|
||||
|
||||
Please provide a helpful, accurate response.
|
||||
]]
|
||||
|
||||
return M
|
||||
93
lua/codetyper/prompts/code.lua
Normal file
93
lua/codetyper/prompts/code.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
---@mod codetyper.prompts.code Code generation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for generating new code.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt template for creating a new function
|
||||
M.create_function = [[Create a function with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Follow the coding style of the existing file
|
||||
- Include proper error handling
|
||||
- Use appropriate types (if applicable)
|
||||
- Make it efficient and readable
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a new class/module
|
||||
M.create_class = [[Create a class/module with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Follow OOP best practices
|
||||
- Include constructor/initialization
|
||||
- Implement proper encapsulation
|
||||
- Add necessary methods as described
|
||||
]]
|
||||
|
||||
--- Prompt template for implementing an interface/trait
|
||||
M.implement_interface = [[Implement the following interface/trait:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Implement all required methods
|
||||
- Follow the interface contract exactly
|
||||
- Handle edge cases appropriately
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a React component
|
||||
M.create_react_component = [[Create a React component with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Use functional components with hooks
|
||||
- Include proper TypeScript types (if .tsx)
|
||||
- Follow React best practices
|
||||
- Make it reusable and composable
|
||||
]]
|
||||
|
||||
--- Prompt template for creating an API endpoint
|
||||
M.create_api_endpoint = [[Create an API endpoint with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Include input validation
|
||||
- Proper error handling and status codes
|
||||
- Follow RESTful conventions
|
||||
- Include appropriate middleware
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a utility function
|
||||
M.create_utility = [[Create a utility function:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Pure function (no side effects) if possible
|
||||
- Handle edge cases
|
||||
- Efficient implementation
|
||||
- Well-typed (if applicable)
|
||||
]]
|
||||
|
||||
--- Prompt template for generic code generation
|
||||
M.generic = [[Generate code based on the following description:
|
||||
|
||||
{{description}}
|
||||
|
||||
Context:
|
||||
- Language: {{language}}
|
||||
- File: {{filepath}}
|
||||
|
||||
Requirements:
|
||||
- Match existing code style
|
||||
- Follow best practices
|
||||
- Handle errors appropriately
|
||||
]]
|
||||
|
||||
return M
|
||||
136
lua/codetyper/prompts/document.lua
Normal file
136
lua/codetyper/prompts/document.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
---@mod codetyper.prompts.document Documentation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for generating documentation.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for adding JSDoc comments
|
||||
M.jsdoc = [[Add JSDoc documentation to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Document all functions and methods
|
||||
- Include @param for all parameters
|
||||
- Include @returns for return values
|
||||
- Add @throws if exceptions are thrown
|
||||
- Include @example where helpful
|
||||
- Use @typedef for complex types
|
||||
]]
|
||||
|
||||
--- Prompt for adding Python docstrings
|
||||
M.python_docstring = [[Add docstrings to this Python code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Use Google-style docstrings
|
||||
- Document all functions and classes
|
||||
- Include Args, Returns, Raises sections
|
||||
- Add Examples where helpful
|
||||
- Include type hints in docstrings
|
||||
]]
|
||||
|
||||
--- Prompt for adding LuaDoc comments
|
||||
M.luadoc = [[Add LuaDoc/EmmyLua annotations to this Lua code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Use ---@param for parameters
|
||||
- Use ---@return for return values
|
||||
- Use ---@class for table structures
|
||||
- Use ---@field for class fields
|
||||
- Add descriptions for all items
|
||||
]]
|
||||
|
||||
--- Prompt for adding Go documentation
|
||||
M.godoc = [[Add GoDoc comments to this Go code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Start comments with the name being documented
|
||||
- Document all exported functions, types, and variables
|
||||
- Keep comments concise but complete
|
||||
- Follow Go documentation conventions
|
||||
]]
|
||||
|
||||
--- Prompt for adding README documentation
|
||||
M.readme = [[Generate README documentation for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include:
|
||||
- Project description
|
||||
- Installation instructions
|
||||
- Usage examples
|
||||
- API documentation
|
||||
- Contributing guidelines
|
||||
]]
|
||||
|
||||
--- Prompt for adding inline comments
|
||||
M.inline_comments = [[Add helpful inline comments to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Guidelines:
|
||||
- Explain complex logic
|
||||
- Document non-obvious decisions
|
||||
- Don't state the obvious
|
||||
- Keep comments concise
|
||||
- Use TODO/FIXME where appropriate
|
||||
]]
|
||||
|
||||
--- Prompt for adding API documentation
|
||||
M.api_docs = [[Generate API documentation for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include for each endpoint/function:
|
||||
- Description
|
||||
- Parameters with types
|
||||
- Return value with type
|
||||
- Example request/response
|
||||
- Error cases
|
||||
]]
|
||||
|
||||
--- Prompt for adding type definitions
|
||||
M.type_definitions = [[Generate type definitions for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Define interfaces/types for all data structures
|
||||
- Include optional properties where appropriate
|
||||
- Add JSDoc/docstring descriptions
|
||||
- Export all types that should be public
|
||||
]]
|
||||
|
||||
--- Prompt for changelog entry
|
||||
M.changelog = [[Generate a changelog entry for these changes:
|
||||
|
||||
{{changes}}
|
||||
|
||||
Format:
|
||||
- Use conventional changelog format
|
||||
- Categorize as Added/Changed/Fixed/Removed
|
||||
- Be concise but descriptive
|
||||
- Include breaking changes prominently
|
||||
]]
|
||||
|
||||
--- Generic documentation prompt
|
||||
M.generic = [[Add documentation to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Language: {{language}}
|
||||
|
||||
Requirements:
|
||||
- Use appropriate documentation format for the language
|
||||
- Document all public APIs
|
||||
- Include parameter and return descriptions
|
||||
- Add examples where helpful
|
||||
]]
|
||||
|
||||
return M
|
||||
56
lua/codetyper/prompts/init.lua
Normal file
56
lua/codetyper/prompts/init.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
---@mod codetyper.prompts Prompt templates for Codetyper.nvim
|
||||
---
|
||||
--- This module provides all prompt templates used by the plugin.
|
||||
--- Prompts are organized by functionality and can be customized.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Load all prompt modules
|
||||
M.system = require("codetyper.prompts.system")
|
||||
M.code = require("codetyper.prompts.code")
|
||||
M.ask = require("codetyper.prompts.ask")
|
||||
M.refactor = require("codetyper.prompts.refactor")
|
||||
M.document = require("codetyper.prompts.document")
|
||||
|
||||
--- Get a prompt by category and name
|
||||
---@param category string Category name (system, code, ask, refactor, document)
|
||||
---@param name string Prompt name
|
||||
---@param vars? table Variables to substitute in the prompt
|
||||
---@return string Formatted prompt
|
||||
function M.get(category, name, vars)
|
||||
local prompts = M[category]
|
||||
if not prompts then
|
||||
error("Unknown prompt category: " .. category)
|
||||
end
|
||||
|
||||
local prompt = prompts[name]
|
||||
if not prompt then
|
||||
error("Unknown prompt: " .. category .. "." .. name)
|
||||
end
|
||||
|
||||
-- Substitute variables if provided
|
||||
if vars then
|
||||
for key, value in pairs(vars) do
|
||||
prompt = prompt:gsub("{{" .. key .. "}}", tostring(value))
|
||||
end
|
||||
end
|
||||
|
||||
return prompt
|
||||
end
|
||||
|
||||
--- List all available prompts
|
||||
---@return table Available prompts by category
|
||||
function M.list()
|
||||
local result = {}
|
||||
for category, prompts in pairs(M) do
|
||||
if type(prompts) == "table" and category ~= "list" and category ~= "get" then
|
||||
result[category] = {}
|
||||
for name, _ in pairs(prompts) do
|
||||
table.insert(result[category], name)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return M
|
||||
128
lua/codetyper/prompts/refactor.lua
Normal file
128
lua/codetyper/prompts/refactor.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
---@mod codetyper.prompts.refactor Refactoring prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for code refactoring operations.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for general refactoring
|
||||
M.general = [[Refactor this code to improve its quality:
|
||||
|
||||
{{code}}
|
||||
|
||||
Focus on:
|
||||
- Readability
|
||||
- Maintainability
|
||||
- Following best practices
|
||||
- Keeping the same functionality
|
||||
]]
|
||||
|
||||
--- Prompt for extracting a function
|
||||
M.extract_function = [[Extract a function from this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
The function should:
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Give it a meaningful name
|
||||
- Include proper parameters
|
||||
- Return appropriate values
|
||||
]]
|
||||
|
||||
--- Prompt for simplifying code
|
||||
M.simplify = [[Simplify this code while maintaining functionality:
|
||||
|
||||
{{code}}
|
||||
|
||||
Goals:
|
||||
- Reduce complexity
|
||||
- Remove redundancy
|
||||
- Improve readability
|
||||
- Keep all existing behavior
|
||||
]]
|
||||
|
||||
--- Prompt for converting to async/await
|
||||
M.async_await = [[Convert this code to use async/await:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Convert all promises to async/await
|
||||
- Maintain error handling
|
||||
- Keep the same functionality
|
||||
]]
|
||||
|
||||
--- Prompt for adding error handling
|
||||
M.add_error_handling = [[Add proper error handling to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Handle all potential errors
|
||||
- Use appropriate error types
|
||||
- Add meaningful error messages
|
||||
- Don't change core functionality
|
||||
]]
|
||||
|
||||
--- Prompt for improving performance
|
||||
M.optimize_performance = [[Optimize this code for better performance:
|
||||
|
||||
{{code}}
|
||||
|
||||
Focus on:
|
||||
- Algorithm efficiency
|
||||
- Memory usage
|
||||
- Reducing unnecessary operations
|
||||
- Maintaining readability
|
||||
]]
|
||||
|
||||
--- Prompt for converting to TypeScript
|
||||
M.convert_to_typescript = [[Convert this JavaScript code to TypeScript:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Add proper type annotations
|
||||
- Use interfaces where appropriate
|
||||
- Handle null/undefined properly
|
||||
- Maintain all functionality
|
||||
]]
|
||||
|
||||
--- Prompt for applying design pattern
|
||||
M.apply_pattern = [[Refactor this code to use the {{pattern}} pattern:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Properly implement the pattern
|
||||
- Maintain existing functionality
|
||||
- Improve code organization
|
||||
]]
|
||||
|
||||
--- Prompt for splitting a large function
|
||||
M.split_function = [[Split this large function into smaller, focused functions:
|
||||
|
||||
{{code}}
|
||||
|
||||
Goals:
|
||||
- Single responsibility per function
|
||||
- Clear function names
|
||||
- Proper parameter passing
|
||||
- Maintain all functionality
|
||||
]]
|
||||
|
||||
--- Prompt for removing code smells
|
||||
M.remove_code_smells = [[Refactor this code to remove code smells:
|
||||
|
||||
{{code}}
|
||||
|
||||
Look for and fix:
|
||||
- Long methods
|
||||
- Duplicated code
|
||||
- Magic numbers
|
||||
- Deep nesting
|
||||
- Other anti-patterns
|
||||
]]
|
||||
|
||||
return M
|
||||
96
lua/codetyper/prompts/system.lua
Normal file
96
lua/codetyper/prompts/system.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
---@mod codetyper.prompts.system System prompts for Codetyper.nvim
|
||||
---
|
||||
--- These are the base system prompts that define the AI's behavior.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Base system prompt for code generation
|
||||
M.code_generation = [[You are an expert code generation assistant integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate high-quality, production-ready code based on the user's prompt.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the code - no explanations, no markdown code blocks, no comments about what you did
|
||||
2. Match the coding style, conventions, and patterns of the existing file
|
||||
3. Use proper indentation and formatting for the language
|
||||
4. Follow best practices for the specific language/framework
|
||||
5. Preserve existing functionality unless explicitly asked to change it
|
||||
6. Use meaningful variable and function names
|
||||
7. Handle edge cases and errors appropriately
|
||||
|
||||
Language: {{language}}
|
||||
File: {{filepath}}
|
||||
]]
|
||||
|
||||
--- System prompt for code explanation/ask
|
||||
M.ask = [[You are a helpful coding assistant integrated into Neovim via Codetyper.nvim.
|
||||
You help developers understand code, explain concepts, and answer programming questions.
|
||||
|
||||
GUIDELINES:
|
||||
1. Be concise but thorough in your explanations
|
||||
2. Use code examples when helpful
|
||||
3. Reference the provided code context in your explanations
|
||||
4. Format responses in markdown for readability
|
||||
5. If you don't know something, say so honestly
|
||||
6. Break down complex concepts into understandable parts
|
||||
7. Provide practical, actionable advice
|
||||
|
||||
IMPORTANT: When file contents are provided, analyze them carefully and base your response on the actual code.
|
||||
]]
|
||||
|
||||
--- System prompt for refactoring
|
||||
M.refactor = [[You are an expert code refactoring assistant integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to refactor code while maintaining its functionality.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the refactored code - no explanations
|
||||
2. Preserve ALL existing functionality
|
||||
3. Improve code quality, readability, and maintainability
|
||||
4. Follow SOLID principles and best practices
|
||||
5. Keep the same coding style as the original
|
||||
6. Do not add new features unless explicitly requested
|
||||
7. Optimize performance where possible without sacrificing readability
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
--- System prompt for documentation
|
||||
M.document = [[You are a documentation expert integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate clear, comprehensive documentation for code.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the documentation/comments - ready to be inserted into code
|
||||
2. Use the appropriate documentation format for the language:
|
||||
- JavaScript/TypeScript: JSDoc
|
||||
- Python: Docstrings (Google or NumPy style)
|
||||
- Lua: LuaDoc/EmmyLua
|
||||
- Go: GoDoc
|
||||
- Rust: RustDoc
|
||||
- Java: Javadoc
|
||||
3. Document all parameters, return values, and exceptions
|
||||
4. Include usage examples where helpful
|
||||
5. Be concise but complete
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
--- System prompt for test generation
|
||||
M.test = [[You are a test generation expert integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate comprehensive unit tests for the provided code.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the test code - no explanations
|
||||
2. Use the appropriate testing framework for the language:
|
||||
- JavaScript/TypeScript: Jest or Vitest
|
||||
- Python: pytest
|
||||
- Lua: busted or plenary
|
||||
- Go: testing package
|
||||
- Rust: built-in tests
|
||||
3. Cover happy paths, edge cases, and error scenarios
|
||||
4. Use descriptive test names
|
||||
5. Follow AAA pattern: Arrange, Act, Assert
|
||||
6. Mock external dependencies appropriately
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
return M
|
||||
245
lua/codetyper/tree.lua
Normal file
245
lua/codetyper/tree.lua
Normal file
@@ -0,0 +1,245 @@
|
||||
---@mod codetyper.tree Project tree logging for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Name of the coder folder
|
||||
local CODER_FOLDER = ".coder"
|
||||
|
||||
--- Name of the tree log file
|
||||
local TREE_LOG_FILE = "tree.log"
|
||||
|
||||
--- Get the path to the .coder folder
|
||||
---@return string|nil Path to .coder folder or nil
|
||||
function M.get_coder_folder()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return nil
|
||||
end
|
||||
return root .. "/" .. CODER_FOLDER
|
||||
end
|
||||
|
||||
--- Get the path to the tree.log file
|
||||
---@return string|nil Path to tree.log or nil
|
||||
function M.get_tree_log_path()
|
||||
local coder_folder = M.get_coder_folder()
|
||||
if not coder_folder then
|
||||
return nil
|
||||
end
|
||||
return coder_folder .. "/" .. TREE_LOG_FILE
|
||||
end
|
||||
|
||||
--- Ensure .coder folder exists
|
||||
---@return boolean Success status
|
||||
function M.ensure_coder_folder()
|
||||
local coder_folder = M.get_coder_folder()
|
||||
if not coder_folder then
|
||||
return false
|
||||
end
|
||||
return utils.ensure_dir(coder_folder)
|
||||
end
|
||||
|
||||
--- Build tree structure recursively
|
||||
---@param path string Directory path
|
||||
---@param prefix string Prefix for tree lines
|
||||
---@param ignore_patterns table Patterns to ignore
|
||||
---@return string[] Tree lines
|
||||
local function build_tree(path, prefix, ignore_patterns)
|
||||
local lines = {}
|
||||
local entries = {}
|
||||
|
||||
-- Get directory entries
|
||||
local handle = vim.loop.fs_scandir(path)
|
||||
if not handle then
|
||||
return lines
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
|
||||
-- Check if should be ignored
|
||||
local should_ignore = false
|
||||
for _, pattern in ipairs(ignore_patterns) do
|
||||
if name:match(pattern) then
|
||||
should_ignore = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not should_ignore then
|
||||
table.insert(entries, { name = name, type = type })
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort entries (directories first, then alphabetically)
|
||||
table.sort(entries, function(a, b)
|
||||
if a.type == "directory" and b.type ~= "directory" then
|
||||
return true
|
||||
elseif a.type ~= "directory" and b.type == "directory" then
|
||||
return false
|
||||
end
|
||||
return a.name < b.name
|
||||
end)
|
||||
|
||||
-- Build tree lines
|
||||
for i, entry in ipairs(entries) do
|
||||
local is_last = i == #entries
|
||||
local connector = is_last and "└── " or "├── "
|
||||
local child_prefix = is_last and " " or "│ "
|
||||
|
||||
local icon = ""
|
||||
if entry.type == "directory" then
|
||||
icon = "📁 "
|
||||
else
|
||||
-- File type icons
|
||||
local ext = entry.name:match("%.([^%.]+)$")
|
||||
local icons = {
|
||||
lua = "🌙 ",
|
||||
ts = "📘 ",
|
||||
tsx = "⚛️ ",
|
||||
js = "📒 ",
|
||||
jsx = "⚛️ ",
|
||||
py = "🐍 ",
|
||||
go = "🐹 ",
|
||||
rs = "🦀 ",
|
||||
md = "📝 ",
|
||||
json = "📋 ",
|
||||
yaml = "📋 ",
|
||||
yml = "📋 ",
|
||||
html = "🌐 ",
|
||||
css = "🎨 ",
|
||||
scss = "🎨 ",
|
||||
}
|
||||
icon = icons[ext] or "📄 "
|
||||
end
|
||||
|
||||
table.insert(lines, prefix .. connector .. icon .. entry.name)
|
||||
|
||||
if entry.type == "directory" then
|
||||
local child_path = path .. "/" .. entry.name
|
||||
local child_lines = build_tree(child_path, prefix .. child_prefix, ignore_patterns)
|
||||
for _, line in ipairs(child_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Generate project tree
|
||||
---@return string Tree content
|
||||
function M.generate_tree()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return "-- Could not determine project root --"
|
||||
end
|
||||
|
||||
local project_name = vim.fn.fnamemodify(root, ":t")
|
||||
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
-- Patterns to ignore
|
||||
local ignore_patterns = {
|
||||
"^%.", -- Hidden files/folders
|
||||
"^node_modules$",
|
||||
"^__pycache__$",
|
||||
"^%.git$",
|
||||
"^%.coder$",
|
||||
"^dist$",
|
||||
"^build$",
|
||||
"^target$",
|
||||
"^vendor$",
|
||||
"%.coder%.", -- Coder files
|
||||
}
|
||||
|
||||
local lines = {
|
||||
"# Project Tree: " .. project_name,
|
||||
"# Generated: " .. timestamp,
|
||||
"# By: Codetyper.nvim",
|
||||
"",
|
||||
"📦 " .. project_name,
|
||||
}
|
||||
|
||||
local tree_lines = build_tree(root, "", ignore_patterns)
|
||||
for _, line in ipairs(tree_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "# Total files tracked by Codetyper")
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Update the tree.log file
|
||||
---@return boolean Success status
|
||||
function M.update_tree_log()
|
||||
-- Ensure .coder folder exists
|
||||
if not M.ensure_coder_folder() then
|
||||
return false
|
||||
end
|
||||
|
||||
local tree_log_path = M.get_tree_log_path()
|
||||
if not tree_log_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local tree_content = M.generate_tree()
|
||||
|
||||
if utils.write_file(tree_log_path, tree_content) then
|
||||
-- Silent update, no notification needed for every file change
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Initialize tree logging (called on setup)
|
||||
function M.setup()
|
||||
-- Create initial tree log
|
||||
M.update_tree_log()
|
||||
end
|
||||
|
||||
--- Get file statistics from tree
|
||||
---@return table Statistics { files: number, directories: number }
|
||||
function M.get_stats()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return { files = 0, directories = 0 }
|
||||
end
|
||||
|
||||
local stats = { files = 0, directories = 0 }
|
||||
|
||||
local function count_recursive(path)
|
||||
local handle = vim.loop.fs_scandir(path)
|
||||
if not handle then
|
||||
return
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
|
||||
-- Skip hidden and special folders
|
||||
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.coder%.") then
|
||||
if type == "directory" then
|
||||
stats.directories = stats.directories + 1
|
||||
count_recursive(path .. "/" .. name)
|
||||
else
|
||||
stats.files = stats.files + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
count_recursive(root)
|
||||
return stats
|
||||
end
|
||||
|
||||
return M
|
||||
44
lua/codetyper/types.lua
Normal file
44
lua/codetyper/types.lua
Normal file
@@ -0,0 +1,44 @@
|
||||
---@mod codetyper.types Type definitions for Codetyper.nvim
|
||||
|
||||
---@class CoderConfig
|
||||
---@field llm LLMConfig LLM provider configuration
|
||||
---@field window WindowConfig Window configuration
|
||||
---@field patterns PatternConfig Pattern configuration
|
||||
---@field auto_gitignore boolean Auto-manage .gitignore
|
||||
|
||||
---@class LLMConfig
|
||||
---@field provider "claude" | "ollama" The LLM provider to use
|
||||
---@field claude ClaudeConfig Claude-specific configuration
|
||||
---@field ollama OllamaConfig Ollama-specific configuration
|
||||
|
||||
---@class ClaudeConfig
|
||||
---@field api_key string | nil Claude API key (or env var ANTHROPIC_API_KEY)
|
||||
---@field model string Claude model to use
|
||||
|
||||
---@class OllamaConfig
|
||||
---@field host string Ollama host URL
|
||||
---@field model string Ollama model to use
|
||||
|
||||
---@class WindowConfig
|
||||
---@field width number Width of the coder window (percentage or columns)
|
||||
---@field position "left" | "right" Position of the coder window
|
||||
---@field border string Border style for floating windows
|
||||
|
||||
---@class PatternConfig
|
||||
---@field open_tag string Opening tag for prompts
|
||||
---@field close_tag string Closing tag for prompts
|
||||
---@field file_pattern string Pattern for coder files
|
||||
|
||||
---@class CoderPrompt
|
||||
---@field content string The prompt content between tags
|
||||
---@field start_line number Starting line number
|
||||
---@field end_line number Ending line number
|
||||
---@field start_col number Starting column
|
||||
---@field end_col number Ending column
|
||||
|
||||
---@class CoderFile
|
||||
---@field coder_path string Path to the .coder.* file
|
||||
---@field target_path string Path to the target file
|
||||
---@field filetype string The filetype/extension
|
||||
|
||||
return {}
|
||||
125
lua/codetyper/utils.lua
Normal file
125
lua/codetyper/utils.lua
Normal file
@@ -0,0 +1,125 @@
|
||||
---@mod codetyper.utils Utility functions for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Get the project root directory
|
||||
---@return string|nil Root directory path or nil if not found
|
||||
function M.get_project_root()
|
||||
-- Try to find common root indicators
|
||||
local markers = { ".git", ".gitignore", "package.json", "Cargo.toml", "go.mod", "pyproject.toml" }
|
||||
|
||||
local current = vim.fn.getcwd()
|
||||
|
||||
for _, marker in ipairs(markers) do
|
||||
local found = vim.fn.findfile(marker, current .. ";")
|
||||
if found ~= "" then
|
||||
return vim.fn.fnamemodify(found, ":p:h")
|
||||
end
|
||||
found = vim.fn.finddir(marker, current .. ";")
|
||||
if found ~= "" then
|
||||
return vim.fn.fnamemodify(found, ":p:h")
|
||||
end
|
||||
end
|
||||
|
||||
return current
|
||||
end
|
||||
|
||||
--- Check if a file is a coder file
|
||||
---@param filepath string File path to check
|
||||
---@return boolean
|
||||
function M.is_coder_file(filepath)
|
||||
return filepath:match("%.coder%.") ~= nil
|
||||
end
|
||||
|
||||
--- Get the target file path from a coder file path
|
||||
---@param coder_path string Path to the coder file
|
||||
---@return string Target file path
|
||||
function M.get_target_path(coder_path)
|
||||
-- Convert index.coder.ts -> index.ts
|
||||
return coder_path:gsub("%.coder%.", ".")
|
||||
end
|
||||
|
||||
--- Get the coder file path from a target file path
|
||||
---@param target_path string Path to the target file
|
||||
---@return string Coder file path
|
||||
function M.get_coder_path(target_path)
|
||||
-- Convert index.ts -> index.coder.ts
|
||||
local dir = vim.fn.fnamemodify(target_path, ":h")
|
||||
local name = vim.fn.fnamemodify(target_path, ":t:r")
|
||||
local ext = vim.fn.fnamemodify(target_path, ":e")
|
||||
|
||||
if dir == "." then
|
||||
return name .. ".coder." .. ext
|
||||
end
|
||||
return dir .. "/" .. name .. ".coder." .. ext
|
||||
end
|
||||
|
||||
--- Check if a file exists
|
||||
---@param filepath string File path to check
|
||||
---@return boolean
|
||||
function M.file_exists(filepath)
|
||||
local stat = vim.loop.fs_stat(filepath)
|
||||
return stat ~= nil
|
||||
end
|
||||
|
||||
--- Read file contents
|
||||
---@param filepath string File path to read
|
||||
---@return string|nil Contents or nil if error
|
||||
function M.read_file(filepath)
|
||||
local file = io.open(filepath, "r")
|
||||
if not file then
|
||||
return nil
|
||||
end
|
||||
local content = file:read("*all")
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
--- Write content to file
|
||||
---@param filepath string File path to write
|
||||
---@param content string Content to write
|
||||
---@return boolean Success status
|
||||
function M.write_file(filepath, content)
|
||||
local file = io.open(filepath, "w")
|
||||
if not file then
|
||||
return false
|
||||
end
|
||||
file:write(content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
--- Create directory if it doesn't exist
|
||||
---@param dirpath string Directory path
|
||||
---@return boolean Success status
|
||||
function M.ensure_dir(dirpath)
|
||||
if vim.fn.isdirectory(dirpath) == 0 then
|
||||
return vim.fn.mkdir(dirpath, "p") == 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Notify user with proper formatting
|
||||
---@param msg string Message to display
|
||||
---@param level? number Vim log level (default: INFO)
|
||||
function M.notify(msg, level)
|
||||
level = level or vim.log.levels.INFO
|
||||
vim.notify("[Codetyper] " .. msg, level)
|
||||
end
|
||||
|
||||
--- Get buffer filetype
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return string Filetype
|
||||
function M.get_filetype(bufnr)
|
||||
bufnr = bufnr or 0
|
||||
return vim.bo[bufnr].filetype
|
||||
end
|
||||
|
||||
--- Escape pattern special characters
|
||||
---@param str string String to escape
|
||||
---@return string Escaped string
|
||||
function M.escape_pattern(str)
|
||||
return str:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
|
||||
end
|
||||
|
||||
return M
|
||||
177
lua/codetyper/window.lua
Normal file
177
lua/codetyper/window.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
---@mod codetyper.window Window management for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
---@type number|nil Current coder window ID
|
||||
M._coder_win = nil
|
||||
|
||||
---@type number|nil Current target window ID
|
||||
M._target_win = nil
|
||||
|
||||
---@type number|nil Current coder buffer ID
|
||||
M._coder_buf = nil
|
||||
|
||||
---@type number|nil Current target buffer ID
|
||||
M._target_buf = nil
|
||||
|
||||
--- Calculate window width based on configuration
|
||||
---@param config CoderConfig Plugin configuration
|
||||
---@return number Width in columns
|
||||
local function calculate_width(config)
|
||||
local width = config.window.width
|
||||
if width <= 1 then
|
||||
-- Percentage of total width
|
||||
return math.floor(vim.o.columns * width)
|
||||
end
|
||||
return math.floor(width)
|
||||
end
|
||||
|
||||
--- Open the coder split view
|
||||
---@param target_path string Path to the target file
|
||||
---@param coder_path string Path to the coder file
|
||||
---@return boolean Success status
|
||||
function M.open_split(target_path, coder_path)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
-- Ensure coder file exists, create if not
|
||||
if not utils.file_exists(coder_path) then
|
||||
local dir = vim.fn.fnamemodify(coder_path, ":h")
|
||||
utils.ensure_dir(dir)
|
||||
utils.write_file(coder_path, "")
|
||||
|
||||
-- Ensure gitignore is updated when creating a new coder file
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.ensure_ignored()
|
||||
end
|
||||
|
||||
-- Store current window as target window
|
||||
M._target_win = vim.api.nvim_get_current_win()
|
||||
M._target_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Open target file if not already open
|
||||
if vim.fn.expand("%:p") ~= target_path then
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(target_path))
|
||||
M._target_buf = vim.api.nvim_get_current_buf()
|
||||
end
|
||||
|
||||
-- Calculate width
|
||||
local width = calculate_width(config)
|
||||
|
||||
-- Create the coder split
|
||||
if config.window.position == "left" then
|
||||
vim.cmd("topleft vsplit " .. vim.fn.fnameescape(coder_path))
|
||||
else
|
||||
vim.cmd("botright vsplit " .. vim.fn.fnameescape(coder_path))
|
||||
end
|
||||
|
||||
-- Store coder window reference
|
||||
M._coder_win = vim.api.nvim_get_current_win()
|
||||
M._coder_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Set coder window width
|
||||
vim.api.nvim_win_set_width(M._coder_win, width)
|
||||
|
||||
-- Set up window options for coder window
|
||||
vim.wo[M._coder_win].number = true
|
||||
vim.wo[M._coder_win].relativenumber = true
|
||||
vim.wo[M._coder_win].wrap = true
|
||||
vim.wo[M._coder_win].signcolumn = "yes"
|
||||
|
||||
-- Focus on target window (right side) by default
|
||||
if config.window.position == "left" then
|
||||
vim.api.nvim_set_current_win(M._target_win)
|
||||
end
|
||||
|
||||
utils.notify("Coder view opened: " .. vim.fn.fnamemodify(coder_path, ":t"))
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Close the coder split view
|
||||
---@return boolean Success status
|
||||
function M.close_split()
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
vim.api.nvim_win_close(M._coder_win, false)
|
||||
M._coder_win = nil
|
||||
M._coder_buf = nil
|
||||
utils.notify("Coder view closed")
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Toggle the coder split view
|
||||
---@param target_path? string Path to the target file
|
||||
---@param coder_path? string Path to the coder file
|
||||
function M.toggle_split(target_path, coder_path)
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
M.close_split()
|
||||
else
|
||||
if target_path and coder_path then
|
||||
M.open_split(target_path, coder_path)
|
||||
else
|
||||
utils.notify("No file specified for coder view", vim.log.levels.WARN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if coder view is currently open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return M._coder_win ~= nil and vim.api.nvim_win_is_valid(M._coder_win)
|
||||
end
|
||||
|
||||
--- Get current coder window ID
|
||||
---@return number|nil
|
||||
function M.get_coder_win()
|
||||
return M._coder_win
|
||||
end
|
||||
|
||||
--- Get current target window ID
|
||||
---@return number|nil
|
||||
function M.get_target_win()
|
||||
return M._target_win
|
||||
end
|
||||
|
||||
--- Get current coder buffer ID
|
||||
---@return number|nil
|
||||
function M.get_coder_buf()
|
||||
return M._coder_buf
|
||||
end
|
||||
|
||||
--- Get current target buffer ID
|
||||
---@return number|nil
|
||||
function M.get_target_buf()
|
||||
return M._target_buf
|
||||
end
|
||||
|
||||
--- Focus on the coder window
|
||||
function M.focus_coder()
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
vim.api.nvim_set_current_win(M._coder_win)
|
||||
end
|
||||
end
|
||||
|
||||
--- Focus on the target window
|
||||
function M.focus_target()
|
||||
if M._target_win and vim.api.nvim_win_is_valid(M._target_win) then
|
||||
vim.api.nvim_set_current_win(M._target_win)
|
||||
end
|
||||
end
|
||||
|
||||
--- Sync scroll between windows (optional feature)
|
||||
---@param enable boolean Enable or disable sync scroll
|
||||
function M.sync_scroll(enable)
|
||||
if not M.is_open() then
|
||||
return
|
||||
end
|
||||
|
||||
local value = enable and "scrollbind" or "noscrollbind"
|
||||
vim.wo[M._coder_win][value] = enable
|
||||
vim.wo[M._target_win][value] = enable
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user