avante.nvim/lua/avante/selection.lua

307 lines
10 KiB
Lua
Raw Normal View History

local Utils = require("avante.utils")
local Config = require("avante.config")
local Llm = require("avante.llm")
local Provider = require("avante.providers")
local RepoMap = require("avante.repo_map")
local PromptInput = require("avante.prompt_input")
2024-08-17 22:29:05 +08:00
local api = vim.api
local fn = vim.fn
local NAMESPACE = api.nvim_create_namespace("avante_selection")
local SELECTED_CODE_NAMESPACE = api.nvim_create_namespace("avante_selected_code")
2024-08-17 22:29:05 +08:00
local PRIORITY = vim.highlight.priorities.user
---@class avante.Selection
2024-08-27 22:44:40 +08:00
---@field selection avante.SelectionResult | nil
---@field cursor_pos table | nil
---@field shortcuts_extmark_id integer | nil
2024-08-27 23:47:15 +08:00
---@field selected_code_extmark_id integer | nil
2024-08-27 22:44:40 +08:00
---@field augroup integer | nil
---@field code_winid integer | nil
---@field prompt_input PromptInput | nil
2024-08-17 22:29:05 +08:00
local Selection = {}
2024-11-23 20:23:05 +08:00
Selection.__index = Selection
2024-08-17 22:29:05 +08:00
Selection.did_setup = false
2024-08-22 14:46:08 +08:00
---@param id integer the tabpage id retrieved from api.nvim_get_current_tabpage()
function Selection:new(id)
2024-08-17 22:29:05 +08:00
return setmetatable({
2024-08-27 22:44:40 +08:00
shortcuts_extmark_id = nil,
2024-08-27 23:47:15 +08:00
selected_code_extmark_id = nil,
augroup = api.nvim_create_augroup("avante_selection_" .. id, { clear = true }),
2024-08-27 22:44:40 +08:00
selection = nil,
cursor_pos = nil,
code_winid = nil,
prompt_input = nil,
2024-11-23 20:23:05 +08:00
}, Selection)
2024-08-17 22:29:05 +08:00
end
function Selection:get_virt_text_line()
local current_pos = fn.getpos(".")
2024-08-17 22:29:05 +08:00
-- Get the current and start position line numbers
local current_line = current_pos[2] - 1 -- 0-indexed
-- Ensure line numbers are not negative and don't exceed buffer range
local total_lines = api.nvim_buf_line_count(0)
if current_line < 0 then current_line = 0 end
if current_line >= total_lines then current_line = total_lines - 1 end
2024-08-17 22:29:05 +08:00
-- Take the first line of the selection to ensure virt_text is always in the top right corner
return current_line
end
2024-08-27 22:44:40 +08:00
function Selection:show_shortcuts_hints_popup()
self:close_shortcuts_hints_popup()
2024-08-17 22:29:05 +08:00
local hint_text = string.format(" [%s: ask, %s: edit] ", Config.mappings.ask, Config.mappings.edit)
2024-08-17 22:29:05 +08:00
local virt_text_line = self:get_virt_text_line()
2024-08-27 22:44:40 +08:00
self.shortcuts_extmark_id = api.nvim_buf_set_extmark(0, NAMESPACE, virt_text_line, -1, {
virt_text = { { hint_text, "AvanteInlineHint" } },
2024-08-17 22:29:05 +08:00
virt_text_pos = "eol",
priority = PRIORITY,
})
end
2024-08-27 22:44:40 +08:00
function Selection:close_shortcuts_hints_popup()
if self.shortcuts_extmark_id then
api.nvim_buf_del_extmark(0, NAMESPACE, self.shortcuts_extmark_id)
self.shortcuts_extmark_id = nil
end
end
function Selection:close_editing_input()
if self.prompt_input then
self.prompt_input:close()
self.prompt_input = nil
2024-08-27 22:44:40 +08:00
end
Llm.cancel_inflight_request()
2024-08-27 23:47:15 +08:00
if self.code_winid and api.nvim_win_is_valid(self.code_winid) then
local code_bufnr = api.nvim_win_get_buf(self.code_winid)
api.nvim_buf_clear_namespace(code_bufnr, SELECTED_CODE_NAMESPACE, 0, -1)
if self.selected_code_extmark_id then
api.nvim_buf_del_extmark(code_bufnr, SELECTED_CODE_NAMESPACE, self.selected_code_extmark_id)
self.selected_code_extmark_id = nil
end
end
2024-08-27 22:44:40 +08:00
if self.cursor_pos and self.code_winid then
vim.schedule(function()
local bufnr = api.nvim_win_get_buf(self.code_winid)
local line_count = api.nvim_buf_line_count(bufnr)
local row = math.min(self.cursor_pos[1], line_count)
local line = api.nvim_buf_get_lines(bufnr, row - 1, row, true)[1] or ""
local col = math.min(self.cursor_pos[2], #line)
api.nvim_win_set_cursor(self.code_winid, { row, col })
2024-08-27 22:44:40 +08:00
end)
end
end
function Selection:create_editing_input()
2024-08-27 23:47:15 +08:00
self:close_editing_input()
if not vim.g.avante_login or vim.g.avante_login == false then
api.nvim_exec_autocmds("User", { pattern = Provider.env.REQUEST_LOGIN_PATTERN })
vim.g.avante_login = true
end
2024-08-27 22:44:40 +08:00
local code_bufnr = api.nvim_get_current_buf()
local code_winid = api.nvim_get_current_win()
self.cursor_pos = api.nvim_win_get_cursor(code_winid)
self.code_winid = code_winid
2024-08-27 22:44:40 +08:00
local code_lines = api.nvim_buf_get_lines(code_bufnr, 0, -1, false)
local code_content = table.concat(code_lines, "\n")
self.selection = Utils.get_visual_selection_and_range()
if self.selection == nil then
Utils.error("No visual selection found", { once = true, title = "Avante" })
return
end
local start_row
local start_col
local end_row
local end_col
if vim.fn.mode() == "V" then
2024-11-23 21:49:33 +08:00
start_row = self.selection.range.start.lnum - 1
start_col = 0
2024-11-23 21:49:33 +08:00
end_row = self.selection.range.finish.lnum - 1
end_col = #code_lines[self.selection.range.finish.lnum]
else
2024-11-23 21:49:33 +08:00
start_row = self.selection.range.start.lnum - 1
start_col = self.selection.range.start.col - 1
2024-11-23 21:49:33 +08:00
end_row = self.selection.range.finish.lnum - 1
end_col = math.min(self.selection.range.finish.col, #code_lines[self.selection.range.finish.lnum])
end
self.selected_code_extmark_id = api.nvim_buf_set_extmark(code_bufnr, SELECTED_CODE_NAMESPACE, start_row, start_col, {
hl_group = "Visual",
hl_mode = "combine",
end_row = end_row,
end_col = end_col,
priority = PRIORITY,
})
2024-08-27 22:44:40 +08:00
local submit_input = function(input)
2024-08-27 22:44:40 +08:00
local full_response = ""
2024-11-23 21:49:33 +08:00
local start_line = self.selection.range.start.lnum
local finish_line = self.selection.range.finish.lnum
2024-08-27 22:44:40 +08:00
2024-11-23 21:49:33 +08:00
local original_first_line_indentation = Utils.get_indentation(code_lines[self.selection.range.start.lnum])
2024-08-27 22:44:40 +08:00
local need_prepend_indentation = false
self.prompt_input:start_spinner()
2024-08-27 22:44:40 +08:00
---@type AvanteChunkParser
local on_chunk = function(chunk)
full_response = full_response .. chunk
local response_lines_ = vim.split(full_response, "\n")
local response_lines = {}
for i, line in ipairs(response_lines_) do
if not (string.match(line, "^```") and (i == 1 or i == #response_lines_)) then
table.insert(response_lines, line)
end
end
if #response_lines == 1 then
2024-08-28 22:17:00 +08:00
local first_line = response_lines[1]
local first_line_indentation = Utils.get_indentation(first_line)
need_prepend_indentation = first_line_indentation ~= original_first_line_indentation
end
if need_prepend_indentation then
for i, line in ipairs(response_lines) do
response_lines[i] = original_first_line_indentation .. line
end
2024-08-27 22:44:40 +08:00
end
api.nvim_buf_set_lines(code_bufnr, start_line - 1, finish_line, true, response_lines)
finish_line = start_line + #response_lines - 1
end
---@type AvanteCompleteParser
local on_complete = function(err)
if err then
-- NOTE: in Ubuntu 22.04+ you will see this ignorable error from ~/.local/share/nvim/lazy/avante.nvim/lua/avante/llm.lua `on_error = function(err)`, check to avoid showing this error.
if type(err) == "table" and err.exit == nil and err.stderr == "{}" then return end
2024-08-27 22:44:40 +08:00
Utils.error(
"Error occurred while processing the response: " .. vim.inspect(err),
{ once = true, title = "Avante" }
)
return
end
self.prompt_input:stop_spinner()
vim.defer_fn(function() self:close_editing_input() end, 0)
2024-08-27 22:44:40 +08:00
end
2024-08-30 22:21:50 +08:00
local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr })
local file_ext = api.nvim_buf_get_name(code_bufnr):match("^.+%.(.+)$")
local mentions = Utils.extract_mentions(input)
input = mentions.new_content
local project_context = mentions.enable_project_context and RepoMap.get_repo_map(file_ext) or nil
2024-08-30 22:21:50 +08:00
local diagnostics = Utils.get_current_selection_diagnostics(code_bufnr, self.selection)
2024-11-23 21:49:33 +08:00
Llm.stream({
ask = true,
project_context = vim.json.encode(project_context),
2024-11-23 21:49:33 +08:00
diagnostics = vim.json.encode(diagnostics),
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 03:29:10 +10:00
selected_files = { { content = code_content, file_type = filetype, path = "" } },
2024-08-30 22:21:50 +08:00
code_lang = filetype,
selected_code = self.selection.content,
instructions = input,
mode = "editing",
on_chunk = on_chunk,
on_complete = on_complete,
})
2024-08-27 22:44:40 +08:00
end
local prompt_input = PromptInput:new({
submit_callback = submit_input,
cancel_callback = function() self:close_editing_input() end,
win_opts = {
border = Config.windows.edit.border,
title = { { "edit selected block", "FloatTitle" } },
},
start_insert = Config.windows.edit.start_insert,
2024-08-27 22:44:40 +08:00
})
self.prompt_input = prompt_input
prompt_input:open()
api.nvim_create_autocmd("InsertEnter", {
group = self.augroup,
buffer = prompt_input.bufnr,
once = true,
desc = "Setup the completion of helpers in the input buffer",
callback = function()
local has_cmp, cmp = pcall(require, "cmp")
if has_cmp then
cmp.register_source(
"avante_mentions",
2024-11-23 20:23:05 +08:00
require("cmp_avante.mentions"):new(Utils.get_mentions(), prompt_input.bufnr)
)
cmp.setup.buffer({
enabled = true,
sources = {
{ name = "avante_mentions" },
},
})
end
end,
})
2024-08-17 22:29:05 +08:00
end
function Selection:setup_autocmds()
Selection.did_setup = true
api.nvim_create_autocmd({ "ModeChanged" }, {
2024-08-17 22:29:05 +08:00
group = self.augroup,
pattern = { "n:v", "n:V", "n:" }, -- Entering Visual mode from Normal mode
callback = function(ev)
if not Utils.is_sidebar_buffer(ev.buf) then self:show_shortcuts_hints_popup() end
2024-08-17 22:29:05 +08:00
end,
})
api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
group = self.augroup,
callback = function(ev)
2024-08-24 00:14:20 +08:00
if not Utils.is_sidebar_buffer(ev.buf) then
if Utils.in_visual_mode() then
2024-08-27 22:44:40 +08:00
self:show_shortcuts_hints_popup()
else
2024-08-27 22:44:40 +08:00
self:close_shortcuts_hints_popup()
end
2024-08-17 22:29:05 +08:00
end
end,
})
api.nvim_create_autocmd({ "ModeChanged" }, {
group = self.augroup,
pattern = { "v:n", "v:i", "v:c" }, -- Switching from visual mode back to normal, insert, or other modes
callback = function(ev)
if not Utils.is_sidebar_buffer(ev.buf) then self:close_shortcuts_hints_popup() end
2024-08-17 22:29:05 +08:00
end,
})
api.nvim_create_autocmd({ "BufLeave" }, {
group = self.augroup,
callback = function(ev)
if not Utils.is_sidebar_buffer(ev.buf) then self:close_shortcuts_hints_popup() end
end,
})
return self
2024-08-17 22:29:05 +08:00
end
function Selection:delete_autocmds()
if self.augroup then api.nvim_del_augroup_by_id(self.augroup) end
2024-08-17 22:29:05 +08:00
self.augroup = nil
Selection.did_setup = false
2024-08-17 22:29:05 +08:00
end
return Selection