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