avante.nvim/lua/avante/suggestion.lua
Christopher Brewin 78dd9b0a6d
feat(context): add a ui for selecting and adding files to the sidebar as context (#912)
* 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>
2024-12-12 01:29:10 +08:00

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