From 78dd9b0a6dba5f9cae5346fe8b421e9d10710161 Mon Sep 17 00:00:00 2001 From: Christopher Brewin Date: Thu, 12 Dec 2024 03:29:10 +1000 Subject: [PATCH] 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 --- crates/avante-templates/src/lib.rs | 13 +- lua/avante/api.lua | 12 +- lua/avante/config.lua | 2 + lua/avante/diff.lua | 5 +- lua/avante/file_selector.lua | 146 ++++++ lua/avante/llm.lua | 30 +- lua/avante/path.lua | 1 + lua/avante/selection.lua | 2 +- lua/avante/sidebar.lua | 611 +++++++++++++++------- lua/avante/suggestion.lua | 2 +- lua/avante/templates/_context.avanterules | 35 +- lua/avante/utils/init.lua | 27 +- lua/cmp_avante/mentions.lua | 33 ++ 13 files changed, 695 insertions(+), 224 deletions(-) create mode 100644 lua/avante/file_selector.lua diff --git a/crates/avante-templates/src/lib.rs b/crates/avante-templates/src/lib.rs index 3b657bc..4e2d7a8 100644 --- a/crates/avante-templates/src/lib.rs +++ b/crates/avante-templates/src/lib.rs @@ -15,13 +15,19 @@ impl<'a> State<'a> { } } +#[derive(Debug, Serialize, Deserialize)] +struct SelectedFile { + path: String, + content: String, + file_type: String, +} + #[derive(Debug, Serialize, Deserialize)] struct TemplateContext { use_xml_format: bool, ask: bool, code_lang: String, - filepath: String, - file_content: String, + selected_files: Vec, selected_code: Option, project_context: Option, diagnostics: Option, @@ -44,8 +50,7 @@ fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult< use_xml_format => context.use_xml_format, ask => context.ask, code_lang => context.code_lang, - filepath => context.filepath, - file_content => context.file_content, + selected_files => context.selected_files, selected_code => context.selected_code, project_context => context.project_context, diagnostics => context.diagnostics, diff --git a/lua/avante/api.lua b/lua/avante/api.lua index 13f19a6..a0f56b4 100644 --- a/lua/avante/api.lua +++ b/lua/avante/api.lua @@ -161,7 +161,7 @@ M.refresh = function(opts) if not sidebar:is_open() then return end local curbuf = vim.api.nvim_get_current_buf() - local focused = sidebar.result.bufnr == curbuf or sidebar.input.bufnr == curbuf + local focused = sidebar.result_container.bufnr == curbuf or sidebar.input_container.bufnr == curbuf if focused or not sidebar:is_open() then return end local listed = vim.api.nvim_get_option_value("buflisted", { buf = curbuf }) @@ -185,19 +185,19 @@ M.focus = function(opts) local curwin = vim.api.nvim_get_current_win() if sidebar:is_open() then - if curbuf == sidebar.input.bufnr then + if curbuf == sidebar.input_container.bufnr then if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end - elseif curbuf == sidebar.result.bufnr then + elseif curbuf == sidebar.result_container.bufnr then if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end else - if sidebar.input.winid and sidebar.input.winid ~= curwin then - vim.api.nvim_set_current_win(sidebar.input.winid) + if sidebar.input_container.winid and sidebar.input_container.winid ~= curwin then + vim.api.nvim_set_current_win(sidebar.input_container.winid) end end else if sidebar.code.winid then vim.api.nvim_set_current_win(sidebar.code.winid) end sidebar:open(opts) - if sidebar.input.winid then vim.api.nvim_set_current_win(sidebar.input.winid) end + if sidebar.input_container.winid then vim.api.nvim_set_current_win(sidebar.input_container.winid) end end end diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 98d5212..1d804c5 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -186,6 +186,8 @@ M.defaults = { apply_cursor = "a", switch_windows = "", reverse_switch_windows = "", + remove_file = "d", + add_file = "@", }, }, windows = { diff --git a/lua/avante/diff.lua b/lua/avante/diff.lua index e83d01b..b7ac15d 100644 --- a/lua/avante/diff.lua +++ b/lua/avante/diff.lua @@ -2,7 +2,6 @@ local api = vim.api local Config = require("avante.config") local Utils = require("avante.utils") -local Highlights = require("avante.highlights") local H = {} local M = {} @@ -558,7 +557,7 @@ end ---@param enable_autojump boolean function M.process_position(bufnr, side, position, enable_autojump) local lines = {} - if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS, SIDES.BASE }, side) then + if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS }, side) then local data = position[name_map[side]] lines = Utils.get_buf_lines(data.content_start, data.content_end + 1) elseif side == SIDES.BOTH then @@ -569,7 +568,7 @@ function M.process_position(bufnr, side, position, enable_autojump) lines = {} elseif side == SIDES.CURSOR then local cursor_line = Utils.get_cursor_pos() - for _, pos in ipairs({ SIDES.OURS, SIDES.THEIRS, SIDES.BASE }) do + for _, pos in ipairs({ SIDES.OURS, SIDES.THEIRS }) do local data = position[name_map[pos]] or {} if data.range_start and data.range_start + 1 <= cursor_line and data.range_end + 1 >= cursor_line then side = pos diff --git a/lua/avante/file_selector.lua b/lua/avante/file_selector.lua new file mode 100644 index 0000000..17bba32 --- /dev/null +++ b/lua/avante/file_selector.lua @@ -0,0 +1,146 @@ +local Utils = require("avante.utils") +local Path = require("plenary.path") +local scan = require("plenary.scandir") + +--- @class FileSelector +local FileSelector = {} + +--- @class FileSelector +--- @field id integer +--- @field selected_filepaths string[] +--- @field file_cache string[] +--- @field event_handlers table + +---@param id integer +---@return FileSelector +function FileSelector:new(id) + return setmetatable({ + id = id, + selected_files = {}, + file_cache = {}, + event_handlers = {}, + }, { __index = self }) +end + +function FileSelector:reset() + self.selected_filepaths = {} + self.event_handlers = {} +end + +function FileSelector:add_selected_file(filepath) table.insert(self.selected_filepaths, Utils.uniform_path(filepath)) end + +function FileSelector:on(event, callback) + local handlers = self.event_handlers[event] + if not handlers then + handlers = {} + self.event_handlers[event] = handlers + end + + table.insert(handlers, callback) +end + +function FileSelector:emit(event, ...) + local handlers = self.event_handlers[event] + if not handlers then return end + + for _, handler in ipairs(handlers) do + handler(...) + end +end + +function FileSelector:off(event, callback) + if not callback then + self.event_handlers[event] = {} + return + end + local handlers = self.event_handlers[event] + if not handlers then return end + + for i, handler in ipairs(handlers) do + if handler == callback then + table.remove(handlers, i) + break + end + end +end + +---@return nil +function FileSelector:open() + self:update_file_cache() + self:show_select_ui() +end + +---@return nil +function FileSelector:update_file_cache() + local project_root = Path:new(Utils.get_project_root()):absolute() + + local filepaths = scan.scan_dir(project_root, { + respect_gitignore = true, + }) + + -- Sort buffer names alphabetically + table.sort(filepaths, function(a, b) return a < b end) + + self.file_cache = vim + .iter(filepaths) + :map(function(filepath) return Path:new(filepath):make_relative(project_root) end) + :totable() +end + +---@return nil +function FileSelector:show_select_ui() + vim.schedule(function() + local filepaths = vim + .iter(self.file_cache) + :filter(function(filepath) return not vim.tbl_contains(self.selected_filepaths, filepath) end) + :totable() + + vim.ui.select(filepaths, { + prompt = "(Avante) Add a file:", + format_item = function(item) return item end, + }, function(filepath) + if not filepath then return end + table.insert(self.selected_filepaths, Utils.uniform_path(filepath)) + self:emit("update") + end) + end) + + -- unlist the current buffer as vim.ui.select will be listed + local winid = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + vim.api.nvim_set_option_value("buflisted", false, { buf = bufnr }) + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = bufnr }) +end + +---@param idx integer +---@return boolean +function FileSelector:remove_selected_filepaths(idx) + if idx > 0 and idx <= #self.selected_filepaths then + table.remove(self.selected_filepaths, idx) + self:emit("update") + return true + end + return false +end + +---@return { path: string, content: string, file_type: string }[] +function FileSelector:get_selected_files_contents() + local contents = {} + for _, file_path in ipairs(self.selected_filepaths) do + local file = io.open(file_path, "r") + if file then + local content = file:read("*all") + file:close() + + -- Detect the file type + local filetype = vim.filetype.match({ filename = file_path, contents = contents }) or "unknown" + + table.insert(contents, { path = file_path, content = content, file_type = filetype }) + end + end + return contents +end + +function FileSelector:get_selected_filepaths() return vim.deepcopy(self.selected_filepaths) end + +return FileSelector diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index bbbfa02..1b8a95a 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -44,14 +44,11 @@ M._stream = function(opts, Provider) Path.prompts.initialize(Path.prompts.get(opts.bufnr)) - local filepath = Utils.relative_path(api.nvim_buf_get_name(opts.bufnr)) - local template_opts = { use_xml_format = Provider.use_xml_format, ask = opts.ask, -- TODO: add mode without ask instruction code_lang = opts.code_lang, - filepath = filepath, - file_content = opts.file_content, + selected_files = opts.selected_files, selected_code = opts.selected_code, project_context = opts.project_context, diagnostics = opts.diagnostics, @@ -335,14 +332,19 @@ end ---@alias LlmMode "planning" | "editing" | "suggesting" --- +---@class SelectedFiles +---@field path string +---@field content string +---@field file_type string +--- ---@class TemplateOptions ---@field use_xml_format boolean ---@field ask boolean ---@field question string ---@field code_lang string ----@field file_content string ---@field selected_code string | nil ---@field project_context string | nil +---@field selected_files SelectedFiles[] | nil ---@field diagnostics string | nil ---@field history_messages AvanteLLMMessage[] --- @@ -357,8 +359,22 @@ end ---@param opts StreamOptions M.stream = function(opts) - if opts.on_chunk ~= nil then opts.on_chunk = vim.schedule_wrap(opts.on_chunk) end - if opts.on_complete ~= nil then opts.on_complete = vim.schedule_wrap(opts.on_complete) end + local is_completed = false + if opts.on_chunk ~= nil then + local original_on_chunk = opts.on_chunk + opts.on_chunk = vim.schedule_wrap(function(chunk) + if is_completed then return end + return original_on_chunk(chunk) + end) + end + if opts.on_complete ~= nil then + local original_on_complete = opts.on_complete + opts.on_complete = vim.schedule_wrap(function(err) + if is_completed then return end + is_completed = true + return original_on_complete(err) + end) + end local Provider = opts.provider or P[Config.provider] if Config.dual_boost.enabled then M._dual_boost_stream(opts, Provider, P[Config.dual_boost.first_provider], P[Config.dual_boost.second_provider]) diff --git a/lua/avante/path.lua b/lua/avante/path.lua index 93b9b11..864d2ba 100644 --- a/lua/avante/path.lua +++ b/lua/avante/path.lua @@ -15,6 +15,7 @@ local Config = require("avante.config") ---@field selected_file {filepath: string}? ---@field selected_code {filetype: string, content: string}? ---@field reset_memory boolean? +---@field selected_filepaths string[] | nil ---@class avante.Path ---@field history_path Path diff --git a/lua/avante/selection.lua b/lua/avante/selection.lua index 6930d77..698c2dd 100644 --- a/lua/avante/selection.lua +++ b/lua/avante/selection.lua @@ -210,7 +210,7 @@ function Selection:create_editing_input() ask = true, project_context = vim.json.encode(project_context), diagnostics = vim.json.encode(diagnostics), - file_content = code_content, + selected_files = { { content = code_content, file_type = filetype, path = "" } }, code_lang = filetype, selected_code = self.selection.content, instructions = input, diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index 29b3bcf..c07d206 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -12,10 +12,12 @@ local Llm = require("avante.llm") local Utils = require("avante.utils") local Highlights = require("avante.highlights") local RepoMap = require("avante.repo_map") +local FileSelector = require("avante.file_selector") local RESULT_BUF_NAME = "AVANTE_RESULT" local VIEW_BUFFER_UPDATED_PATTERN = "AvanteViewBufferUpdated" local CODEBLOCK_KEYBINDING_NAMESPACE = api.nvim_create_namespace("AVANTE_CODEBLOCK_KEYBINDING") +local SELECTED_FILES_HINT_NAMESPACE = api.nvim_create_namespace("AVANTE_SELECTED_FILES_HINT") local PRIORITY = vim.highlight.priorities.user ---@class avante.Sidebar @@ -30,10 +32,12 @@ local Sidebar = {} ---@field id integer ---@field augroup integer ---@field code avante.CodeState ----@field winids table this table stores the winids of the sidebar components (result, selected_code, input), even though they are destroyed. ----@field result NuiSplit | nil ----@field selected_code NuiSplit | nil ----@field input NuiSplit | nil +---@field winids table +---@field result_container NuiSplit | nil +---@field selected_code_container NuiSplit | nil +---@field selected_files_container NuiSplit | nil +---@field input_container NuiSplit | nil +---@field file_selector FileSelector ---@param id integer the tabpage id retrieved from api.nvim_get_current_tabpage() function Sidebar:new(id) @@ -41,13 +45,16 @@ function Sidebar:new(id) id = id, code = { bufnr = 0, winid = 0, selection = nil }, winids = { - result = 0, - selected_code = 0, - input = 0, + result_container = 0, + selected_files_container = 0, + selected_code_container = 0, + input_container = 0, }, - result = nil, - selected_code = nil, - input = nil, + result_container = nil, + selected_code_container = nil, + selected_files_container = nil, + input_container = nil, + file_selector = FileSelector:new(id), }, { __index = self }) end @@ -59,10 +66,12 @@ end function Sidebar:reset() self:delete_autocmds() self.code = { bufnr = 0, winid = 0, selection = nil } - self.winids = { result = 0, selected_code = 0, input = 0 } - self.result = nil - self.selected_code = nil - self.input = nil + self.winids = + { result_container = 0, selected_files_container = 0, selected_code_container = 0, input_container = 0 } + self.result_container = nil + self.selected_code_container = nil + self.selected_files_container = nil + self.input_container = nil end ---@class SidebarOpenOptions: AskOptions @@ -118,18 +127,18 @@ end ---@return boolean function Sidebar:focus() if self:is_open() then - fn.win_gotoid(self.result.winid) + fn.win_gotoid(self.result_container.winid) return true end return false end function Sidebar:is_open() - return self.result - and self.result.bufnr - and api.nvim_buf_is_valid(self.result.bufnr) - and self.result.winid - and api.nvim_win_is_valid(self.result.winid) + return self.result_container + and self.result_container.bufnr + and api.nvim_buf_is_valid(self.result_container.bufnr) + and self.result_container.winid + and api.nvim_win_is_valid(self.result_container.winid) end function Sidebar:in_code_win() return self.code.winid == api.nvim_get_current_win() end @@ -148,19 +157,18 @@ end ---@class AvanteReplacementResult ---@field content string +---@field current_filepath string ---@field is_searching boolean ---@field is_replacing boolean ---@field last_search_tag_start_line integer ---@field last_replace_tag_start_line integer ----@param original_content string +---@param selected_files {path: string, content: string, file_type: string | nil}[] ---@param result_content string ----@param code_lang string ---@return AvanteReplacementResult -local function transform_result_content(original_content, result_content, code_lang) +local function transform_result_content(selected_files, result_content, prev_filepath) local transformed_lines = {} - local original_lines = vim.split(original_content, "\n") local result_lines = vim.split(result_content, "\n") local is_searching = false @@ -170,12 +178,15 @@ local function transform_result_content(original_content, result_content, code_l local search_start = 0 + local current_filepath + local i = 1 while i <= #result_lines do local line_content = result_lines[i] if line_content:match(".+") then local filepath = line_content:match("(.+)") if filepath then + current_filepath = filepath table.insert(transformed_lines, string.format("Filepath: %s", filepath)) goto continue end @@ -196,21 +207,29 @@ local function transform_result_content(original_content, result_content, code_l local start_line = 0 local end_line = 0 - for j = 1, #original_lines - (search_end - search_start) + 1 do - local match = true - for k = 0, search_end - search_start - 1 do - if - Utils.remove_indentation(original_lines[j + k]) ~= Utils.remove_indentation(result_lines[search_start + k]) - then - match = false + local match_filetype = nil + for _, file in ipairs(selected_files) do + if not Utils.is_same_file(file.path, prev_filepath or "") then goto continue1 end + local file_content = vim.split(file.content, "\n") + if start_line ~= 0 or end_line ~= 0 then break end + for j = 1, #file_content - (search_end - search_start) + 1 do + local match = true + for k = 0, search_end - search_start - 1 do + if + Utils.remove_indentation(file_content[j + k]) ~= Utils.remove_indentation(result_lines[search_start + k]) + then + match = false + break + end + end + if match then + start_line = j + end_line = j + (search_end - search_start) - 1 + match_filetype = file.file_type break end end - if match then - start_line = j - end_line = j + (search_end - search_start) - 1 - break - end + ::continue1:: end local search_start_tag_idx_in_transformed_lines = 0 @@ -225,7 +244,7 @@ local function transform_result_content(original_content, result_content, code_l end vim.list_extend(transformed_lines, { string.format("Replace lines: %d-%d", start_line, end_line), - string.format("```%s", code_lang), + string.format("```%s", match_filetype), }) goto continue elseif line_content == "" then @@ -246,6 +265,7 @@ local function transform_result_content(original_content, result_content, code_l end return { + current_filepath = current_filepath, content = table.concat(transformed_lines, "\n"), is_searching = is_searching, is_replacing = is_replacing, @@ -609,8 +629,8 @@ function Sidebar:apply(current_cursor) all_snippets_map = ensure_snippets_no_overlap(all_snippets_map) local selected_snippets_map = {} if current_cursor then - if self.result and self.result.winid then - local cursor_line = Utils.get_cursor_pos(self.result.winid) + if self.result_container and self.result_container.winid then + local cursor_line = Utils.get_cursor_pos(self.result_container.winid) for filepath, snippets in pairs(all_snippets_map) do for _, snippet in ipairs(snippets) do if @@ -636,18 +656,30 @@ function Sidebar:apply(current_cursor) for filepath, snippets in pairs(selected_snippets_map) do local bufnr = Utils.get_or_create_buffer_with_filepath(filepath) insert_conflict_contents(bufnr, snippets) + local process = function(winid) + api.nvim_set_current_win(winid) + api.nvim_feedkeys(api.nvim_replace_termcodes("", true, false, true), "n", true) + Diff.add_visited_buffer(bufnr) + Diff.process(bufnr) + api.nvim_win_set_cursor(winid, { 1, 0 }) + vim.defer_fn(function() + Diff.find_next(Config.windows.ask.focus_on_apply) + vim.cmd("normal! zz") + end, 100) + end local winid = Utils.get_winid(bufnr) - if not winid then goto continue end - api.nvim_set_current_win(winid) - api.nvim_feedkeys(api.nvim_replace_termcodes("", true, false, true), "n", true) - Diff.add_visited_buffer(bufnr) - Diff.process(bufnr) - api.nvim_win_set_cursor(winid, { 1, 0 }) - vim.defer_fn(function() - Diff.find_next(Config.windows.ask.focus_on_apply) - vim.cmd("normal! zz") - end, 100) - ::continue:: + if winid then + process(winid) + else + api.nvim_create_autocmd("BufWinEnter", { + buffer = bufnr, + once = true, + callback = function() + local winid_ = Utils.get_winid(bufnr) + if winid_ then process(winid_) end + end, + }) + end end end, 10) end @@ -706,18 +738,25 @@ function Sidebar:render_header(winid, bufnr, header_text, hl, reverse_hl) end function Sidebar:render_result() - if not self.result or not self.result.bufnr or not api.nvim_buf_is_valid(self.result.bufnr) then return end + if + not self.result_container + or not self.result_container.bufnr + or not api.nvim_buf_is_valid(self.result_container.bufnr) + then + return + end local header_text = "󰭻 Avante" - self:render_header(self.result.winid, self.result.bufnr, header_text, Highlights.TITLE, Highlights.REVERSED_TITLE) + self:render_header( + self.result_container.winid, + self.result_container.bufnr, + header_text, + Highlights.TITLE, + Highlights.REVERSED_TITLE + ) end ----@param ask? boolean -function Sidebar:render_input(ask) - if ask == nil then ask = true end - if not self.input or not self.input.bufnr or not api.nvim_buf_is_valid(self.input.bufnr) then return end - - local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr }) - +function Sidebar:get_file_icon(filepath) + local filetype = vim.filetype.match({ filename = filepath }) or "unknown" ---@type string local icon ---@diagnostic disable-next-line: undefined-field @@ -732,30 +771,37 @@ function Sidebar:render_input(ask) icon = "" end end + return icon +end + +---@param ask? boolean +function Sidebar:render_input(ask) + if ask == nil then ask = true end + if + not self.input_container + or not self.input_container.bufnr + or not api.nvim_buf_is_valid(self.input_container.bufnr) + then + return + end - local code_file_fullpath = api.nvim_buf_get_name(self.code.bufnr) - local code_filename = fn.fnamemodify(code_file_fullpath, ":t") local header_text = string.format( - "󱜸 %s %s %s (" .. Config.mappings.sidebar.switch_windows .. ": switch focus)", - ask and "Ask" or "Chat with", - icon, - code_filename + "󱜸 %s (" .. Config.mappings.sidebar.switch_windows .. ": switch focus)", + ask and "Ask" or "Chat with" ) if self.code.selection ~= nil then header_text = string.format( - "󱜸 %s %s %s(%d:%d) (: switch focus)", + "󱜸 %s (%d:%d) (: switch focus)", ask and "Ask" or "Chat with", - icon, - code_filename, self.code.selection.range.start.lnum, self.code.selection.range.finish.lnum ) end self:render_header( - self.input.winid, - self.input.bufnr, + self.input_container.winid, + self.input_container.bufnr, header_text, Highlights.THIRD_TITLE, Highlights.REVERSED_THIRD_TITLE @@ -763,7 +809,11 @@ function Sidebar:render_input(ask) end function Sidebar:render_selected_code() - if not self.selected_code or not self.selected_code.bufnr or not api.nvim_buf_is_valid(self.selected_code.bufnr) then + if + not self.selected_code_container + or not self.selected_code_container.bufnr + or not api.nvim_buf_is_valid(self.selected_code_container.bufnr) + then return end @@ -783,8 +833,8 @@ function Sidebar:render_selected_code() ) self:render_header( - self.selected_code.winid, - self.selected_code.bufnr, + self.selected_code_container.winid, + self.selected_code_container.bufnr, header_text, Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE @@ -795,17 +845,17 @@ end function Sidebar:on_mount(opts) self:refresh_winids() - api.nvim_set_option_value("wrap", Config.windows.wrap, { win = self.result.winid }) + api.nvim_set_option_value("wrap", Config.windows.wrap, { win = self.result_container.winid }) local current_apply_extmark_id = nil local function show_apply_button(block) if current_apply_extmark_id then - api.nvim_buf_del_extmark(self.result.bufnr, CODEBLOCK_KEYBINDING_NAMESPACE, current_apply_extmark_id) + api.nvim_buf_del_extmark(self.result_container.bufnr, CODEBLOCK_KEYBINDING_NAMESPACE, current_apply_extmark_id) end current_apply_extmark_id = - api.nvim_buf_set_extmark(self.result.bufnr, CODEBLOCK_KEYBINDING_NAMESPACE, block.start_line, -1, { + api.nvim_buf_set_extmark(self.result_container.bufnr, CODEBLOCK_KEYBINDING_NAMESPACE, block.start_line, -1, { virt_text = { { string.format( @@ -827,12 +877,12 @@ function Sidebar:on_mount(opts) "n", Config.mappings.sidebar.apply_cursor, function() self:apply(true) end, - { buffer = self.result.bufnr, noremap = true, silent = true } + { buffer = self.result_container.bufnr, noremap = true, silent = true } ) end local function unbind_apply_key() - pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_cursor, { buffer = self.result.bufnr }) + pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_cursor, { buffer = self.result_container.bufnr }) end ---@type AvanteCodeblock[] @@ -840,7 +890,7 @@ function Sidebar:on_mount(opts) ---@param direction "next" | "prev" local function jump_to_codeblock(direction) - local cursor_line = api.nvim_win_get_cursor(self.result.winid)[1] + local cursor_line = api.nvim_win_get_cursor(self.result_container.winid)[1] ---@type AvanteCodeblock local target_block @@ -863,7 +913,7 @@ function Sidebar:on_mount(opts) end if target_block then - api.nvim_win_set_cursor(self.result.winid, { target_block.start_line + 1, 0 }) + api.nvim_win_set_cursor(self.result_container.winid, { target_block.start_line + 1, 0 }) vim.cmd("normal! zz") end end @@ -873,32 +923,36 @@ function Sidebar:on_mount(opts) "n", Config.mappings.sidebar.apply_all, function() self:apply(false) end, - { buffer = self.result.bufnr, noremap = true, silent = true } + { buffer = self.result_container.bufnr, noremap = true, silent = true } ) vim.keymap.set( "n", Config.mappings.jump.next, function() jump_to_codeblock("next") end, - { buffer = self.result.bufnr, noremap = true, silent = true } + { buffer = self.result_container.bufnr, noremap = true, silent = true } ) vim.keymap.set( "n", Config.mappings.jump.prev, function() jump_to_codeblock("prev") end, - { buffer = self.result.bufnr, noremap = true, silent = true } + { buffer = self.result_container.bufnr, noremap = true, silent = true } ) end local function unbind_sidebar_keys() - if self.result and self.result.bufnr and api.nvim_buf_is_valid(self.result.bufnr) then - pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_all, { buffer = self.result.bufnr }) - pcall(vim.keymap.del, "n", Config.mappings.jump.next, { buffer = self.result.bufnr }) - pcall(vim.keymap.del, "n", Config.mappings.jump.prev, { buffer = self.result.bufnr }) + if + self.result_container + and self.result_container.bufnr + and api.nvim_buf_is_valid(self.result_container.bufnr) + then + pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_all, { buffer = self.result_container.bufnr }) + pcall(vim.keymap.del, "n", Config.mappings.jump.next, { buffer = self.result_container.bufnr }) + pcall(vim.keymap.del, "n", Config.mappings.jump.prev, { buffer = self.result_container.bufnr }) end end api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { - buffer = self.result.bufnr, + buffer = self.result_container.bufnr, callback = function(ev) local block = is_cursor_in_codeblock(codeblocks) @@ -913,7 +967,7 @@ function Sidebar:on_mount(opts) }) api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, { - buffer = self.result.bufnr, + buffer = self.result_container.bufnr, callback = function(ev) codeblocks = parse_codeblocks(ev.buf) bind_sidebar_keys() @@ -923,14 +977,20 @@ function Sidebar:on_mount(opts) api.nvim_create_autocmd("User", { pattern = VIEW_BUFFER_UPDATED_PATTERN, callback = function() - if not self.result or not self.result.bufnr or not api.nvim_buf_is_valid(self.result.bufnr) then return end - codeblocks = parse_codeblocks(self.result.bufnr) + if + not self.result_container + or not self.result_container.bufnr + or not api.nvim_buf_is_valid(self.result_container.bufnr) + then + return + end + codeblocks = parse_codeblocks(self.result_container.bufnr) bind_sidebar_keys() end, }) api.nvim_create_autocmd("BufLeave", { - buffer = self.result.bufnr, + buffer = self.result_container.bufnr, callback = function() unbind_sidebar_keys() end, }) @@ -940,8 +1000,8 @@ function Sidebar:on_mount(opts) local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr }) - if self.selected_code ~= nil then - local selected_code_buf = self.selected_code.bufnr + if self.selected_code_container ~= nil then + local selected_code_buf = self.selected_code_container.bufnr if selected_code_buf ~= nil then if self.code.selection ~= nil then Utils.unlock_buf(selected_code_buf) @@ -955,11 +1015,11 @@ function Sidebar:on_mount(opts) api.nvim_create_autocmd("BufEnter", { group = self.augroup, - buffer = self.result.bufnr, + buffer = self.result_container.bufnr, callback = function() self:focus() - if self.input and self.input.winid and api.nvim_win_is_valid(self.input.winid) then - api.nvim_set_current_win(self.input.winid) + if self.input_container and self.input_container.winid and api.nvim_win_is_valid(self.input_container.winid) then + api.nvim_set_current_win(self.input_container.winid) if Config.windows.ask.start_insert then vim.cmd("startinsert") end end return true @@ -991,9 +1051,10 @@ function Sidebar:refresh_winids() end local winids = {} - if self.winids.result then table.insert(winids, self.winids.result) end - if self.winids.selected_code then table.insert(winids, self.winids.selected_code) end - if self.winids.input then table.insert(winids, self.winids.input) end + if self.winids.result_container then table.insert(winids, self.winids.result_container) end + if self.winids.selected_files_container then table.insert(winids, self.winids.selected_files_container) end + if self.winids.selected_code_container then table.insert(winids, self.winids.selected_code_container) end + if self.winids.input_container then table.insert(winids, self.winids.input_container) end local function switch_windows() local current_winid = api.nvim_get_current_win() @@ -1055,11 +1116,16 @@ function Sidebar:initialize() self.code.bufnr = api.nvim_get_current_buf() self.code.selection = Utils.get_visual_selection_and_range() + if not self.code.bufnr or not api.nvim_buf_is_valid(self.code.bufnr) then return self end + + self.file_selector:reset() + self.file_selector:add_selected_file(Utils.relative_path(api.nvim_buf_get_name(self.code.bufnr))) + return self end function Sidebar:is_focused_on_result() - return self:is_open() and self.result and self.result.winid == api.nvim_get_current_win() + return self:is_open() and self.result_container and self.result_container.winid == api.nvim_get_current_win() end function Sidebar:is_focused_on(winid) @@ -1100,7 +1166,7 @@ end ---@param content string concatenated content of the buffer ---@param opts? {focus?: boolean, scroll?: boolean, backspace?: integer, ignore_history?: boolean, callback?: fun(): nil} whether to focus the result view function Sidebar:update_content(content, opts) - if not self.result or not self.result.bufnr then return end + if not self.result_container or not self.result_container.bufnr then return end opts = vim.tbl_deep_extend("force", { focus = true, scroll = true, stream = false, callback = nil }, opts or {}) if not opts.ignore_history then local chat_history = Path.history.load(self.code.bufnr) @@ -1108,48 +1174,62 @@ function Sidebar:update_content(content, opts) end if opts.stream then local scroll_to_bottom = function() - local last_line = api.nvim_buf_line_count(self.result.bufnr) + local last_line = api.nvim_buf_line_count(self.result_container.bufnr) - local current_lines = Utils.get_buf_lines(last_line - 1, last_line, self.result.bufnr) + local current_lines = Utils.get_buf_lines(last_line - 1, last_line, self.result_container.bufnr) if #current_lines > 0 then local last_line_content = current_lines[1] local last_col = #last_line_content xpcall( - function() api.nvim_win_set_cursor(self.result.winid, { last_line, last_col }) end, + function() api.nvim_win_set_cursor(self.result_container.winid, { last_line, last_col }) end, function(err) return err end ) end end vim.schedule(function() - if not self.result or not self.result.bufnr or not api.nvim_buf_is_valid(self.result.bufnr) then return end - Utils.unlock_buf(self.result.bufnr) - if opts.backspace ~= nil and opts.backspace > 0 then delete_last_n_chars(self.result.bufnr, opts.backspace) end + if + not self.result_container + or not self.result_container.bufnr + or not api.nvim_buf_is_valid(self.result_container.bufnr) + then + return + end + Utils.unlock_buf(self.result_container.bufnr) + if opts.backspace ~= nil and opts.backspace > 0 then + delete_last_n_chars(self.result_container.bufnr, opts.backspace) + end scroll_to_bottom() local lines = vim.split(content, "\n") - api.nvim_buf_call(self.result.bufnr, function() api.nvim_put(lines, "c", true, true) end) - Utils.lock_buf(self.result.bufnr) - api.nvim_set_option_value("filetype", "Avante", { buf = self.result.bufnr }) + api.nvim_buf_call(self.result_container.bufnr, function() api.nvim_put(lines, "c", true, true) end) + Utils.lock_buf(self.result_container.bufnr) + api.nvim_set_option_value("filetype", "Avante", { buf = self.result_container.bufnr }) if opts.scroll then scroll_to_bottom() end if opts.callback ~= nil then opts.callback() end end) else vim.defer_fn(function() - if not self.result or not self.result.bufnr or not api.nvim_buf_is_valid(self.result.bufnr) then return end + if + not self.result_container + or not self.result_container.bufnr + or not api.nvim_buf_is_valid(self.result_container.bufnr) + then + return + end local lines = vim.split(content, "\n") - Utils.unlock_buf(self.result.bufnr) - Utils.update_buffer_content(self.result.bufnr, lines) - Utils.lock_buf(self.result.bufnr) - api.nvim_set_option_value("filetype", "Avante", { buf = self.result.bufnr }) + Utils.unlock_buf(self.result_container.bufnr) + Utils.update_buffer_content(self.result_container.bufnr, lines) + Utils.lock_buf(self.result_container.bufnr) + api.nvim_set_option_value("filetype", "Avante", { buf = self.result_container.bufnr }) if opts.focus and not self:is_focused_on_result() then xpcall(function() --- set cursor to bottom of result view - api.nvim_set_current_win(self.result.winid) + api.nvim_set_current_win(self.result_container.winid) end, function(err) return err end) end - if opts.scroll then Utils.buf_scroll_to_end(self.result.bufnr) end + if opts.scroll then Utils.buf_scroll_to_end(self.result_container.bufnr) end if opts.callback ~= nil then opts.callback() end end, 0) @@ -1164,14 +1244,19 @@ local function get_timestamp() return os.date("%Y-%m-%d %H:%M:%S") end ---@param provider string ---@param model string ---@param request string ----@param selected_file {filepath: string}? +---@param selected_filepaths string[] ---@param selected_code {filetype: string, content: string}? ---@return string -local function render_chat_record_prefix(timestamp, provider, model, request, selected_file, selected_code) +local function render_chat_record_prefix(timestamp, provider, model, request, selected_filepaths, selected_code) provider = provider or "unknown" model = model or "unknown" local res = "- Datetime: " .. timestamp .. "\n\n" .. "- Model: " .. provider .. "/" .. model - if selected_file ~= nil then res = res .. "\n\n- Selected file: " .. selected_file.filepath end + if selected_filepaths ~= nil then + res = res .. "\n\n- Selected files:" + for _, path in ipairs(selected_filepaths) do + res = res .. "\n - " .. path + end + end if selected_code ~= nil then res = res .. "\n\n- Selected code: " @@ -1181,6 +1266,7 @@ local function render_chat_record_prefix(timestamp, provider, model, request, se .. selected_code.content .. "\n```" end + return res .. "\n\n> " .. request:gsub("\n", "\n> "):gsub("([%w-_]+)%b[]", "`%0`") .. "\n\n" end @@ -1216,12 +1302,14 @@ function Sidebar:render_history_content(history) if idx < #history then content = content .. "---\n\n" end goto continue end + local selected_filepaths = entry.selected_filepaths + if not selected_filepaths then selected_filepaths = { entry.selected_file.filepath } end local prefix = render_chat_record_prefix( entry.timestamp, entry.provider, entry.model, entry.request or "", - entry.selected_file, + selected_filepaths, entry.selected_code ) content = content .. prefix @@ -1241,7 +1329,7 @@ end function Sidebar:get_content_between_separators() local separator = "---" local cursor_line, _ = Utils.get_cursor_pos() - local lines = Utils.get_buf_lines(0, -1, self.result.bufnr) + local lines = Utils.get_buf_lines(0, -1, self.result_container.bufnr) local start_line, end_line for i = cursor_line, 1, -1 do @@ -1360,33 +1448,33 @@ function Sidebar:get_commands() :totable() end -function Sidebar:create_selected_code() - if self.selected_code ~= nil then - self.selected_code:unmount() - self.selected_code = nil +function Sidebar:create_selected_code_container() + if self.selected_code_container ~= nil then + self.selected_code_container:unmount() + self.selected_code_container = nil end local selected_code_size = self:get_selected_code_size() if self.code.selection ~= nil then - self.selected_code = Split({ + self.selected_code_container = Split({ enter = false, relative = { type = "win", - winid = self.input.winid, + winid = self.input_container.winid, }, buf_options = buf_options, - win_options = vim.tbl_deep_extend("force", base_win_options, { - wrap = Config.windows.wrap, - }), - position = "top", size = { height = selected_code_size + 3, }, + position = "top", }) - self.selected_code:mount() + self.selected_code_container:mount() if self:get_layout() == "horizontal" then - api.nvim_win_set_height(self.result.winid, api.nvim_win_get_height(self.result.winid) - selected_code_size - 3) + api.nvim_win_set_height( + self.result_container.winid, + api.nvim_win_get_height(self.result_container.winid) - selected_code_size - 3 + ) end end end @@ -1396,8 +1484,8 @@ local generating_text = "**Generating response ...**\n" local hint_window = nil ---@param opts AskOptions -function Sidebar:create_input(opts) - if self.input then self.input:unmount() end +function Sidebar:create_input_container(opts) + if self.input_container then self.input_container:unmount() end if not self.code.bufnr or not api.nvim_buf_is_valid(self.code.bufnr) then return end @@ -1411,9 +1499,7 @@ function Sidebar:create_input(opts) local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr }) - local selected_file = { - filepath = Utils.relative_path(api.nvim_buf_get_name(self.code.bufnr)), - } + local selected_filepaths = self.file_selector:get_selected_filepaths() local selected_code = nil if self.code.selection ~= nil then @@ -1424,7 +1510,7 @@ function Sidebar:create_input(opts) end local content_prefix = - render_chat_record_prefix(timestamp, Config.provider, model, request, selected_file, selected_code) + render_chat_record_prefix(timestamp, Config.provider, model, request, selected_filepaths, selected_code) --- HACK: we need to set focus to true and scroll to false to --- prevent the cursor from jumping to the bottom of the @@ -1432,8 +1518,6 @@ function Sidebar:create_input(opts) self:update_content("", { focus = true, scroll = false }) self:update_content(content_prefix .. generating_text) - local content = table.concat(Utils.get_buf_lines(0, -1, self.code.bufnr), "\n") - local selected_code_content = nil if self.code.selection ~= nil then selected_code_content = self.code.selection.content end @@ -1475,14 +1559,21 @@ function Sidebar:create_input(opts) local original_response = "" local transformed_response = "" local displayed_response = "" + local current_path = "" local is_first_chunk = true ---@type AvanteChunkParser local on_chunk = function(chunk) original_response = original_response .. chunk - local transformed = transform_result_content(content, transformed_response .. chunk, filetype) + + local selected_files = self.file_selector:get_selected_files_contents() + + local transformed = transform_result_content(selected_files, transformed_response .. chunk, current_path) transformed_response = transformed.content + if transformed.current_filepath and transformed.current_filepath ~= "" then + current_path = transformed.current_filepath + end local cur_displayed_response = generate_display_content(transformed) if is_first_chunk then is_first_chunk = false @@ -1517,8 +1608,12 @@ function Sidebar:create_input(opts) ) vim.defer_fn(function() - if self.result and self.result.winid and api.nvim_win_is_valid(self.result.winid) then - api.nvim_set_current_win(self.result.winid) + if + self.result_container + and self.result_container.winid + and api.nvim_win_is_valid(self.result_container.winid) + then + api.nvim_set_current_win(self.result_container.winid) end if Config.behaviour.auto_apply_diff_after_generation then self:apply(false) end end, 0) @@ -1531,7 +1626,7 @@ function Sidebar:create_input(opts) request = request, response = displayed_response, original_response = original_response, - selected_file = selected_file, + selected_filepaths = selected_filepaths, selected_code = selected_code, }) Path.history.save(self.code.bufnr, chat_history) @@ -1544,6 +1639,8 @@ function Sidebar:create_input(opts) local project_context = mentions.enable_project_context and RepoMap.get_repo_map(file_ext) or nil + local selected_files_contents = self.file_selector:get_selected_files_contents() + local diagnostics = nil if mentions.enable_diagnostics then if self.code ~= nil and self.code.bufnr ~= nil and self.code.selection ~= nil then @@ -1586,9 +1683,9 @@ function Sidebar:create_input(opts) bufnr = self.code.bufnr, ask = opts.ask, project_context = vim.json.encode(project_context), + selected_files = selected_files_contents, diagnostics = vim.json.encode(diagnostics), history_messages = history_messages, - file_content = content, code_lang = filetype, selected_code = selected_code_content, instructions = request, @@ -1612,15 +1709,19 @@ function Sidebar:create_input(opts) return { width = "40%", - height = math.max(1, api.nvim_win_get_height(self.result.winid) - selected_code_size), + height = math.max(1, api.nvim_win_get_height(self.result_container.winid) - selected_code_size), } end - self.input = Split({ + self.input_container = Split({ enter = false, relative = { type = "win", - winid = self.result.winid, + winid = self.result_container.winid, + }, + buf_options = { + swapfile = false, + buftype = "nofile", }, win_options = vim.tbl_deep_extend("force", base_win_options, { signcolumn = "yes", wrap = Config.windows.wrap }), position = get_position(), @@ -1632,15 +1733,21 @@ function Sidebar:create_input(opts) Utils.warn("Sending message to fast!, API key is not yet set", { title = "Avante" }) return end - if not self.input or not self.input.bufnr or not api.nvim_buf_is_valid(self.input.bufnr) then return end - local lines = api.nvim_buf_get_lines(self.input.bufnr, 0, -1, false) + if + not self.input_container + or not self.input_container.bufnr + or not api.nvim_buf_is_valid(self.input_container.bufnr) + then + return + end + local lines = api.nvim_buf_get_lines(self.input_container.bufnr, 0, -1, false) local request = table.concat(lines, "\n") if request == "" then return end - api.nvim_buf_set_lines(self.input.bufnr, 0, -1, false, {}) + api.nvim_buf_set_lines(self.input_container.bufnr, 0, -1, false, {}) handle_submit(request) end - self.input:mount() + self.input_container:mount() local function place_sign_at_first_line(bufnr) local group = "avante_input_prompt_group" @@ -1650,40 +1757,51 @@ function Sidebar:create_input(opts) fn.sign_place(0, group, "AvanteInputPromptSign", bufnr, { lnum = 1 }) end - place_sign_at_first_line(self.input.bufnr) + place_sign_at_first_line(self.input_container.bufnr) if Utils.in_visual_mode() then -- Exit visual mode api.nvim_feedkeys(api.nvim_replace_termcodes("", true, false, true), "n", true) end - self.input:map("n", Config.mappings.submit.normal, on_submit) - self.input:map("i", Config.mappings.submit.insert, on_submit) + self.input_container:map("n", Config.mappings.submit.normal, on_submit) + self.input_container:map("i", Config.mappings.submit.insert, on_submit) - api.nvim_set_option_value("filetype", "AvanteInput", { buf = self.input.bufnr }) + api.nvim_set_option_value("filetype", "AvanteInput", { buf = self.input_container.bufnr }) -- Setup completion api.nvim_create_autocmd("InsertEnter", { group = self.augroup, - buffer = self.input.bufnr, + buffer = self.input_container.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 + local mentions = Utils.get_mentions() + + table.insert(mentions, { + description = "file", + command = "file", + details = "add files...", + callback = function() self.file_selector:open() end, + }) + cmp.register_source( "avante_commands", - require("cmp_avante.commands"):new(self:get_commands(), self.input.bufnr) + require("cmp_avante.commands"):new(self:get_commands(), self.input_container.bufnr) ) cmp.register_source( "avante_mentions", - require("cmp_avante.mentions"):new(Utils.get_mentions(), self.input.bufnr) + require("cmp_avante.mentions"):new(mentions, self.input_container.bufnr) ) + cmp.setup.buffer({ enabled = true, sources = { { name = "avante_commands" }, { name = "avante_mentions" }, + { name = "avante_files" }, }, }) end @@ -1699,8 +1817,8 @@ function Sidebar:create_input(opts) end local function get_float_window_row() - local win_height = vim.api.nvim_win_get_height(self.input.winid) - local winline = Utils.winline(self.input.winid) + local win_height = api.nvim_win_get_height(self.input_container.winid) + local winline = Utils.winline(self.input_container.winid) if winline >= win_height - 1 then return 0 end return winline end @@ -1709,7 +1827,7 @@ function Sidebar:create_input(opts) local function show_hint() close_hint() -- Close the existing hint window - local hint_text = (vim.fn.mode() ~= "i" and Config.mappings.submit.normal or Config.mappings.submit.insert) + local hint_text = (fn.mode() ~= "i" and Config.mappings.submit.normal or Config.mappings.submit.insert) .. ": submit" local buf = api.nvim_create_buf(false, true) @@ -1717,13 +1835,13 @@ function Sidebar:create_input(opts) api.nvim_buf_add_highlight(buf, 0, "AvantePopupHint", 0, 0, -1) -- Get the current window size - local win_width = api.nvim_win_get_width(self.input.winid) + local win_width = api.nvim_win_get_width(self.input_container.winid) local width = #hint_text -- Set the floating window options local win_opts = { relative = "win", - win = self.input.winid, + win = self.input_container.winid, width = width, height = 1, row = get_float_window_row(), @@ -1740,16 +1858,16 @@ function Sidebar:create_input(opts) api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "VimResized" }, { group = self.augroup, - buffer = self.input.bufnr, + buffer = self.input_container.bufnr, callback = function() show_hint() - place_sign_at_first_line(self.input.bufnr) + place_sign_at_first_line(self.input_container.bufnr) end, }) api.nvim_create_autocmd("QuitPre", { group = self.augroup, - buffer = self.input.bufnr, + buffer = self.input_container.bufnr, callback = function() close_hint() end, }) @@ -1759,7 +1877,7 @@ function Sidebar:create_input(opts) pattern = "*:i", callback = function() local cur_buf = api.nvim_get_current_buf() - if self.input and cur_buf == self.input.bufnr then show_hint() end + if self.input_container and cur_buf == self.input_container.bufnr then show_hint() end end, }) @@ -1769,14 +1887,14 @@ function Sidebar:create_input(opts) pattern = "i:*", callback = function() local cur_buf = api.nvim_get_current_buf() - if self.input and cur_buf == self.input.bufnr then show_hint() end + if self.input_container and cur_buf == self.input_container.bufnr then show_hint() end end, }) api.nvim_create_autocmd("WinEnter", { callback = function() local cur_win = api.nvim_get_current_win() - if self.input and cur_win == self.input.winid then + if self.input_container and cur_win == self.input_container.winid then show_hint() else close_hint() @@ -1831,7 +1949,7 @@ function Sidebar:render(opts) return math.max(1, api.nvim_win_get_width(self.code.winid)) end - self.result = Split({ + self.result_container = Split({ enter = false, relative = "editor", position = get_position(), @@ -1851,25 +1969,27 @@ function Sidebar:render(opts) }, }) - self.result:mount() + self.result_container:mount() - self.augroup = api.nvim_create_augroup("avante_sidebar_" .. self.id .. self.result.winid, { clear = true }) + self.augroup = api.nvim_create_augroup("avante_sidebar_" .. self.id .. self.result_container.winid, { clear = true }) - self.result:on(event.BufWinEnter, function() - xpcall(function() api.nvim_buf_set_name(self.result.bufnr, RESULT_BUF_NAME) end, function(_) end) + self.result_container:on(event.BufWinEnter, function() + xpcall(function() api.nvim_buf_set_name(self.result_container.bufnr, RESULT_BUF_NAME) end, function(_) end) end) - self.result:map("n", "q", function() + self.result_container:map("n", "q", function() Llm.cancel_inflight_request() self:close() end) - self.result:map("n", "", function() + self.result_container:map("n", "", function() Llm.cancel_inflight_request() self:close() end) - self:create_input(opts) + self:create_input_container(opts) + + self:create_selected_files_container() self:update_content_with_history(chat_history) @@ -1878,11 +1998,138 @@ function Sidebar:render(opts) on_detach = function(_, _) self:reset() end, }) - self:create_selected_code() + self:create_selected_code_container() self:on_mount(opts) return self end +function Sidebar:create_selected_files_container() + if self.selected_files_container then self.selected_files_container:unmount() end + + local selected_filepaths = self.file_selector:get_selected_filepaths() + if #selected_filepaths == 0 then + self.file_selector:off("update") + self.file_selector:on("update", function() self:create_selected_files_container() end) + return + end + + self.selected_files_container = Split({ + enter = false, + relative = { + type = "win", + winid = self.input_container.winid, + }, + buf_options = vim.tbl_deep_extend("force", buf_options, { + modifiable = false, + swapfile = false, + buftype = "nofile", + bufhidden = "wipe", + filetype = "Avante", + }), + win_options = vim.tbl_deep_extend("force", base_win_options, { + wrap = Config.windows.wrap, + }), + position = "top", + size = { + width = "40%", + height = 2, + }, + }) + + self.selected_files_container:mount() + + local render = function() + local selected_filepaths_ = self.file_selector:get_selected_filepaths() + + if #selected_filepaths_ == 0 then + self.selected_files_container:unmount() + return + end + + local selected_filepaths_with_icon = {} + for _, filepath in ipairs(selected_filepaths_) do + local icon = self:get_file_icon(filepath) + table.insert(selected_filepaths_with_icon, string.format("%s %s", icon, filepath)) + end + + local selected_files_buf = api.nvim_win_get_buf(self.selected_files_container.winid) + Utils.unlock_buf(selected_files_buf) + api.nvim_buf_set_lines(selected_files_buf, 0, -1, true, selected_filepaths_with_icon) + Utils.lock_buf(selected_files_buf) + local win_height = math.min(vim.o.lines - 2, #selected_filepaths_ + 1) + api.nvim_win_set_height(self.selected_files_container.winid, win_height) + self:render_header( + self.selected_files_container.winid, + selected_files_buf, + " Selected Files", + Highlights.SUBTITLE, + Highlights.REVERSED_SUBTITLE + ) + end + + self.file_selector:on("update", render) + + local remove_file = function(line_number) + if self.file_selector:remove_selected_filepaths(line_number) then render() end + end + + -- Function to show hint + local function show_hint() + local cursor_pos = api.nvim_win_get_cursor(self.selected_files_container.winid) + local line_number = cursor_pos[1] + local col_number = cursor_pos[2] + + local selected_filepaths_ = self.file_selector:get_selected_filepaths() + local hint + if #selected_filepaths_ == 0 then + hint = string.format(" [%s: add] ", Config.mappings.sidebar.add_file) + else + hint = + string.format(" [%s: delete, %s: add] ", Config.mappings.sidebar.remove_file, Config.mappings.sidebar.add_file) + end + + api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1) + + api.nvim_buf_set_extmark( + self.selected_files_container.bufnr, + SELECTED_FILES_HINT_NAMESPACE, + line_number - 1, + col_number, + { + virt_text = { { hint, "AvanteInlineHint" } }, + virt_text_pos = "right_align", + hl_group = "AvanteInlineHint", + priority = PRIORITY, + } + ) + end + + -- Set up keybinding to remove files + self.selected_files_container:map("n", Config.mappings.sidebar.remove_file, function() + local line_number = api.nvim_win_get_cursor(self.selected_files_container.winid)[1] + remove_file(line_number) + end, { noremap = true, silent = true }) + + self.selected_files_container:map( + "n", + Config.mappings.sidebar.add_file, + function() self.file_selector:open() end, + { noremap = true, silent = true } + ) + + -- Set up autocmd to show hint on cursor move + self.selected_files_container:on({ event.CursorMoved }, show_hint, {}) + + -- Clear hint when leaving the window + self.selected_files_container:on( + event.BufLeave, + function() api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1) end, + {} + ) + + render() +end + return Sidebar diff --git a/lua/avante/suggestion.lua b/lua/avante/suggestion.lua index 3b3d348..82db64b 100644 --- a/lua/avante/suggestion.lua +++ b/lua/avante/suggestion.lua @@ -71,7 +71,7 @@ function Suggestion:suggest() provider = provider, bufnr = bufnr, ask = true, - file_content = code_content, + selected_files = { { content = code_content, file_type = filetype, path = "" } }, code_lang = filetype, instructions = vim.json.encode(doc), mode = "suggesting", diff --git a/lua/avante/templates/_context.avanterules b/lua/avante/templates/_context.avanterules index 7a94264..6639719 100644 --- a/lua/avante/templates/_context.avanterules +++ b/lua/avante/templates/_context.avanterules @@ -1,12 +1,15 @@ {%- if use_xml_format -%} -{{filepath}} {% if selected_code -%} +{% for file in selected_files %} +{{file.path}} + -```{{code_lang}} -{{file_content}} +```{{file.file_type}} +{{file.content}} ``` +{% endfor %} ```{{code_lang}} @@ -14,28 +17,38 @@ ``` {%- else -%} +{% for file in selected_files %} +{{file.path}} + -```{{code_lang}} -{{file_content}} +```{{file.file_type}} +{{file.content}} ``` +{% endfor %} {%- endif %} {% else %} -FILEPATH: {{filepath}} - {% if selected_code -%} +{% for file in selected_files %} +FILEPATH: {{file.path}} + CONTEXT: -```{{code_lang}} -{{file_content}} +```{{file.file_type}} +{{file.content}} ``` +{% endfor %} CODE: ```{{code_lang}} {{selected_code}} ``` {%- else -%} +{% for file in selected_files %} +FILEPATH: {{file.path}} + CODE: -```{{code_lang}} -{{file_content}} +```{{file.file_type}} +{{file.content}} ``` +{% endfor %} {%- endif %}{%- endif %} diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index 28ac2a2..9299e31 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -58,17 +58,17 @@ M.shell_run = function(input_cmd) -- powershell then we can just run the cmd if shell:match("powershell") or shell:match("pwsh") then cmd = input_cmd - elseif vim.fn.has("wsl") > 0 then + elseif fn.has("wsl") > 0 then -- wsl: powershell.exe -Command 'command "/path"' cmd = "powershell.exe -NoProfile -Command '" .. input_cmd:gsub("'", '"') .. "'" - elseif vim.fn.has("win32") > 0 then + elseif fn.has("win32") > 0 then cmd = 'powershell.exe -NoProfile -Command "' .. input_cmd:gsub('"', "'") .. '"' else -- linux and macos we wil just do sh -c - cmd = "sh -c " .. vim.fn.shellescape(input_cmd) + cmd = "sh -c " .. fn.shellescape(input_cmd) end - local output = vim.fn.system(cmd) + local output = fn.system(cmd) local code = vim.v.shell_error return { stdout = output, code = code } @@ -562,10 +562,10 @@ function M.debounce(func, delay) end function M.winline(winid) - local current_win = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(winid) - local line = vim.fn.winline() - vim.api.nvim_set_current_win(current_win) + local current_win = api.nvim_get_current_win() + api.nvim_set_current_win(winid) + local line = fn.winline() + api.nvim_set_current_win(current_win) return line end @@ -725,7 +725,7 @@ function M.get_or_create_buffer_with_filepath(filepath) api.nvim_set_current_buf(buf) -- Use the edit command to load the file content and set the buffer name - vim.cmd("edit " .. vim.fn.fnameescape(filepath)) + vim.cmd("edit " .. fn.fnameescape(filepath)) return buf end @@ -823,4 +823,13 @@ function M.get_current_selection_diagnostics(bufnr, selection) return selection_diagnostics end +function M.uniform_path(path) + local project_root = M.get_project_root() + local abs_path = Path:new(project_root):joinpath(path):absolute() + local relative_path = Path:new(abs_path):make_relative(project_root) + return relative_path +end + +function M.is_same_file(filepath_a, filepath_b) return M.uniform_path(filepath_a) == M.uniform_path(filepath_b) end + return M diff --git a/lua/cmp_avante/mentions.lua b/lua/cmp_avante/mentions.lua index fe4d9d1..7179b44 100644 --- a/lua/cmp_avante/mentions.lua +++ b/lua/cmp_avante/mentions.lua @@ -44,4 +44,37 @@ function mentions_source:complete(_, callback) }) end +---@param completion_item table +---@param callback fun(response: {behavior: number}) +function mentions_source:execute(completion_item, callback) + local current_line = api.nvim_get_current_line() + local label = completion_item.label:match("^@(%S+)") -- Extract mention command without '@' and space + + -- Find the corresponding mention + local selected_mention + for _, mention in ipairs(self.mentions) do + if mention.command == label then + selected_mention = mention + break + end + end + + -- Execute the mention's callback if it exists + if selected_mention and type(selected_mention.callback) == "function" then + selected_mention.callback(selected_mention) + -- Get the current cursor position + local row, col = unpack(api.nvim_win_get_cursor(0)) + + -- Replace the current line with the new line (removing the mention) + local new_line = current_line:gsub(vim.pesc(completion_item.label), "") + api.nvim_buf_set_lines(0, row - 1, row, false, { new_line }) + + -- Adjust the cursor position if needed + local new_col = math.min(col, #new_line) + api.nvim_win_set_cursor(0, { row, new_col }) + end + + callback({ behavior = require("cmp").ConfirmBehavior.Insert }) +end + return mentions_source