refactoring the context
This commit is contained in:
@@ -4,38 +4,36 @@
|
||||
local function get_brain_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local ok_brain, brain = pcall(require, "codetyper.brain")
|
||||
if not ok_brain then
|
||||
local brain_loaded, brain = pcall(require, "codetyper.brain")
|
||||
if not brain_loaded then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Check if brain is initialized safely
|
||||
local is_init = false
|
||||
local brain_initialized = false
|
||||
if brain.is_initialized then
|
||||
local ok, result = pcall(brain.is_initialized)
|
||||
is_init = ok and result
|
||||
local init_check_success, init_state = pcall(brain.is_initialized)
|
||||
brain_initialized = init_check_success and init_state
|
||||
end
|
||||
|
||||
if not is_init then
|
||||
if not brain_initialized then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Query brain for relevant patterns
|
||||
local ok_query, result = pcall(brain.query, {
|
||||
local query_success, query_result = pcall(brain.query, {
|
||||
query = prefix,
|
||||
max_results = 10,
|
||||
types = { "pattern" },
|
||||
})
|
||||
|
||||
if ok_query and result and result.nodes then
|
||||
for _, node in ipairs(result.nodes) do
|
||||
if query_success and query_result and query_result.nodes then
|
||||
for _, node in ipairs(query_result.nodes) do
|
||||
if node.c and node.c.s then
|
||||
local summary = node.c.s
|
||||
for name in summary:gmatch("functions:%s*([^;]+)") do
|
||||
for func in name:gmatch("([%w_]+)") do
|
||||
if func:lower():find(prefix:lower(), 1, true) then
|
||||
for matched_functions in summary:gmatch("functions:%s*([^;]+)") do
|
||||
for func_name in matched_functions:gmatch("([%w_]+)") do
|
||||
if func_name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func,
|
||||
label = func_name,
|
||||
kind = 3, -- Function
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
@@ -43,11 +41,11 @@ local function get_brain_completions(prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
for name in summary:gmatch("classes:%s*([^;]+)") do
|
||||
for class in name:gmatch("([%w_]+)") do
|
||||
if class:lower():find(prefix:lower(), 1, true) then
|
||||
for matched_classes in summary:gmatch("classes:%s*([^;]+)") do
|
||||
for class_name in matched_classes:gmatch("([%w_]+)") do
|
||||
if class_name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class,
|
||||
label = class_name,
|
||||
kind = 7, -- Class
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
|
||||
@@ -2,24 +2,18 @@
|
||||
---@param prefix string
|
||||
---@return string|nil suggestion
|
||||
local function get_copilot_suggestion(prefix)
|
||||
-- Try copilot.lua suggestion API first
|
||||
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
|
||||
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
|
||||
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
|
||||
if ok2 and suggestion and suggestion ~= "" then
|
||||
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
|
||||
return suggestion
|
||||
else
|
||||
return suggestion
|
||||
end
|
||||
local suggestion_api_loaded, copilot_suggestion_api = pcall(require, "copilot.suggestion")
|
||||
if suggestion_api_loaded and copilot_suggestion_api and type(copilot_suggestion_api.get_suggestion) == "function" then
|
||||
local suggestion_fetch_success, suggestion = pcall(copilot_suggestion_api.get_suggestion)
|
||||
if suggestion_fetch_success and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: try older copilot module if present
|
||||
local ok3, copilot = pcall(require, "copilot")
|
||||
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
|
||||
local ok4, suggestion = pcall(copilot.get_suggestion)
|
||||
if ok4 and suggestion and suggestion ~= "" then
|
||||
local copilot_loaded, copilot = pcall(require, "copilot")
|
||||
if copilot_loaded and copilot and type(copilot.get_suggestion) == "function" then
|
||||
local suggestion_fetch_success, suggestion = pcall(copilot.get_suggestion)
|
||||
if suggestion_fetch_success and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,54 +4,52 @@
|
||||
local function get_indexer_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local ok_indexer, indexer = pcall(require, "codetyper.indexer")
|
||||
if not ok_indexer then
|
||||
local indexer_loaded, indexer = pcall(require, "codetyper.indexer")
|
||||
if not indexer_loaded then
|
||||
return items
|
||||
end
|
||||
|
||||
local ok_load, index = pcall(indexer.load_index)
|
||||
if not ok_load or not index then
|
||||
local index_load_success, index = pcall(indexer.load_index)
|
||||
if not index_load_success or not index then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Search symbols
|
||||
if index.symbols then
|
||||
for symbol, files in pairs(index.symbols) do
|
||||
if symbol:lower():find(prefix:lower(), 1, true) then
|
||||
local files_str = type(files) == "table" and table.concat(files, ", ") or tostring(files)
|
||||
local files_display = type(files) == "table" and table.concat(files, ", ") or tostring(files)
|
||||
table.insert(items, {
|
||||
label = symbol,
|
||||
kind = 6, -- Variable (generic)
|
||||
detail = "[index] " .. files_str:sub(1, 30),
|
||||
documentation = "Symbol found in: " .. files_str,
|
||||
detail = "[index] " .. files_display:sub(1, 30),
|
||||
documentation = "Symbol found in: " .. files_display,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Search functions in files
|
||||
if index.files then
|
||||
for filepath, file_index in pairs(index.files) do
|
||||
if file_index and file_index.functions then
|
||||
for _, func in ipairs(file_index.functions) do
|
||||
if func.name and func.name:lower():find(prefix:lower(), 1, true) then
|
||||
for _, func_entry in ipairs(file_index.functions) do
|
||||
if func_entry.name and func_entry.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func.name,
|
||||
label = func_entry.name,
|
||||
kind = 3, -- Function
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = func.docstring or ("Function at line " .. (func.line or "?")),
|
||||
documentation = func_entry.docstring or ("Function at line " .. (func_entry.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
if file_index and file_index.classes then
|
||||
for _, class in ipairs(file_index.classes) do
|
||||
if class.name and class.name:lower():find(prefix:lower(), 1, true) then
|
||||
for _, class_entry in ipairs(file_index.classes) do
|
||||
if class_entry.name and class_entry.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class.name,
|
||||
label = class_entry.name,
|
||||
kind = 7, -- Class
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = class.docstring or ("Class at line " .. (class.line or "?")),
|
||||
documentation = class_entry.docstring or ("Class at line " .. (class_entry.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,8 +30,8 @@ function source:complete(params, callback)
|
||||
local seen = {}
|
||||
|
||||
-- Get brain completions (highest priority)
|
||||
local ok1, brain_items = pcall(get_brain_completions, prefix)
|
||||
if ok1 and brain_items then
|
||||
local brain_completions_success, brain_items = pcall(get_brain_completions, prefix)
|
||||
if brain_completions_success and brain_items then
|
||||
for _, item in ipairs(brain_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
@@ -42,8 +42,8 @@ function source:complete(params, callback)
|
||||
end
|
||||
|
||||
-- Get indexer completions
|
||||
local ok2, indexer_items = pcall(get_indexer_completions, prefix)
|
||||
if ok2 and indexer_items then
|
||||
local indexer_completions_success, indexer_items = pcall(get_indexer_completions, prefix)
|
||||
if indexer_completions_success and indexer_items then
|
||||
for _, item in ipairs(indexer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
@@ -56,8 +56,8 @@ function source:complete(params, callback)
|
||||
-- Get buffer completions as fallback (lower priority)
|
||||
local bufnr = params.context.bufnr
|
||||
if bufnr then
|
||||
local ok3, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
|
||||
if ok3 and buffer_items then
|
||||
local buffer_completions_success, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
|
||||
if buffer_completions_success and buffer_items then
|
||||
for _, item in ipairs(buffer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
@@ -69,12 +69,12 @@ function source:complete(params, callback)
|
||||
end
|
||||
|
||||
-- If Copilot is installed, prefer its suggestion as a top-priority completion
|
||||
local ok_cp, _ = pcall(require, "copilot")
|
||||
if ok_cp then
|
||||
local copilot_installed = pcall(require, "copilot")
|
||||
if copilot_installed then
|
||||
local suggestion = nil
|
||||
local ok_sug, res = pcall(get_copilot_suggestion, prefix)
|
||||
if ok_sug then
|
||||
suggestion = res
|
||||
local copilot_suggestion_success, copilot_suggestion_result = pcall(get_copilot_suggestion, prefix)
|
||||
if copilot_suggestion_success then
|
||||
suggestion = copilot_suggestion_result
|
||||
end
|
||||
if suggestion and suggestion ~= "" then
|
||||
local first_line = suggestion:match("([^\n]+)") or suggestion
|
||||
@@ -115,8 +115,8 @@ end
|
||||
--- Check if source is registered
|
||||
---@return boolean
|
||||
function M.is_registered()
|
||||
local ok, cmp = pcall(require, "cmp")
|
||||
if not ok then
|
||||
local cmp_loaded, cmp = pcall(require, "cmp")
|
||||
if not cmp_loaded then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@@ -6,395 +6,9 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ContextModalState
|
||||
---@field buf number|nil Buffer number
|
||||
---@field win number|nil Window number
|
||||
---@field original_event table|nil Original prompt event
|
||||
---@field callback function|nil Callback with additional context
|
||||
---@field llm_response string|nil LLM's response asking for context
|
||||
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
original_event = nil,
|
||||
callback = nil,
|
||||
llm_response = nil,
|
||||
attached_files = nil,
|
||||
}
|
||||
|
||||
--- Close the context modal
|
||||
function M.close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
--- Submit the additional context
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
-- Trim whitespace
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
M.close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
|
||||
M.close()
|
||||
|
||||
if callback and original_event then
|
||||
-- Pass attached_files as third optional parameter
|
||||
callback(original_event, additional_context, state.attached_files)
|
||||
end
|
||||
end
|
||||
|
||||
--- Parse requested file paths from LLM response and resolve to full paths
|
||||
local function parse_requested_files(response)
|
||||
if not response or response == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local candidates = {}
|
||||
local seen = {}
|
||||
|
||||
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Resolve to full paths using cwd and glob
|
||||
local resolved = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local full = nil
|
||||
if p:sub(1, 1) == "/" and vim.fn.filereadable(p) == 1 then
|
||||
full = p
|
||||
else
|
||||
local try1 = cwd .. "/" .. p
|
||||
if vim.fn.filereadable(try1) == 1 then
|
||||
full = try1
|
||||
else
|
||||
local tail = p:match("[^/]+$") or p
|
||||
local matches = vim.fn.globpath(cwd, "**/" .. tail, false, true)
|
||||
if matches and #matches > 0 then
|
||||
full = matches[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
if full and vim.fn.filereadable(full) == 1 then
|
||||
table.insert(resolved, full)
|
||||
end
|
||||
end
|
||||
return resolved
|
||||
end
|
||||
|
||||
--- Attach parsed files into the modal buffer and remember them for submission
|
||||
local function attach_requested_files()
|
||||
if not state.llm_response or state.llm_response == "" then
|
||||
return
|
||||
end
|
||||
local files = parse_requested_files(state.llm_response)
|
||||
if #files == 0 then
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
|
||||
return
|
||||
end
|
||||
|
||||
state.attached_files = state.attached_files or {}
|
||||
|
||||
for _, full in ipairs(files) do
|
||||
local ok, lines = pcall(vim.fn.readfile, full)
|
||||
if ok and lines and #lines > 0 then
|
||||
table.insert(
|
||||
state.attached_files,
|
||||
{ path = vim.fn.fnamemodify(full, ":~:."), full_path = full, content = table.concat(lines, "\n") }
|
||||
)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. full .. " --" })
|
||||
for i, l in ipairs(lines) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1 + i, insert_at + 1 + i, false, { l })
|
||||
end
|
||||
else
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Failed to read: " .. full .. " --" })
|
||||
end
|
||||
end
|
||||
-- Move cursor to end and enter insert mode
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string, attached_files?: table)
|
||||
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
|
||||
function M.open(original_event, llm_response, callback, suggested_commands)
|
||||
-- Close any existing modal
|
||||
M.close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
-- Calculate window size
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
-- Create buffer
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
-- Create window
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
-- Set window options
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
|
||||
-- Add header showing what the LLM said
|
||||
local header_lines = {
|
||||
ui_prompts.llm_response_header,
|
||||
}
|
||||
|
||||
-- Truncate LLM response for display
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
-- If suggested commands were provided, show them in the header
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.suggested_commands_header)
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local label = s.label or s.cmd
|
||||
table.insert(header_lines, string.format("[%d] %s: %s", i, label, s.cmd))
|
||||
end
|
||||
table.insert(header_lines, ui_prompts.commands_hint)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.input_header)
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
|
||||
-- Move cursor to the end
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
-- Set up keymaps
|
||||
local opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
-- Submit with Ctrl+Enter or <leader>s
|
||||
vim.keymap.set("n", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, opts)
|
||||
|
||||
-- Attach parsed files (from LLM response)
|
||||
vim.keymap.set("n", "a", function()
|
||||
attach_requested_files()
|
||||
end, opts)
|
||||
|
||||
-- Confirm and submit with 'c' (convenient when doing question round)
|
||||
vim.keymap.set("n", "c", submit, opts)
|
||||
|
||||
-- Quick run of project inspection from modal with <leader>r / <C-r> in insert mode
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, opts)
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
|
||||
-- If suggested commands provided, create per-command keymaps <leader>1..n to run them
|
||||
state.suggested_commands = suggested_commands
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local key = "<leader>" .. tostring(i)
|
||||
vim.keymap.set("n", key, function()
|
||||
-- run this single command and append output
|
||||
if not s or not s.cmd then
|
||||
return
|
||||
end
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1,
|
||||
insert_at + 1,
|
||||
false,
|
||||
{ "(no output or command failed)" }
|
||||
)
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
-- Also map <leader>0 to run all suggested commands
|
||||
vim.keymap.set("n", "<leader>0", function()
|
||||
for _, s in ipairs(suggested_commands) do
|
||||
pcall(function()
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1,
|
||||
insert_at + 1,
|
||||
false,
|
||||
{ "(no output or command failed)" }
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
|
||||
-- Close with Esc or q
|
||||
vim.keymap.set("n", "<Esc>", M.close, opts)
|
||||
vim.keymap.set("n", "q", M.close, opts)
|
||||
|
||||
-- Start in insert mode
|
||||
vim.cmd("startinsert")
|
||||
|
||||
-- Log
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
|
||||
local function run_project_inspect()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local cmds = {
|
||||
{ label = "List files (ls -la)", cmd = "ls -la" },
|
||||
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
|
||||
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
|
||||
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
|
||||
}
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
|
||||
|
||||
for _, c in ipairs(cmds) do
|
||||
local ok, out = pcall(vim.fn.systemlist, c.cmd)
|
||||
if ok and out and #out > 0 then
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --" })
|
||||
for i, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2 + i, insert_pos + 2 + i, false, { line })
|
||||
end
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2,
|
||||
insert_pos + 2,
|
||||
false,
|
||||
{ "-- " .. c.label .. " --", "(no output or command failed)" }
|
||||
)
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
end
|
||||
end
|
||||
|
||||
-- Move cursor to end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
-- Provide a keybinding in the modal to run project inspection commands
|
||||
pcall(function()
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, { buffer = state.buf, noremap = true, silent = true })
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
end
|
||||
end)
|
||||
|
||||
--- Check if modal is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
-- Close context modal when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
M.close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
M.close = require("codetyper.adapters.nvim.ui.context_modal.close")
|
||||
M.is_open = require("codetyper.adapters.nvim.ui.context_modal.is_open")
|
||||
M.setup = require("codetyper.adapters.nvim.ui.context_modal.setup")
|
||||
M.open = require("codetyper.adapters.nvim.ui.context_modal.open")
|
||||
|
||||
return M
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local parse_requested_files = require("codetyper.utils.parse_requested_files")
|
||||
|
||||
--- Attach parsed files from LLM response into the modal buffer
|
||||
local function attach_requested_files()
|
||||
if not state.llm_response or state.llm_response == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local resolved_files = parse_requested_files(state.llm_response)
|
||||
|
||||
if #resolved_files == 0 then
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
|
||||
return
|
||||
end
|
||||
|
||||
state.attached_files = state.attached_files or {}
|
||||
|
||||
for _, file_path in ipairs(resolved_files) do
|
||||
local read_success, file_lines = pcall(vim.fn.readfile, file_path)
|
||||
if read_success and file_lines and #file_lines > 0 then
|
||||
table.insert(state.attached_files, {
|
||||
path = vim.fn.fnamemodify(file_path, ":~:."),
|
||||
full_path = file_path,
|
||||
content = table.concat(file_lines, "\n"),
|
||||
})
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. file_path .. " --" })
|
||||
for line_index, line_content in ipairs(file_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1 + line_index,
|
||||
insert_at + 1 + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
else
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at,
|
||||
insert_at,
|
||||
false,
|
||||
{ "", "-- Failed to read: " .. file_path .. " --" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return attach_requested_files
|
||||
18
lua/codetyper/adapters/nvim/ui/context_modal/close.lua
Normal file
18
lua/codetyper/adapters/nvim/ui/context_modal/close.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Close the context modal and reset state
|
||||
local function close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
return close
|
||||
9
lua/codetyper/adapters/nvim/ui/context_modal/is_open.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/context_modal/is_open.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Check if the context modal is currently open
|
||||
---@return boolean
|
||||
local function is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
return is_open
|
||||
118
lua/codetyper/adapters/nvim/ui/context_modal/open.lua
Normal file
118
lua/codetyper/adapters/nvim/ui/context_modal/open.lua
Normal file
@@ -0,0 +1,118 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local submit = require("codetyper.adapters.nvim.ui.context_modal.submit")
|
||||
local attach_requested_files = require("codetyper.adapters.nvim.ui.context_modal.attach_requested_files")
|
||||
local run_project_inspect = require("codetyper.adapters.nvim.ui.context_modal.run_project_inspect")
|
||||
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
|
||||
local run_all_suggested_commands = require("codetyper.adapters.nvim.ui.context_modal.run_all_suggested_commands")
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string, attached_files?: table)
|
||||
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
|
||||
function M.open(original_event, llm_response, callback, suggested_commands)
|
||||
close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
|
||||
local header_lines = {
|
||||
ui_prompts.llm_response_header,
|
||||
}
|
||||
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.suggested_commands_header)
|
||||
for command_index, command in ipairs(suggested_commands) do
|
||||
local label = command.label or command.cmd
|
||||
table.insert(header_lines, string.format("[%d] %s: %s", command_index, label, command.cmd))
|
||||
end
|
||||
table.insert(header_lines, ui_prompts.commands_hint)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.input_header)
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
local keymap_opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
vim.keymap.set("n", "<C-CR>", submit, keymap_opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, keymap_opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, keymap_opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, keymap_opts)
|
||||
vim.keymap.set("n", "c", submit, keymap_opts)
|
||||
|
||||
vim.keymap.set("n", "a", attach_requested_files, keymap_opts)
|
||||
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, keymap_opts)
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, keymap_opts)
|
||||
|
||||
state.suggested_commands = suggested_commands
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
for command_index, command in ipairs(suggested_commands) do
|
||||
local key = "<leader>" .. tostring(command_index)
|
||||
vim.keymap.set("n", key, function()
|
||||
run_suggested_command(command)
|
||||
end, keymap_opts)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "<leader>0", function()
|
||||
run_all_suggested_commands(suggested_commands)
|
||||
end, keymap_opts)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "<Esc>", close, keymap_opts)
|
||||
vim.keymap.set("n", "q", close, keymap_opts)
|
||||
|
||||
vim.cmd("startinsert")
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
|
||||
|
||||
--- Run all suggested shell commands and append their outputs to the modal buffer
|
||||
---@param commands table[] List of {label, cmd} suggested command entries
|
||||
local function run_all_suggested_commands(commands)
|
||||
for _, command in ipairs(commands) do
|
||||
pcall(run_suggested_command, command)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_all_suggested_commands
|
||||
@@ -0,0 +1,56 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
|
||||
local function run_project_inspect()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local inspection_commands = {
|
||||
{ label = "List files (ls -la)", cmd = "ls -la" },
|
||||
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
|
||||
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
|
||||
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
|
||||
}
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
|
||||
|
||||
for _, command in ipairs(inspection_commands) do
|
||||
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
|
||||
if run_success and output_lines and #output_lines > 0 then
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2,
|
||||
insert_pos + 2,
|
||||
false,
|
||||
{ "-- " .. command.label .. " --" }
|
||||
)
|
||||
for line_index, line_content in ipairs(output_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2 + line_index,
|
||||
insert_pos + 2 + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2,
|
||||
insert_pos + 2,
|
||||
false,
|
||||
{ "-- " .. command.label .. " --", "(no output or command failed)" }
|
||||
)
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_project_inspect
|
||||
@@ -0,0 +1,38 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Run a single suggested shell command and append its output to the modal buffer
|
||||
---@param command table A {label, cmd} suggested command entry
|
||||
local function run_suggested_command(command)
|
||||
if not command or not command.cmd then
|
||||
return
|
||||
end
|
||||
|
||||
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. command.cmd .. " --" })
|
||||
|
||||
if run_success and output_lines and #output_lines > 0 then
|
||||
for line_index, line_content in ipairs(output_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + line_index,
|
||||
insert_at + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1,
|
||||
insert_at + 1,
|
||||
false,
|
||||
{ "(no output or command failed)" }
|
||||
)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_suggested_command
|
||||
16
lua/codetyper/adapters/nvim/ui/context_modal/setup.lua
Normal file
16
lua/codetyper/adapters/nvim/ui/context_modal/setup.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
local function setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return setup
|
||||
31
lua/codetyper/adapters/nvim/ui/context_modal/submit.lua
Normal file
31
lua/codetyper/adapters/nvim/ui/context_modal/submit.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
|
||||
|
||||
--- Submit the additional context from the modal buffer
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
local attached_files = state.attached_files
|
||||
|
||||
close()
|
||||
|
||||
if callback and original_event then
|
||||
callback(original_event, additional_context, attached_files)
|
||||
end
|
||||
end
|
||||
|
||||
return submit
|
||||
10
lua/codetyper/state/state.lua
Normal file
10
lua/codetyper/state/state.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
original_event = nil,
|
||||
callback = nil,
|
||||
llm_response = nil,
|
||||
attached_files = nil,
|
||||
}
|
||||
|
||||
return state
|
||||
51
lua/codetyper/utils/parse_requested_files.lua
Normal file
51
lua/codetyper/utils/parse_requested_files.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
--- Parse requested file paths from LLM response and resolve to full paths
|
||||
---@param response string LLM response text
|
||||
---@return string[] resolved_paths List of resolved absolute file paths
|
||||
local function parse_requested_files(response)
|
||||
if not response or response == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local candidates = {}
|
||||
local seen = {}
|
||||
|
||||
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
local resolved = {}
|
||||
for _, candidate_path in ipairs(candidates) do
|
||||
local full_path = nil
|
||||
if candidate_path:sub(1, 1) == "/" and vim.fn.filereadable(candidate_path) == 1 then
|
||||
full_path = candidate_path
|
||||
else
|
||||
local relative_path = cwd .. "/" .. candidate_path
|
||||
if vim.fn.filereadable(relative_path) == 1 then
|
||||
full_path = relative_path
|
||||
else
|
||||
local filename = candidate_path:match("[^/]+$") or candidate_path
|
||||
local glob_matches = vim.fn.globpath(cwd, "**/" .. filename, false, true)
|
||||
if glob_matches and #glob_matches > 0 then
|
||||
full_path = glob_matches[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
if full_path and vim.fn.filereadable(full_path) == 1 then
|
||||
table.insert(resolved, full_path)
|
||||
end
|
||||
end
|
||||
|
||||
return resolved
|
||||
end
|
||||
|
||||
return parse_requested_files
|
||||
Reference in New Issue
Block a user