
* feat(sidebar): supports select files chore (context) update add type annotations to context functions chore (sidebar) remove unused notify function call refactor (sidebar) remove setting search file to file path chore (sidebar) remove nvim_notify debugging api call * feat (files) allow selecting a file by string via cmp suggestion menu * chore (context) refactor to allow context using @file with a context view * refactor (context) refactor seletected file types as an array of path and content * refactor (config) remove unused configuration options * refactor (sidebar) remove unused unbild key * refactor (context) remove unused imports * refactor (mentions) update mentions to support items with callback functions and removal of the underlying selection. * fix (sidebar) add file context as a window that is visitable via the tab key * refactor (file_content) remove file content as an input to llm * feat (sidebar) support suggesting and applying code in all languages that are in the context * feat (sidebar) configurable mapping for removing a file from the context. * feat (context_view) configure hints for the context view for adding and deleting a file. * feat (context) add hints for the context view. * fix (sidebar) type when scrolling the results buffer. * refactor (selected files) refactor llm stream to accept an array of selected file metadata * refactor: context => selected_files --------- Co-authored-by: yetone <yetoneful@gmail.com>
336 lines
9.7 KiB
Lua
336 lines
9.7 KiB
Lua
local Utils = require("avante.utils")
|
|
local Llm = require("avante.llm")
|
|
local Highlights = require("avante.highlights")
|
|
local Config = require("avante.config")
|
|
local Providers = require("avante.providers")
|
|
local api = vim.api
|
|
local fn = vim.fn
|
|
|
|
local SUGGESTION_NS = api.nvim_create_namespace("avante_suggestion")
|
|
|
|
---@class avante.SuggestionItem
|
|
---@field content string
|
|
---@field row number
|
|
---@field col number
|
|
|
|
---@class avante.SuggestionContext
|
|
---@field suggestions avante.SuggestionItem[]
|
|
---@field current_suggestion_idx number
|
|
---@field prev_doc? table
|
|
|
|
---@class avante.Suggestion
|
|
---@field id number
|
|
---@field augroup integer
|
|
---@field extmark_id integer
|
|
---@field _timer? table
|
|
---@field _contexts table
|
|
local Suggestion = {}
|
|
Suggestion.__index = Suggestion
|
|
|
|
---@param id number
|
|
---@return avante.Suggestion
|
|
function Suggestion:new(id)
|
|
local instance = setmetatable({}, self)
|
|
instance.id = id
|
|
instance.extmark_id = 1
|
|
instance._timer = nil
|
|
instance._contexts = {}
|
|
if Config.behaviour.auto_suggestions then
|
|
if not vim.g.avante_login or vim.g.avante_login == false then
|
|
api.nvim_exec_autocmds("User", { pattern = Providers.env.REQUEST_LOGIN_PATTERN })
|
|
vim.g.avante_login = true
|
|
end
|
|
instance:setup_autocmds()
|
|
end
|
|
return instance
|
|
end
|
|
|
|
function Suggestion:destroy()
|
|
self:stop_timer()
|
|
self:reset()
|
|
self:delete_autocmds()
|
|
end
|
|
|
|
function Suggestion:suggest()
|
|
Utils.debug("suggesting")
|
|
|
|
local ctx = self:ctx()
|
|
local doc = Utils.get_doc()
|
|
ctx.prev_doc = doc
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local filetype = api.nvim_get_option_value("filetype", { buf = bufnr })
|
|
local code_content =
|
|
Utils.prepend_line_number(table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n\n")
|
|
|
|
local full_response = ""
|
|
|
|
local provider = Providers[Config.auto_suggestions_provider]
|
|
|
|
Llm.stream({
|
|
provider = provider,
|
|
bufnr = bufnr,
|
|
ask = true,
|
|
selected_files = { { content = code_content, file_type = filetype, path = "" } },
|
|
code_lang = filetype,
|
|
instructions = vim.json.encode(doc),
|
|
mode = "suggesting",
|
|
on_chunk = function(chunk) full_response = full_response .. chunk end,
|
|
on_complete = function(err)
|
|
if err then
|
|
Utils.error("Error while suggesting: " .. vim.inspect(err), { once = true, title = "Avante" })
|
|
return
|
|
end
|
|
Utils.debug("full_response:", full_response)
|
|
vim.schedule(function()
|
|
local cursor_row, cursor_col = Utils.get_cursor_pos()
|
|
if cursor_row ~= doc.position.row or cursor_col ~= doc.position.col then return end
|
|
-- Clean up markdown code blocks
|
|
full_response = full_response:gsub("^```%w*\n(.-)\n```$", "%1")
|
|
-- Remove everything before the first '[' to ensure we get just the JSON array
|
|
full_response = full_response:gsub("^.-(%[.*)", "%1")
|
|
local ok, suggestions = pcall(vim.json.decode, full_response)
|
|
if not ok then
|
|
Utils.error("Error while decoding suggestions: " .. full_response, { once = true, title = "Avante" })
|
|
return
|
|
end
|
|
if not suggestions then
|
|
Utils.info("No suggestions found", { once = true, title = "Avante" })
|
|
return
|
|
end
|
|
suggestions = vim
|
|
.iter(suggestions)
|
|
:map(function(s) return { row = s.row, col = s.col, content = Utils.trim_all_line_numbers(s.content) } end)
|
|
:totable()
|
|
ctx.suggestions = suggestions
|
|
ctx.current_suggestion_idx = 1
|
|
self:show()
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
|
|
function Suggestion:show()
|
|
self:hide()
|
|
|
|
if not fn.mode():match("^[iR]") then return end
|
|
|
|
local ctx = self:ctx()
|
|
local suggestion = ctx.suggestions[ctx.current_suggestion_idx]
|
|
if not suggestion then return end
|
|
|
|
local cursor_row, cursor_col = Utils.get_cursor_pos()
|
|
|
|
if suggestion.row < cursor_row then return end
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local row = suggestion.row
|
|
local col = suggestion.col
|
|
local content = suggestion.content
|
|
|
|
local lines = vim.split(content, "\n")
|
|
|
|
local extmark_col = cursor_col
|
|
|
|
if cursor_row < row then extmark_col = 0 end
|
|
|
|
local current_lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
|
|
if cursor_row == row then
|
|
local cursor_line_col = #current_lines[cursor_row] - 1
|
|
if cursor_col ~= cursor_line_col then
|
|
local current_line = current_lines[cursor_row]
|
|
lines[1] = lines[1] .. current_line:sub(col + 1, -1)
|
|
end
|
|
end
|
|
|
|
local extmark = {
|
|
id = self.extmark_id,
|
|
virt_text_win_col = col,
|
|
virt_text = { { lines[1], Highlights.SUGGESTION } },
|
|
}
|
|
|
|
if #lines > 1 then
|
|
extmark.virt_lines = {}
|
|
for i = 2, #lines do
|
|
extmark.virt_lines[i - 1] = { { lines[i], Highlights.SUGGESTION } }
|
|
end
|
|
end
|
|
|
|
extmark.hl_mode = "combine"
|
|
|
|
local buf_lines = Utils.get_buf_lines(0, -1, bufnr)
|
|
local buf_lines_count = #buf_lines
|
|
|
|
while buf_lines_count < row do
|
|
api.nvim_buf_set_lines(bufnr, buf_lines_count, -1, false, { "" })
|
|
buf_lines_count = buf_lines_count + 1
|
|
end
|
|
|
|
api.nvim_buf_set_extmark(bufnr, SUGGESTION_NS, row - 1, extmark_col, extmark)
|
|
end
|
|
|
|
function Suggestion:is_visible()
|
|
return not not api.nvim_buf_get_extmark_by_id(0, SUGGESTION_NS, self.extmark_id, { details = false })[1]
|
|
end
|
|
|
|
function Suggestion:hide() api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id) end
|
|
|
|
function Suggestion:ctx()
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local ctx = self._contexts[bufnr]
|
|
if not ctx then
|
|
ctx = {
|
|
suggestions = {},
|
|
current_suggestion_idx = 0,
|
|
prev_doc = {},
|
|
}
|
|
self._contexts[bufnr] = ctx
|
|
end
|
|
return ctx
|
|
end
|
|
|
|
function Suggestion:reset()
|
|
self._timer = nil
|
|
local bufnr = api.nvim_get_current_buf()
|
|
self._contexts[bufnr] = nil
|
|
end
|
|
|
|
function Suggestion:stop_timer()
|
|
if self._timer then
|
|
pcall(function() fn.timer_stop(self._timer) end)
|
|
self._timer = nil
|
|
end
|
|
end
|
|
|
|
function Suggestion:next()
|
|
local ctx = self:ctx()
|
|
if #ctx.suggestions == 0 then return end
|
|
ctx.current_suggestion_idx = (ctx.current_suggestion_idx % #ctx.suggestions) + 1
|
|
self:show()
|
|
end
|
|
|
|
function Suggestion:prev()
|
|
local ctx = self:ctx()
|
|
if #ctx.suggestions == 0 then return end
|
|
ctx.current_suggestion_idx = ((ctx.current_suggestion_idx - 2 + #ctx.suggestions) % #ctx.suggestions) + 1
|
|
self:show()
|
|
end
|
|
|
|
function Suggestion:dismiss()
|
|
self:stop_timer()
|
|
self:hide()
|
|
self:reset()
|
|
end
|
|
|
|
function Suggestion:accept()
|
|
-- Llm.cancel_inflight_request()
|
|
api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id)
|
|
local ctx = self:ctx()
|
|
local suggestion = ctx.suggestions and ctx.suggestions[ctx.current_suggestion_idx] or nil
|
|
if not suggestion then
|
|
if Config.mappings.suggestion and Config.mappings.suggestion.accept == "<Tab>" then
|
|
api.nvim_feedkeys(api.nvim_replace_termcodes("<Tab>", true, false, true), "n", true)
|
|
end
|
|
return
|
|
end
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local current_lines = Utils.get_buf_lines(0, -1, bufnr)
|
|
local row = suggestion.row
|
|
local col = suggestion.col
|
|
local content = suggestion.content
|
|
local lines = vim.split(content, "\n")
|
|
local cursor_row, cursor_col = Utils.get_cursor_pos()
|
|
if row > cursor_row then api.nvim_buf_set_lines(bufnr, row - 1, row - 1, false, { "" }) end
|
|
local line_count = #lines
|
|
if line_count > 0 then
|
|
if cursor_row == row then
|
|
local cursor_line_col = #current_lines[cursor_row] - 1
|
|
if cursor_col ~= cursor_line_col then
|
|
local current_line_ = current_lines[cursor_row]
|
|
lines[1] = lines[1] .. current_line_:sub(col + 1, -1)
|
|
end
|
|
end
|
|
local current_line = current_lines[row] or ""
|
|
local current_line_max_col = #current_line - 1
|
|
local start_col = col
|
|
if start_col > current_line_max_col then
|
|
lines[1] = string.rep(" ", start_col - current_line_max_col - 1) .. lines[1]
|
|
start_col = -1
|
|
end
|
|
api.nvim_buf_set_text(bufnr, row - 1, start_col, row - 1, -1, { lines[1] })
|
|
if #lines > 1 then
|
|
local insert_lines = vim.list_slice(lines, 2)
|
|
api.nvim_buf_set_lines(bufnr, row, row, true, insert_lines)
|
|
end
|
|
end
|
|
|
|
local down_count = line_count - 1
|
|
if row > cursor_row then down_count = down_count + 1 end
|
|
|
|
local cursor_keys = string.rep("<Down>", down_count) .. "<End>"
|
|
api.nvim_feedkeys(api.nvim_replace_termcodes(cursor_keys, true, false, true), "n", false)
|
|
|
|
self:hide()
|
|
self:reset()
|
|
end
|
|
|
|
function Suggestion:setup_autocmds()
|
|
self.augroup = api.nvim_create_augroup("avante_suggestion_" .. self.id, { clear = true })
|
|
local last_cursor_pos = {}
|
|
|
|
local check_for_suggestion = Utils.debounce(function()
|
|
local current_cursor_pos = api.nvim_win_get_cursor(0)
|
|
if last_cursor_pos[1] == current_cursor_pos[1] and last_cursor_pos[2] == current_cursor_pos[2] then
|
|
self:suggest()
|
|
end
|
|
end, 700)
|
|
|
|
local function suggest_callback()
|
|
if not vim.bo.buflisted then return end
|
|
|
|
if vim.bo.buftype ~= "" then return end
|
|
|
|
local ctx = self:ctx()
|
|
|
|
if ctx.prev_doc and vim.deep_equal(ctx.prev_doc, Utils.get_doc()) then return end
|
|
|
|
self:hide()
|
|
last_cursor_pos = api.nvim_win_get_cursor(0)
|
|
self._timer = check_for_suggestion()
|
|
end
|
|
|
|
api.nvim_create_autocmd("InsertEnter", {
|
|
group = self.augroup,
|
|
callback = suggest_callback,
|
|
})
|
|
|
|
api.nvim_create_autocmd("BufEnter", {
|
|
group = self.augroup,
|
|
callback = function()
|
|
if fn.mode():match("^[iR]") then suggest_callback() end
|
|
end,
|
|
})
|
|
|
|
api.nvim_create_autocmd("CursorMovedI", {
|
|
group = self.augroup,
|
|
callback = suggest_callback,
|
|
})
|
|
|
|
api.nvim_create_autocmd("InsertLeave", {
|
|
group = self.augroup,
|
|
callback = function()
|
|
last_cursor_pos = {}
|
|
self:hide()
|
|
self:reset()
|
|
end,
|
|
})
|
|
end
|
|
|
|
function Suggestion:delete_autocmds()
|
|
if self.augroup then api.nvim_del_augroup_by_id(self.augroup) end
|
|
self.augroup = nil
|
|
end
|
|
|
|
return Suggestion
|