Files
avante.nvim/lua/avante/sidebar.lua
2025-06-23 03:13:37 +08:00

3333 lines
112 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
local api = vim.api
local fn = vim.fn
local Split = require("nui.split")
local event = require("nui.utils.autocmd").event
local PPath = require("plenary.path")
local Provider = require("avante.providers")
local Path = require("avante.path")
local Config = require("avante.config")
local Diff = require("avante.diff")
local Llm = require("avante.llm")
local Utils = require("avante.utils")
local PromptLogger = require("avante.utils.promptLogger")
local Highlights = require("avante.highlights")
local RepoMap = require("avante.repo_map")
local FileSelector = require("avante.file_selector")
local LLMTools = require("avante.llm_tools")
local HistoryMessage = require("avante.history_message")
local Line = require("avante.ui.line")
local RESULT_BUF_NAME = "AVANTE_RESULT"
local VIEW_BUFFER_UPDATED_PATTERN = "AvanteViewBufferUpdated"
local CODEBLOCK_KEYBINDING_NAMESPACE = api.nvim_create_namespace("AVANTE_CODEBLOCK_KEYBINDING")
local USER_REQUEST_BLOCK_KEYBINDING_NAMESPACE = api.nvim_create_namespace("AVANTE_USER_REQUEST_BLOCK_KEYBINDING")
local SELECTED_FILES_HINT_NAMESPACE = api.nvim_create_namespace("AVANTE_SELECTED_FILES_HINT")
local SELECTED_FILES_ICON_NAMESPACE = api.nvim_create_namespace("AVANTE_SELECTED_FILES_ICON")
local INPUT_HINT_NAMESPACE = api.nvim_create_namespace("AVANTE_INPUT_HINT")
local STATE_NAMESPACE = api.nvim_create_namespace("AVANTE_STATE")
local RESULT_BUF_HL_NAMESPACE = api.nvim_create_namespace("AVANTE_RESULT_BUF_HL")
local PRIORITY = (vim.hl or vim.highlight).priorities.user
local RESP_SEPARATOR = "-------"
---@class avante.Sidebar
local Sidebar = {}
Sidebar.__index = Sidebar
---@class avante.CodeState
---@field winid integer
---@field bufnr integer
---@field selection avante.SelectionResult | nil
---@field old_winhl string | nil
---@class avante.Sidebar
---@field id integer
---@field augroup integer
---@field code avante.CodeState
---@field winids table<"result_container" | "todos_container" | "selected_code_container" | "selected_files_container" | "input_container", integer>
---@field result_container NuiSplit | nil
---@field todos_container NuiSplit | nil
---@field selected_code_container NuiSplit | nil
---@field selected_files_container NuiSplit | nil
---@field input_container NuiSplit | nil
---@field file_selector FileSelector
---@field chat_history avante.ChatHistory | nil
---@field current_state avante.GenerateState | nil
---@field state_timer table | nil
---@field state_spinner_chars string[]
---@field thinking_spinner_chars string[]
---@field state_spinner_idx integer
---@field state_extmark_id integer | nil
---@field scroll boolean
---@field input_hint_window integer | nil
---@field ask_opts AskOptions
---@field old_result_lines avante.ui.Line[]
---@param id integer the tabpage id retrieved from api.nvim_get_current_tabpage()
function Sidebar:new(id)
return setmetatable({
id = id,
code = { bufnr = 0, winid = 0, selection = nil, old_winhl = nil },
winids = {
result_container = 0,
todos_container = 0,
selected_files_container = 0,
selected_code_container = 0,
input_container = 0,
},
result_container = nil,
todos_container = nil,
selected_code_container = nil,
selected_files_container = nil,
input_container = nil,
file_selector = FileSelector:new(id),
is_generating = false,
chat_history = nil,
current_state = nil,
state_timer = nil,
state_spinner_chars = { "·", "", "", "", "", "" },
thinking_spinner_chars = { "🤯", "🙄" },
state_spinner_idx = 1,
state_extmark_id = nil,
scroll = true,
input_hint_window = nil,
ask_opts = {},
old_result_lines = {},
-- 缓存相关字段
_cached_history_lines = nil,
_history_cache_invalidated = true,
}, Sidebar)
end
function Sidebar:delete_autocmds()
if self.augroup then api.nvim_del_augroup_by_id(self.augroup) end
self.augroup = nil
end
function Sidebar:reset()
-- clean up event handlers
if self.augroup then
api.nvim_del_augroup_by_id(self.augroup)
self.augroup = nil
end
-- clean up keymaps
self:unbind_apply_key()
self:unbind_sidebar_keys()
-- clean up file selector events
if self.file_selector then self.file_selector:off("update") end
if self.result_container then self.result_container:unmount() end
if self.selected_code_container then self.selected_code_container:unmount() end
if self.selected_files_container then self.selected_files_container:unmount() end
if self.input_container then self.input_container:unmount() end
self.code = { bufnr = 0, winid = 0, selection = nil }
self.winids =
{ result_container = 0, selected_files_container = 0, selected_code_container = 0, input_container = 0 }
self.result_container = nil
self.todos_container = nil
self.selected_code_container = nil
self.selected_files_container = nil
self.input_container = nil
self.scroll = true
self.old_result_lines = {}
end
---@class SidebarOpenOptions: AskOptions
---@field selection? avante.SelectionResult
---@param opts SidebarOpenOptions
function Sidebar:open(opts)
opts = opts or {}
local in_visual_mode = Utils.in_visual_mode() and self:in_code_win()
if not self:is_open() then
self:reset()
self:initialize()
if opts.selection then self.code.selection = opts.selection end
self:render(opts)
self:focus()
else
if in_visual_mode or opts.selection then
self:close()
self:reset()
self:initialize()
if opts.selection then self.code.selection = opts.selection end
self:render(opts)
return self
end
self:focus()
end
if not vim.g.avante_login or vim.g.avante_login == false then
api.nvim_exec_autocmds("User", { pattern = Provider.env.REQUEST_LOGIN_PATTERN })
vim.g.avante_login = true
end
return self
end
function Sidebar:setup_colors()
self:set_code_winhl()
vim.api.nvim_create_autocmd("WinNew", {
group = self.augroup,
callback = function()
for _, winid in ipairs(vim.api.nvim_list_wins()) do
if not vim.api.nvim_win_is_valid(winid) or self:is_sidebar_winid(winid) then goto continue end
local winhl = vim.wo[winid].winhl
if
winhl:find(Highlights.AVANTE_SIDEBAR_WIN_SEPARATOR)
and not Utils.should_hidden_border(self.code.winid, winid)
then
vim.wo[winid].winhl = self.code.old_winhl or ""
end
::continue::
end
self:set_code_winhl()
self:adjust_layout()
end,
})
end
function Sidebar:set_code_winhl()
if not self.code.winid or not api.nvim_win_is_valid(self.code.winid) then return end
if Utils.should_hidden_border(self.code.winid, self.winids.result_container) then
Utils.debug("setting winhl")
local old_winhl = vim.wo[self.code.winid].winhl
if self.code.old_winhl == nil then
self.code.old_winhl = old_winhl
else
old_winhl = self.code.old_winhl
end
local pieces = vim.split(old_winhl or "", ",")
local new_pieces = {}
for _, piece in ipairs(pieces) do
if not piece:find("WinSeparator:") and piece ~= "" then table.insert(new_pieces, piece) end
end
table.insert(new_pieces, "WinSeparator:" .. Highlights.AVANTE_SIDEBAR_WIN_SEPARATOR)
local new_winhl = table.concat(new_pieces, ",")
vim.wo[self.code.winid].winhl = new_winhl
end
end
function Sidebar:recover_code_winhl()
if self.code.old_winhl ~= nil then
vim.wo[self.code.winid].winhl = self.code.old_winhl
self.code.old_winhl = nil
end
end
---@class SidebarCloseOptions
---@field goto_code_win? boolean
---@param opts? SidebarCloseOptions
function Sidebar:close(opts)
opts = vim.tbl_extend("force", { goto_code_win = true }, opts or {})
self:delete_autocmds()
for _, comp in pairs(self) do
if comp and type(comp) == "table" and comp.unmount then comp:unmount() end
end
self.old_result_lines = {}
if opts.goto_code_win and self.code and self.code.winid and api.nvim_win_is_valid(self.code.winid) then
fn.win_gotoid(self.code.winid)
end
self:recover_code_winhl()
end
function Sidebar:shutdown()
Llm.cancel_inflight_request()
self:close()
vim.cmd("noautocmd stopinsert")
end
---@return boolean
function Sidebar:focus()
if self:is_open() then
fn.win_gotoid(self.result_container.winid)
return true
end
return false
end
function Sidebar:focus_input()
if Utils.is_valid_container(self.input_container, true) then api.nvim_set_current_win(self.input_container.winid) end
end
function Sidebar:is_open() return Utils.is_valid_container(self.result_container, true) end
function Sidebar:in_code_win() return self.code.winid == api.nvim_get_current_win() end
---@param opts AskOptions
function Sidebar:toggle(opts)
local in_visual_mode = Utils.in_visual_mode() and self:in_code_win()
if self:is_open() and not in_visual_mode then
self:close()
return false
else
---@cast opts SidebarOpenOptions
self:open(opts)
return true
end
end
---@class AvanteReplacementResult
---@field content string
---@field current_filepath string
---@field is_searching boolean
---@field is_replacing boolean
---@field is_thinking boolean
---@field waiting_for_breakline boolean
---@field last_search_tag_start_line integer
---@field last_replace_tag_start_line integer
---@field last_think_tag_start_line integer
---@field last_think_tag_end_line integer
---@param result_content string
---@param prev_filepath string
---@return AvanteReplacementResult
local function transform_result_content(result_content, prev_filepath)
local transformed_lines = {}
local result_lines = vim.split(result_content, "\n")
local is_searching = false
local is_replacing = false
local is_thinking = false
local last_search_tag_start_line = 0
local last_replace_tag_start_line = 0
local last_think_tag_start_line = 0
local last_think_tag_end_line = 0
local search_start = 0
local current_filepath
local waiting_for_breakline = false
local i = 1
while true do
if i > #result_lines then break end
local line_content = result_lines[i]
local matched_filepath =
line_content:match("<[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>(.+)</[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>")
if matched_filepath then
if i > 1 then
local prev_line = result_lines[i - 1]
if prev_line and prev_line:match("^%s*```%w+$") then
transformed_lines = vim.list_slice(transformed_lines, 1, #transformed_lines - 1)
end
end
current_filepath = matched_filepath
table.insert(transformed_lines, string.format("Filepath: %s", matched_filepath))
goto continue
end
if line_content:match("^%s*<[Ss][Ee][Aa][Rr][Cc][Hh]>") then
is_searching = true
if not line_content:match("^%s*<[Ss][Ee][Aa][Rr][Cc][Hh]>%s*$") then
local search_start_line = line_content:match("<[Ss][Ee][Aa][Rr][Cc][Hh]>(.+)$")
line_content = "<SEARCH>"
result_lines[i] = line_content
if search_start_line and search_start_line ~= "" then table.insert(result_lines, i + 1, search_start_line) end
end
line_content = "<SEARCH>"
local prev_line = result_lines[i - 1]
if
prev_line
and prev_filepath
and not prev_line:match("Filepath:.+")
and not prev_line:match("<[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>.+</[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>")
then
table.insert(transformed_lines, string.format("Filepath: %s", prev_filepath))
end
local next_line = result_lines[i + 1]
if next_line and next_line:match("^%s*```%w+$") then i = i + 1 end
search_start = i + 1
last_search_tag_start_line = i
elseif line_content:match("</[Ss][Ee][Aa][Rr][Cc][Hh]>%s*$") then
if is_replacing then
result_lines[i] = line_content:gsub("</[Ss][Ee][Aa][Rr][Cc][Hh]>", "</REPLACE>")
goto continue_without_increment
end
-- Handle case where </SEARCH> is a suffix
if not line_content:match("^%s*</[Ss][Ee][Aa][Rr][Cc][Hh]>%s*$") then
local search_end_line = line_content:match("^(.+)</[Ss][Ee][Aa][Rr][Cc][Hh]>")
line_content = "</SEARCH>"
result_lines[i] = line_content
if search_end_line and search_end_line ~= "" then
table.insert(result_lines, i, search_end_line)
goto continue_without_increment
end
end
is_searching = false
local search_end = i
local prev_line = result_lines[i - 1]
if prev_line and prev_line:match("^%s*```$") then search_end = i - 1 end
local match_filetype = nil
local filepath = current_filepath or prev_filepath or ""
if filepath == "" then goto continue end
local file_content_lines = Utils.read_file_from_buf_or_disk(filepath) or {}
local file_type = Utils.get_filetype(filepath)
local search_lines = vim.list_slice(result_lines, search_start, search_end - 1)
local start_line, end_line = Utils.fuzzy_match(file_content_lines, search_lines)
if start_line ~= nil and end_line ~= nil then
match_filetype = file_type
else
start_line = 0
end_line = 0
end
-- when the filetype isn't detected, fallback to matching based on filepath.
-- can happen if the llm tries to edit or create a file outside of it's context.
if not match_filetype then
local snippet_file_path = current_filepath or prev_filepath
match_filetype = Utils.get_filetype(snippet_file_path)
end
local search_start_tag_idx_in_transformed_lines = 0
for j = 1, #transformed_lines do
if transformed_lines[j] == "<SEARCH>" then
search_start_tag_idx_in_transformed_lines = j
break
end
end
if search_start_tag_idx_in_transformed_lines > 0 then
transformed_lines = vim.list_slice(transformed_lines, 1, search_start_tag_idx_in_transformed_lines - 1)
end
waiting_for_breakline = true
vim.list_extend(transformed_lines, {
string.format("Replace lines: %d-%d", start_line, end_line),
string.format("```%s", match_filetype),
})
goto continue
elseif line_content:match("^%s*<[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>") then
is_replacing = true
if not line_content:match("^%s*<[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>%s*$") then
local replace_first_line = line_content:match("<[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>(.+)$")
line_content = "<REPLACE>"
result_lines[i] = line_content
if replace_first_line and replace_first_line ~= "" then
table.insert(result_lines, i + 1, replace_first_line)
end
end
local next_line = result_lines[i + 1]
if next_line and next_line:match("^%s*```%w+$") then i = i + 1 end
last_replace_tag_start_line = i
goto continue
elseif line_content:match("</[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>%s*$") then
-- Handle case where </REPLACE> is a suffix
if not line_content:match("^%s*</[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>%s*$") then
local replace_end_line = line_content:match("^(.+)</[Rr][Ee][Pp][Ll][Aa][Cc][Ee]>")
line_content = "</REPLACE>"
result_lines[i] = line_content
if replace_end_line and replace_end_line ~= "" then
table.insert(result_lines, i, replace_end_line)
goto continue_without_increment
end
end
is_replacing = false
local prev_line = result_lines[i - 1]
if not (prev_line and prev_line:match("^%s*```$")) then table.insert(transformed_lines, "```") end
local next_line = result_lines[i + 1]
if next_line and next_line:match("^%s*```%s*$") then i = i + 1 end
goto continue
elseif line_content == "<think>" then
is_thinking = true
last_think_tag_start_line = i
last_think_tag_end_line = 0
elseif line_content == "</think>" then
is_thinking = false
last_think_tag_end_line = i
elseif line_content:match("^%s*```%s*$") then
local prev_line = result_lines[i - 1]
if prev_line and prev_line:match("^%s*```$") then goto continue end
end
waiting_for_breakline = false
table.insert(transformed_lines, line_content)
::continue::
i = i + 1
::continue_without_increment::
end
return {
current_filepath = current_filepath,
content = table.concat(transformed_lines, "\n"),
waiting_for_breakline = waiting_for_breakline,
is_searching = is_searching,
is_replacing = is_replacing,
is_thinking = is_thinking,
last_search_tag_start_line = last_search_tag_start_line,
last_replace_tag_start_line = last_replace_tag_start_line,
last_think_tag_start_line = last_think_tag_start_line,
last_think_tag_end_line = last_think_tag_end_line,
}
end
---@param replacement AvanteReplacementResult
---@return string
local function generate_display_content(replacement)
if replacement.is_searching then
return table.concat(
vim.list_slice(vim.split(replacement.content, "\n"), 1, replacement.last_search_tag_start_line - 1),
"\n"
)
end
if replacement.last_think_tag_start_line > 0 then
local lines = vim.split(replacement.content, "\n")
local last_think_tag_end_line = replacement.last_think_tag_end_line
if last_think_tag_end_line == 0 then last_think_tag_end_line = #lines + 1 end
local thinking_content_lines =
vim.list_slice(lines, replacement.last_think_tag_start_line + 2, last_think_tag_end_line - 1)
local formatted_thinking_content_lines = vim
.iter(thinking_content_lines)
:map(function(line)
if Utils.trim_spaces(line) == "" then return line end
return string.format(" > %s", line)
end)
:totable()
local result_lines = vim.list_extend(
vim.list_slice(lines, 1, replacement.last_search_tag_start_line),
{ Utils.icon("🤔 ") .. "Thought content:" }
)
result_lines = vim.list_extend(result_lines, formatted_thinking_content_lines)
result_lines = vim.list_extend(result_lines, vim.list_slice(lines, last_think_tag_end_line + 1))
return table.concat(result_lines, "\n")
end
return replacement.content
end
---@class AvanteCodeSnippet
---@field range integer[]
---@field content string
---@field lang string
---@field explanation string
---@field start_line_in_response_buf integer
---@field end_line_in_response_buf integer
---@field filepath string
---@param source string|integer
---@return TSNode[]
local function tree_sitter_markdown_parse_code_blocks(source)
local query = require("vim.treesitter.query")
local parser
if type(source) == "string" then
parser = vim.treesitter.get_string_parser(source, "markdown")
else
parser = vim.treesitter.get_parser(source, "markdown")
end
if parser == nil then
Utils.warn("Failed to get markdown parser")
return {}
end
local tree = parser:parse()[1]
local root = tree:root()
local code_block_query = query.parse(
"markdown",
[[ (fenced_code_block
(info_string
(language) @language)?
(block_continuation) @code_start
(fenced_code_block_delimiter) @code_end) ]]
)
local nodes = {}
for _, node in code_block_query:iter_captures(root, source) do
table.insert(nodes, node)
end
return nodes
end
---@param response_content string
---@return table<string, AvanteCodeSnippet[]>
local function extract_code_snippets_map(response_content)
local snippets = {}
local lines = vim.split(response_content, "\n")
-- use tree-sitter-markdown to parse all code blocks in response_content
local lang = "text"
local start_line, end_line
local start_line_in_response_buf, end_line_in_response_buf
local explanation_start_line = 0
for _, node in ipairs(tree_sitter_markdown_parse_code_blocks(response_content)) do
if node:type() == "language" then
lang = vim.treesitter.get_node_text(node, response_content)
elseif node:type() == "block_continuation" and node:start() > 1 then
start_line_in_response_buf = node:start()
local number_line = lines[start_line_in_response_buf - 1]
local _, start_line_str, end_line_str =
number_line:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)")
if start_line_str ~= nil and end_line_str ~= nil then
start_line = tonumber(start_line_str)
end_line = tonumber(end_line_str)
else
_, start_line_str = number_line:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ine:?%s*(%d+)")
if start_line_str ~= nil then
start_line = tonumber(start_line_str)
end_line = tonumber(start_line_str)
else
start_line_str = number_line:match("[Aa]fter%s+[Ll]ine:?%s*(%d+)")
if start_line_str ~= nil then
start_line = tonumber(start_line_str) + 1
end_line = tonumber(start_line_str) + 1
end
end
end
elseif
node:type() == "fenced_code_block_delimiter"
and start_line_in_response_buf ~= nil
and node:start() >= start_line_in_response_buf
then
end_line_in_response_buf, _ = node:start()
if start_line ~= nil and end_line ~= nil then
local filepath = lines[start_line_in_response_buf - 2]
if filepath:match("^[Ff]ilepath:") then filepath = filepath:match("^[Ff]ilepath:%s*(.+)") end
local content =
table.concat(vim.list_slice(lines, start_line_in_response_buf + 1, end_line_in_response_buf), "\n")
local explanation = ""
if start_line_in_response_buf > explanation_start_line + 2 then
explanation =
table.concat(vim.list_slice(lines, explanation_start_line, start_line_in_response_buf - 3), "\n")
end
local snippet = {
range = { start_line, end_line },
content = content,
lang = lang,
explanation = explanation,
start_line_in_response_buf = start_line_in_response_buf,
end_line_in_response_buf = end_line_in_response_buf + 1,
filepath = filepath,
}
table.insert(snippets, snippet)
end
lang = "text"
explanation_start_line = end_line_in_response_buf + 2
end
end
local snippets_map = {}
for _, snippet in ipairs(snippets) do
if snippet.filepath == "" then goto continue end
snippets_map[snippet.filepath] = snippets_map[snippet.filepath] or {}
table.insert(snippets_map[snippet.filepath], snippet)
::continue::
end
return snippets_map
end
local function insert_conflict_contents(bufnr, snippets)
-- sort snippets by start_line
table.sort(snippets, function(a, b) return a.range[1] < b.range[1] end)
local lines = Utils.get_buf_lines(0, -1, bufnr)
local offset = 0
for _, snippet in ipairs(snippets) do
local start_line, end_line = unpack(snippet.range)
local first_line_content = lines[start_line]
local old_first_line_indentation = ""
if first_line_content then old_first_line_indentation = Utils.get_indentation(first_line_content) end
local result = {}
table.insert(result, "<<<<<<< HEAD")
for i = start_line, end_line do
table.insert(result, lines[i])
end
table.insert(result, "=======")
local snippet_lines = vim.split(snippet.content, "\n")
if #snippet_lines > 0 then
local new_first_line_indentation = Utils.get_indentation(snippet_lines[1])
if #old_first_line_indentation > #new_first_line_indentation then
local line_indentation = old_first_line_indentation:sub(#new_first_line_indentation + 1)
snippet_lines = vim.iter(snippet_lines):map(function(line) return line_indentation .. line end):totable()
end
end
vim.list_extend(result, snippet_lines)
table.insert(result, ">>>>>>> Snippet")
api.nvim_buf_set_lines(bufnr, offset + start_line - 1, offset + end_line, false, result)
offset = offset + #snippet_lines + 3
end
end
---@param codeblocks table<integer, any>
local function is_cursor_in_codeblock(codeblocks)
local cursor_line, _ = Utils.get_cursor_pos()
for _, block in ipairs(codeblocks) do
if cursor_line >= block.start_line and cursor_line <= block.end_line then return block end
end
return nil
end
---@class AvanteRespUserRequestBlock
---@field start_line number 1-indexed
---@field end_line number 1-indexed
---@field content string
---@return AvanteRespUserRequestBlock | nil
function Sidebar:get_current_user_request_block()
local current_resp_content, current_resp_start_line = self:get_content_between_separators()
if current_resp_content == nil then return nil end
if current_resp_content == "" then return nil end
local lines = vim.split(current_resp_content, "\n")
local start_line = nil
local end_line = nil
local content_lines = {}
for i = 1, #lines do
local line = lines[i]
local m = line:match("^>%s+(.+)$")
if m then
if start_line == nil then start_line = i end
table.insert(content_lines, m)
end_line = i
elseif line ~= "" then
if start_line ~= nil then
end_line = i - 2
break
end
else
if start_line ~= nil then table.insert(content_lines, line) end
end
end
if start_line == nil then return nil end
content_lines = vim.list_slice(content_lines, 1, #content_lines - 1)
local content = table.concat(content_lines, "\n")
return {
start_line = current_resp_start_line + start_line - 1,
end_line = current_resp_start_line + end_line - 1,
content = content,
}
end
function Sidebar:is_cursor_in_user_request_block()
local block = self:get_current_user_request_block()
if block == nil then return false end
local cursor_line = api.nvim_win_get_cursor(self.result_container.winid)[1]
return cursor_line >= block.start_line and cursor_line <= block.end_line
end
---@class AvanteCodeblock
---@field start_line integer 1-indexed
---@field end_line integer 1-indexed
---@field lang string
---@param buf integer
---@return AvanteCodeblock[]
local function parse_codeblocks(buf)
local codeblocks = {}
local lines = Utils.get_buf_lines(0, -1, buf)
local lang, start_line, valid
for _, node in ipairs(tree_sitter_markdown_parse_code_blocks(buf)) do
if node:type() == "language" then
lang = vim.treesitter.get_node_text(node, buf)
elseif node:type() == "block_continuation" then
start_line, _ = node:start()
elseif node:type() == "fenced_code_block_delimiter" and start_line ~= nil and node:start() >= start_line then
local end_line, _ = node:start()
valid = lines[start_line - 1]:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)")
~= nil
if valid then table.insert(codeblocks, { start_line = start_line, end_line = end_line + 1, lang = lang }) end
end
end
return codeblocks
end
---@param original_lines string[]
---@param snippet AvanteCodeSnippet
---@return AvanteCodeSnippet[]
local function minimize_snippet(original_lines, snippet)
local start_line = snippet.range[1]
local end_line = snippet.range[2]
local original_snippet_lines = vim.list_slice(original_lines, start_line, end_line)
local original_snippet_content = table.concat(original_snippet_lines, "\n")
local snippet_content = snippet.content
local snippet_lines = vim.split(snippet_content, "\n")
---@diagnostic disable-next-line: assign-type-mismatch
local patch = vim.diff( ---@type integer[][]
original_snippet_content,
snippet_content,
---@diagnostic disable-next-line: missing-fields
{ algorithm = "histogram", result_type = "indices", ctxlen = vim.o.scrolloff }
)
---@type AvanteCodeSnippet[]
local new_snippets = {}
for _, hunk in ipairs(patch) do
local start_a, count_a, start_b, count_b = unpack(hunk)
---@type AvanteCodeSnippet
local new_snippet = {
range = {
count_a > 0 and start_line + start_a - 1 or start_line + start_a,
start_line + start_a + math.max(count_a, 1) - 2,
},
content = table.concat(vim.list_slice(snippet_lines, start_b, start_b + count_b - 1), "\n"),
lang = snippet.lang,
explanation = snippet.explanation,
start_line_in_response_buf = snippet.start_line_in_response_buf,
end_line_in_response_buf = snippet.end_line_in_response_buf,
filepath = snippet.filepath,
}
table.insert(new_snippets, new_snippet)
end
return new_snippets
end
---@param filepath string
---@param snippets AvanteCodeSnippet[]
---@return table<string, AvanteCodeSnippet[]>
function Sidebar:minimize_snippets(filepath, snippets)
local original_lines = {}
local original_lines_ = Utils.read_file_from_buf_or_disk(filepath)
if original_lines_ then original_lines = original_lines_ end
local results = {}
for _, snippet in ipairs(snippets) do
local new_snippets = minimize_snippet(original_lines, snippet)
if new_snippets then
for _, new_snippet in ipairs(new_snippets) do
table.insert(results, new_snippet)
end
end
end
return results
end
function Sidebar:retry_user_request()
local block = self:get_current_user_request_block()
if not block then return end
self.handle_submit(block.content)
end
function Sidebar:edit_user_request()
local block = self:get_current_user_request_block()
if not block then return end
if Utils.is_valid_container(self.input_container) then
local lines = vim.split(block.content, "\n")
api.nvim_buf_set_lines(self.input_container.bufnr, 0, -1, false, lines)
api.nvim_set_current_win(self.input_container.winid)
api.nvim_win_set_cursor(self.input_container.winid, { 1, #lines > 0 and #lines[1] or 0 })
end
end
---@param current_cursor boolean
function Sidebar:apply(current_cursor)
local response, response_start_line = self:get_content_between_separators()
local all_snippets_map = extract_code_snippets_map(response)
local selected_snippets_map = {}
if current_cursor then
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
cursor_line >= snippet.start_line_in_response_buf + response_start_line - 1
and cursor_line <= snippet.end_line_in_response_buf + response_start_line - 1
then
selected_snippets_map[filepath] = { snippet }
break
end
end
end
end
else
selected_snippets_map = all_snippets_map
end
vim.defer_fn(function()
api.nvim_set_current_win(self.code.winid)
for filepath, snippets in pairs(selected_snippets_map) do
if Config.behaviour.minimize_diff then snippets = self:minimize_snippets(filepath, snippets) end
local bufnr = Utils.open_buffer(filepath)
local path_ = PPath:new(Utils.is_win() and filepath:gsub("/", "\\") or filepath)
path_:parent():mkdir({ parents = true, exists_ok = true })
insert_conflict_contents(bufnr, snippets)
local function process(winid)
api.nvim_set_current_win(winid)
vim.cmd("noautocmd stopinsert")
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 winid then
process(winid)
else
api.nvim_create_autocmd("BufWinEnter", {
group = self.augroup,
buffer = bufnr,
once = true,
callback = function()
local winid_ = Utils.get_winid(bufnr)
if winid_ then process(winid_) end
end,
})
end
end
end, 10)
end
local buf_options = {
modifiable = false,
swapfile = false,
buftype = "nofile",
}
local base_win_options = {
winfixbuf = true,
spell = false,
signcolumn = "no",
foldcolumn = "0",
number = false,
relativenumber = false,
winfixwidth = true,
list = false,
linebreak = true,
breakindent = true,
wrap = false,
cursorline = false,
fillchars = "eob: ",
winhighlight = "CursorLine:Normal,CursorColumn:Normal,WinSeparator:"
.. Highlights.AVANTE_SIDEBAR_WIN_SEPARATOR
.. ",Normal:"
.. Highlights.AVANTE_SIDEBAR_NORMAL,
winbar = "",
statusline = " ",
}
function Sidebar:render_header(winid, bufnr, header_text, hl, reverse_hl)
if not bufnr or not api.nvim_buf_is_valid(bufnr) then return end
local is_result_win = self.winids.result_container == winid
local separator_char = is_result_win and " " or "-"
if not Config.windows.sidebar_header.enabled then return end
if not Config.windows.sidebar_header.rounded then header_text = " " .. header_text .. " " end
local win_width = vim.api.nvim_win_get_width(winid)
local padding = math.floor((win_width - #header_text) / 2)
if Config.windows.sidebar_header.align ~= "center" then padding = win_width - #header_text end
local winbar_text = "%#" .. Highlights.AVANTE_SIDEBAR_WIN_HORIZONTAL_SEPARATOR .. "#"
if Config.windows.sidebar_header.align ~= "left" then
if not Config.windows.sidebar_header.rounded then winbar_text = winbar_text .. " " end
winbar_text = winbar_text .. string.rep(separator_char, padding)
end
-- if Config.windows.sidebar_header.align == "center" then
-- winbar_text = winbar_text .. "%="
-- elseif Config.windows.sidebar_header.align == "right" then
-- winbar_text = winbar_text .. "%="
-- end
if Config.windows.sidebar_header.rounded then
winbar_text = winbar_text .. "%#" .. reverse_hl .. "#" .. Utils.icon("", "") .. "%#" .. hl .. "#"
else
winbar_text = winbar_text .. "%#" .. hl .. "#"
end
winbar_text = winbar_text .. header_text
if Config.windows.sidebar_header.rounded then
winbar_text = winbar_text .. "%#" .. reverse_hl .. "#" .. Utils.icon("", "")
end
-- if Config.windows.sidebar_header.align == "center" then winbar_text = winbar_text .. "%=" end
winbar_text = winbar_text .. "%#" .. Highlights.AVANTE_SIDEBAR_WIN_HORIZONTAL_SEPARATOR .. "#"
if Config.windows.sidebar_header.align ~= "right" then
winbar_text = winbar_text .. string.rep(separator_char, padding)
end
api.nvim_set_option_value("winbar", winbar_text, { win = winid })
end
function Sidebar:render_result()
if not Utils.is_valid_container(self.result_container) then return end
local header_text = Utils.icon("󰭻 ") .. "Avante"
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 Utils.is_valid_container(self.input_container) then return end
local header_text = string.format(
"%s%s (" .. Config.mappings.sidebar.switch_windows .. ": switch focus)",
Utils.icon("󱜸 "),
ask and "Ask" or "Chat with"
)
if self.code.selection ~= nil then
header_text = string.format(
"%s%s (%d:%d) (<Tab>: switch focus)",
Utils.icon("󱜸 "),
ask and "Ask" or "Chat with",
self.code.selection.range.start.lnum,
self.code.selection.range.finish.lnum
)
end
self:render_header(
self.input_container.winid,
self.input_container.bufnr,
header_text,
Highlights.THIRD_TITLE,
Highlights.REVERSED_THIRD_TITLE
)
end
function Sidebar:render_selected_code()
if not Utils.is_valid_container(self.selected_code_container) then return end
local selected_code_lines_count = 0
local selected_code_max_lines_count = 5
if self.code.selection ~= nil then
local selected_code_lines = vim.split(self.code.selection.content, "\n")
selected_code_lines_count = #selected_code_lines
end
local header_text = Utils.icon("")
.. "Selected Code"
.. (
selected_code_lines_count > selected_code_max_lines_count
and " (Show only the first " .. tostring(selected_code_max_lines_count) .. " lines)"
or ""
)
self:render_header(
self.selected_code_container.winid,
self.selected_code_container.bufnr,
header_text,
Highlights.SUBTITLE,
Highlights.REVERSED_SUBTITLE
)
end
function Sidebar:bind_apply_key()
if self.result_container then
vim.keymap.set(
"n",
Config.mappings.sidebar.apply_cursor,
function() self:apply(true) end,
{ buffer = self.result_container.bufnr, noremap = true, silent = true }
)
end
end
function Sidebar:unbind_apply_key()
if self.result_container then
pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_cursor, { buffer = self.result_container.bufnr })
end
end
function Sidebar:bind_retry_user_request_key()
if self.result_container then
vim.keymap.set(
"n",
Config.mappings.sidebar.retry_user_request,
function() self:retry_user_request() end,
{ buffer = self.result_container.bufnr, noremap = true, silent = true }
)
end
end
function Sidebar:unbind_retry_user_request_key()
if self.result_container then
pcall(vim.keymap.del, "n", Config.mappings.sidebar.retry_user_request, { buffer = self.result_container.bufnr })
end
end
function Sidebar:bind_edit_user_request_key()
if self.result_container then
vim.keymap.set(
"n",
Config.mappings.sidebar.edit_user_request,
function() self:edit_user_request() end,
{ buffer = self.result_container.bufnr, noremap = true, silent = true }
)
end
end
function Sidebar:unbind_edit_user_request_key()
if self.result_container then
pcall(vim.keymap.del, "n", Config.mappings.sidebar.edit_user_request, { buffer = self.result_container.bufnr })
end
end
function Sidebar:bind_sidebar_keys(codeblocks)
---@param direction "next" | "prev"
local function jump_to_codeblock(direction)
local cursor_line = api.nvim_win_get_cursor(self.result_container.winid)[1]
---@type AvanteCodeblock
local target_block
if direction == "next" then
for _, block in ipairs(codeblocks) do
if block.start_line > cursor_line then
target_block = block
break
end
end
if not target_block and #codeblocks > 0 then target_block = codeblocks[1] end
elseif direction == "prev" then
for i = #codeblocks, 1, -1 do
if codeblocks[i].end_line < cursor_line then
target_block = codeblocks[i]
break
end
end
if not target_block and #codeblocks > 0 then target_block = codeblocks[#codeblocks] end
end
if target_block then
api.nvim_win_set_cursor(self.result_container.winid, { target_block.start_line, 0 })
vim.cmd("normal! zz")
end
end
vim.keymap.set(
"n",
Config.mappings.sidebar.apply_all,
function() self:apply(false) end,
{ 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_container.bufnr, noremap = true, silent = true }
)
vim.keymap.set(
"n",
Config.mappings.jump.prev,
function() jump_to_codeblock("prev") end,
{ buffer = self.result_container.bufnr, noremap = true, silent = true }
)
end
function Sidebar:unbind_sidebar_keys()
if Utils.is_valid_container(self.result_container) 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
---@param opts AskOptions
function Sidebar:on_mount(opts)
self:refresh_winids()
-- Add keymap to add current buffer while sidebar is open
if Config.mappings.files and Config.mappings.files.add_current then
vim.keymap.set("n", Config.mappings.files.add_current, function()
if self:is_open() and self.file_selector:add_current_buffer() then
vim.notify("Added current buffer to file selector", vim.log.levels.DEBUG, { title = "Avante" })
else
vim.notify("Failed to add current buffer", vim.log.levels.WARN, { title = "Avante" })
end
end, {
desc = "avante: add current buffer to file selector",
noremap = true,
silent = true,
})
end
api.nvim_set_option_value("wrap", Config.windows.wrap, { win = self.result_container.winid })
local current_apply_extmark_id = nil
---@param block AvanteCodeblock
local function show_apply_button(block)
if current_apply_extmark_id then
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_container.bufnr, CODEBLOCK_KEYBINDING_NAMESPACE, block.start_line - 1, -1, {
virt_text = {
{
string.format(
" [<%s>: apply this, <%s>: apply all] ",
Config.mappings.sidebar.apply_cursor,
Config.mappings.sidebar.apply_all
),
"AvanteInlineHint",
},
},
virt_text_pos = "right_align",
hl_group = "AvanteInlineHint",
priority = PRIORITY,
})
end
local current_user_request_block_extmark_id = nil
local function show_user_request_block_control_buttons()
if current_user_request_block_extmark_id then
api.nvim_buf_del_extmark(
self.result_container.bufnr,
USER_REQUEST_BLOCK_KEYBINDING_NAMESPACE,
current_user_request_block_extmark_id
)
end
local block = self:get_current_user_request_block()
if not block then return end
current_user_request_block_extmark_id = api.nvim_buf_set_extmark(
self.result_container.bufnr,
USER_REQUEST_BLOCK_KEYBINDING_NAMESPACE,
block.start_line - 1,
-1,
{
virt_text = {
{
string.format(
" [<%s>: retry, <%s>: edit] ",
Config.mappings.sidebar.retry_user_request,
Config.mappings.sidebar.edit_user_request
),
"AvanteInlineHint",
},
},
virt_text_pos = "right_align",
hl_group = "AvanteInlineHint",
priority = PRIORITY,
}
)
end
---@type AvanteCodeblock[]
local codeblocks = {}
api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
group = self.augroup,
buffer = self.result_container.bufnr,
callback = function(ev)
local in_codeblock = is_cursor_in_codeblock(codeblocks)
if in_codeblock then
show_apply_button(in_codeblock)
self:bind_apply_key()
else
api.nvim_buf_clear_namespace(ev.buf, CODEBLOCK_KEYBINDING_NAMESPACE, 0, -1)
self:unbind_apply_key()
end
local in_user_request_block = self:is_cursor_in_user_request_block()
if in_user_request_block then
show_user_request_block_control_buttons()
self:bind_retry_user_request_key()
self:bind_edit_user_request_key()
else
api.nvim_buf_clear_namespace(ev.buf, USER_REQUEST_BLOCK_KEYBINDING_NAMESPACE, 0, -1)
self:unbind_retry_user_request_key()
self:unbind_edit_user_request_key()
end
end,
})
if self.code.bufnr and api.nvim_buf_is_valid(self.code.bufnr) then
api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, {
group = self.augroup,
buffer = self.result_container.bufnr,
callback = function(ev)
codeblocks = parse_codeblocks(ev.buf)
self:bind_sidebar_keys(codeblocks)
end,
})
api.nvim_create_autocmd("User", {
group = self.augroup,
pattern = VIEW_BUFFER_UPDATED_PATTERN,
callback = function()
if not Utils.is_valid_container(self.result_container) then return end
codeblocks = parse_codeblocks(self.result_container.bufnr)
self:bind_sidebar_keys(codeblocks)
end,
})
end
api.nvim_create_autocmd("BufLeave", {
group = self.augroup,
buffer = self.result_container.bufnr,
callback = function() self:unbind_sidebar_keys() end,
})
self:render_result()
self:render_input(opts.ask)
self:render_selected_code()
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)
local lines = vim.split(self.code.selection.content, "\n")
api.nvim_buf_set_lines(selected_code_buf, 0, -1, false, lines)
Utils.lock_buf(selected_code_buf)
end
if self.code.bufnr and api.nvim_buf_is_valid(self.code.bufnr) then
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
api.nvim_set_option_value("filetype", filetype, { buf = selected_code_buf })
end
end
end
api.nvim_create_autocmd("BufEnter", {
group = self.augroup,
buffer = self.result_container.bufnr,
callback = function()
if Config.behaviour.auto_focus_sidebar then
self:focus()
if Utils.is_valid_container(self.input_container, true) then
api.nvim_set_current_win(self.input_container.winid)
vim.defer_fn(function()
if Config.windows.ask.start_insert then vim.cmd("noautocmd startinsert!") end
end, 300)
end
end
return true
end,
})
api.nvim_create_autocmd("WinClosed", {
group = self.augroup,
callback = function(args)
local closed_winid = tonumber(args.match)
if closed_winid == self.winids.selected_files_container then return end
if closed_winid == self.winids.todos_container then return end
if not self:is_sidebar_winid(closed_winid) then return end
self:close()
end,
})
for _, comp in pairs(self) do
if comp and type(comp) == "table" and comp.mount and comp.bufnr and api.nvim_buf_is_valid(comp.bufnr) then
Utils.mark_as_sidebar_buffer(comp.bufnr)
end
end
end
function Sidebar:refresh_winids()
self.winids = {}
for key, comp in pairs(self) do
if comp and type(comp) == "table" and comp.winid and api.nvim_win_is_valid(comp.winid) then
self.winids[key] = comp.winid
end
end
local winids = {}
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.todos_container then table.insert(winids, self.winids.todos_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()
winids = vim.iter(winids):filter(function(winid) return api.nvim_win_is_valid(winid) end):totable()
local current_idx = Utils.tbl_indexof(winids, current_winid) or 1
if current_idx == #winids then
current_idx = 1
else
current_idx = current_idx + 1
end
local winid = winids[current_idx]
if winid and api.nvim_win_is_valid(winid) then pcall(api.nvim_set_current_win, winid) end
end
local function reverse_switch_windows()
local current_winid = api.nvim_get_current_win()
winids = vim.iter(winids):filter(function(winid) return api.nvim_win_is_valid(winid) end):totable()
local current_idx = Utils.tbl_indexof(winids, current_winid) or 1
if current_idx == 1 then
current_idx = #winids
else
current_idx = current_idx - 1
end
local winid = winids[current_idx]
if winid and api.nvim_win_is_valid(winid) then api.nvim_set_current_win(winid) end
end
for _, winid in ipairs(winids) do
local buf = api.nvim_win_get_buf(winid)
Utils.safe_keymap_set(
{ "n", "i" },
Config.mappings.sidebar.switch_windows,
function() switch_windows() end,
{ buffer = buf, noremap = true, silent = true, nowait = true }
)
Utils.safe_keymap_set(
{ "n", "i" },
Config.mappings.sidebar.reverse_switch_windows,
function() reverse_switch_windows() end,
{ buffer = buf, noremap = true, silent = true, nowait = true }
)
end
end
function Sidebar:resize()
for _, comp in pairs(self) do
if comp and type(comp) == "table" and comp.winid and api.nvim_win_is_valid(comp.winid) then
api.nvim_win_set_width(comp.winid, Config.get_window_width())
end
end
self:render_result()
self:render_input()
self:render_selected_code()
vim.defer_fn(function() vim.cmd("AvanteRefresh") end, 200)
end
--- Initialize the sidebar instance.
--- @return avante.Sidebar The Sidebar instance.
function Sidebar:initialize()
self.code.winid = api.nvim_get_current_win()
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
-- check if the filetype of self.code.bufnr is disabled
local buf_ft = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
if vim.list_contains(Config.selector.exclude_auto_select, buf_ft) then return self end
local buf_path = api.nvim_buf_get_name(self.code.bufnr)
-- if the filepath is outside of the current working directory then we want the absolute path
local filepath = Utils.file.is_in_project(buf_path) and Utils.relative_path(buf_path) or buf_path
Utils.debug("Sidebar:initialize adding buffer to file selector", buf_path)
self.file_selector:reset()
local stat = vim.uv.fs_stat(filepath)
if stat == nil or stat.type == "file" then self.file_selector:add_selected_file(filepath) end
self:reload_chat_history()
return self
end
function Sidebar:is_focused()
if not self:is_open() then return false end
local current_winid = api.nvim_get_current_win()
if self.winids.result_container and self.winids.result_container == current_winid then return true end
if self.winids.selected_files_container and self.winids.selected_files_container == current_winid then
return true
end
if self.winids.selected_code_container and self.winids.selected_code_container == current_winid then return true end
if self.winids.input_container and self.winids.input_container == current_winid then return true end
return false
end
function Sidebar:is_focused_on_result()
return self:is_open() and self.result_container and self.result_container.winid == api.nvim_get_current_win()
end
function Sidebar:is_sidebar_winid(winid)
for _, stored_winid in pairs(self.winids) do
if stored_winid == winid then return true end
end
return false
end
---@return boolean
function Sidebar:should_auto_scroll()
if not self.result_container or not self.result_container.winid then return false end
if not api.nvim_win_is_valid(self.result_container.winid) then return false end
local win_height = api.nvim_win_get_height(self.result_container.winid)
local total_lines = api.nvim_buf_line_count(self.result_container.bufnr)
local topline = vim.fn.line("w0", self.result_container.winid)
local last_visible_line = topline + win_height - 1
local is_scrolled_to_bottom = last_visible_line >= total_lines - 1
return is_scrolled_to_bottom
end
---@param content string concatenated content of the buffer
---@param opts? {focus?: boolean, scroll?: boolean, backspace?: integer, callback?: fun(): nil} whether to focus the result view
function Sidebar:update_content(content, opts)
if not self.result_container or not self.result_container.bufnr then return end
-- 提前验证容器有效性,避免后续无效操作
if not Utils.is_valid_container(self.result_container) then return end
local should_auto_scroll = self:should_auto_scroll()
opts = vim.tbl_deep_extend(
"force",
{ focus = false, scroll = should_auto_scroll and self.scroll, callback = nil },
opts or {}
)
-- 缓存历史行,避免重复计算
local history_lines
if not self._cached_history_lines or self._history_cache_invalidated then
history_lines = self.get_history_lines(self.chat_history)
self._cached_history_lines = history_lines
self._history_cache_invalidated = false
else
history_lines = vim.deepcopy(self._cached_history_lines)
end
-- 批量处理内容行,减少表操作
if content ~= nil and content ~= "" then
local content_lines = vim.split(content, "\n")
local new_lines = { Line:new({ { "" } }) }
-- 预分配表大小,提升性能
for i = 1, #content_lines do
new_lines[i + 1] = Line:new({ { content_lines[i] } })
end
-- 一次性扩展,而不是逐个插入
vim.list_extend(history_lines, new_lines)
end
-- 使用 vim.schedule 而不是 vim.defer_fn(0),性能更好
-- 再次检查容器有效性
if not Utils.is_valid_container(self.result_container) then return end
self:clear_state()
-- 批量更新操作
local bufnr = self.result_container.bufnr
Utils.unlock_buf(bufnr)
Utils.update_buffer_lines(RESULT_BUF_HL_NAMESPACE, bufnr, self.old_result_lines, history_lines)
-- 缓存结果行
self.old_result_lines = history_lines
-- 批量设置选项
api.nvim_set_option_value("filetype", "Avante", { buf = bufnr })
Utils.lock_buf(bufnr)
-- 处理焦点和滚动
if opts.focus and not self:is_focused_on_result() then
xpcall(function() api.nvim_set_current_win(self.result_container.winid) end, function(err)
Utils.debug("Failed to set current win:", err)
return err
end)
end
if opts.scroll then Utils.buf_scroll_to_end(bufnr) end
-- 延迟执行回调和状态渲染
if opts.callback then vim.schedule(opts.callback) end
-- 最后渲染状态
vim.schedule(function()
self:render_state()
-- 延迟重绘,避免阻塞
vim.defer_fn(function() vim.cmd("redraw") end, 10)
end)
return self
end
---@param timestamp string|osdate
---@param provider string
---@param model string
---@param request string
---@param selected_filepaths string[]
---@param selected_code AvanteSelectedCode?
---@return string
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_filepaths ~= nil and #selected_filepaths > 0 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: "
.. "\n\n```"
.. (selected_code.file_type or "")
.. (selected_code.path and " " .. selected_code.path or "")
.. "\n"
.. selected_code.content
.. "\n```"
end
return res .. "\n\n> " .. request:gsub("\n", "\n> "):gsub("([%w-_]+)%b[]", "`%0`")
end
local function calculate_config_window_position()
local position = Config.windows.position
if position == "smart" then
-- get editor width
local editor_width = vim.o.columns
-- get editor height
local editor_height = vim.o.lines * 3
if editor_width > editor_height then
position = "right"
else
position = "bottom"
end
end
---@cast position -"smart", -string
return position
end
function Sidebar:get_layout()
return vim.tbl_contains({ "left", "right" }, calculate_config_window_position()) and "vertical" or "horizontal"
end
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param ctx table
---@return avante.ui.Line[]
local function get_message_lines(message, messages, ctx)
if message.visible == false then return {} end
local lines = Utils.message_to_lines(message, messages)
if message.is_user_submission then
ctx.selected_filepaths = message.selected_filepaths
local text = table.concat(vim.tbl_map(function(line) return tostring(line) end, lines), "\n")
local prefix = render_chat_record_prefix(
message.timestamp,
message.provider,
message.model,
text,
message.selected_filepaths,
message.selected_code
)
local res = {}
for _, line_ in ipairs(vim.split(prefix, "\n")) do
table.insert(res, Line:new({ { line_ } }))
end
return res
end
if message.message.role == "user" then
local res = {}
for _, line_ in ipairs(lines) do
local sections = { { "> " } }
sections = vim.list_extend(sections, line_.sections)
table.insert(res, Line:new(sections))
end
return res
end
if message.message.role == "assistant" then
local content = message.message.content
if type(content) == "table" and content[1].type == "tool_use" then return lines end
local text = table.concat(vim.tbl_map(function(line) return tostring(line) end, lines), "\n")
local transformed = transform_result_content(text, ctx.prev_filepath)
ctx.prev_filepath = transformed.current_filepath
local displayed_content = generate_display_content(transformed)
local res = {}
for _, line_ in ipairs(vim.split(displayed_content, "\n")) do
table.insert(res, Line:new({ { line_ } }))
end
return res
end
return lines
end
---@param history avante.ChatHistory
---@return avante.ui.Line[]
function Sidebar.get_history_lines(history)
local history_messages = Utils.get_history_messages(history)
local ctx = {}
---@type avante.ui.Line[][]
local group = {}
for _, message in ipairs(history_messages) do
local lines = get_message_lines(message, history_messages, ctx)
if #lines == 0 then goto continue end
if message.is_user_submission then table.insert(group, {}) end
local last_item = group[#group]
if last_item == nil then
table.insert(group, {})
last_item = group[#group]
end
if message.message.role == "assistant" and not message.just_for_display and tostring(lines[1]) ~= "" then
table.insert(lines, 1, Line:new({ { "" } }))
table.insert(lines, 1, Line:new({ { "" } }))
end
last_item = vim.list_extend(last_item, lines)
group[#group] = last_item
::continue::
end
local res = {}
for idx, item in ipairs(group) do
if idx ~= 1 then
res = vim.list_extend(res, { Line:new({ { "" } }), Line:new({ { RESP_SEPARATOR } }), Line:new({ { "" } }) })
end
res = vim.list_extend(res, item)
end
table.insert(res, Line:new({ { "" } }))
return res
end
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param ctx table
---@return string | nil
local function render_message(message, messages, ctx)
if message.visible == false then return nil end
local text = Utils.message_to_text(message, messages)
if text == "" then return nil end
if message.is_user_submission then
ctx.selected_filepaths = message.selected_filepaths
local prefix = render_chat_record_prefix(
message.timestamp,
message.provider,
message.model,
text,
message.selected_filepaths,
message.selected_code
)
return prefix
end
if message.message.role == "user" then
local lines = vim.split(text, "\n")
lines = vim.iter(lines):map(function(line) return "> " .. line end):totable()
text = table.concat(lines, "\n")
return text
end
if message.message.role == "assistant" then
local transformed = transform_result_content(text, ctx.prev_filepath)
ctx.prev_filepath = transformed.current_filepath
local displayed_content = generate_display_content(transformed)
return displayed_content
end
return ""
end
---@param history avante.ChatHistory
---@return string
function Sidebar.render_history_content(history)
local history_messages = Utils.get_history_messages(history)
local ctx = {}
local group = {}
for _, message in ipairs(history_messages) do
local text = render_message(message, history_messages, ctx)
if text == nil then goto continue end
if message.is_user_submission then table.insert(group, {}) end
local last_item = group[#group]
if last_item == nil then
table.insert(group, {})
last_item = group[#group]
end
if message.message.role == "assistant" and not message.just_for_display and text:sub(1, 2) ~= "\n\n" then
text = "\n\n" .. text
end
table.insert(last_item, text)
::continue::
end
local pieces = {}
for _, item in ipairs(group) do
table.insert(pieces, table.concat(item, ""))
end
return table.concat(pieces, "\n\n" .. RESP_SEPARATOR .. "\n\n") .. "\n\n"
end
function Sidebar:update_content_with_history()
self:reload_chat_history()
self:update_content("")
end
---@return string, integer
function Sidebar:get_content_between_separators()
local separator = RESP_SEPARATOR
local cursor_line, _ = Utils.get_cursor_pos()
local lines = Utils.get_buf_lines(0, -1, self.result_container.bufnr)
local start_line, end_line
for i = cursor_line, 1, -1 do
if lines[i] == separator then
start_line = i + 1
break
end
end
start_line = start_line or 1
for i = cursor_line, #lines do
if lines[i] == separator then
end_line = i - 1
break
end
end
end_line = end_line or #lines
if lines[cursor_line] == separator then
if cursor_line > 1 and lines[cursor_line - 1] ~= separator then
end_line = cursor_line - 1
elseif cursor_line < #lines and lines[cursor_line + 1] ~= separator then
start_line = cursor_line + 1
end
end
local content = table.concat(vim.list_slice(lines, start_line, end_line), "\n")
return content, start_line
end
function Sidebar:clear_history(args, cb)
self.current_state = nil
if next(self.chat_history) ~= nil then
self.chat_history.messages = {}
self.chat_history.entries = {}
Path.history.save(self.code.bufnr, self.chat_history)
self._history_cache_invalidated = true
self:reload_chat_history()
self:update_content_with_history()
self:update_content(
"Chat history cleared",
{ focus = false, scroll = false, callback = function() self:focus_input() end }
)
if cb then cb(args) end
else
self:update_content(
"Chat history is already empty",
{ focus = false, scroll = false, callback = function() self:focus_input() end }
)
end
end
function Sidebar:clear_state()
if self.state_extmark_id then
pcall(api.nvim_buf_del_extmark, self.result_container.bufnr, STATE_NAMESPACE, self.state_extmark_id)
end
self.state_extmark_id = nil
self.state_spinner_idx = 1
if self.state_timer then self.state_timer:stop() end
end
function Sidebar:render_state()
if not Utils.is_valid_container(self.result_container) then return end
if not self.current_state then return end
local lines = vim.api.nvim_buf_get_lines(self.result_container.bufnr, 0, -1, false)
if self.state_extmark_id then
api.nvim_buf_del_extmark(self.result_container.bufnr, STATE_NAMESPACE, self.state_extmark_id)
end
local spinner_chars = self.state_spinner_chars
if self.current_state == "thinking" then spinner_chars = self.thinking_spinner_chars end
local hl = "AvanteStateSpinnerGenerating"
if self.current_state == "tool calling" then hl = "AvanteStateSpinnerToolCalling" end
if self.current_state == "failed" then hl = "AvanteStateSpinnerFailed" end
if self.current_state == "succeeded" then hl = "AvanteStateSpinnerSucceeded" end
if self.current_state == "searching" then hl = "AvanteStateSpinnerSearching" end
if self.current_state == "thinking" then hl = "AvanteStateSpinnerThinking" end
if self.current_state == "compacting" then hl = "AvanteStateSpinnerCompacting" end
local spinner_char = spinner_chars[self.state_spinner_idx]
self.state_spinner_idx = (self.state_spinner_idx % #spinner_chars) + 1
if
self.current_state ~= "generating"
and self.current_state ~= "tool calling"
and self.current_state ~= "thinking"
and self.current_state ~= "compacting"
then
spinner_char = ""
end
local virt_line
if spinner_char == "" then
virt_line = " " .. self.current_state .. " "
else
virt_line = " " .. spinner_char .. " " .. self.current_state .. " "
end
local win_width = api.nvim_win_get_width(self.result_container.winid)
local padding = math.floor((win_width - vim.fn.strdisplaywidth(virt_line)) / 2)
local centered_virt_lines = {
{ { string.rep(" ", padding) }, { virt_line, hl } },
}
local line_num = math.max(0, #lines - 2)
self.state_extmark_id = api.nvim_buf_set_extmark(self.result_container.bufnr, STATE_NAMESPACE, line_num, 0, {
virt_lines = centered_virt_lines,
hl_eol = true,
hl_mode = "combine",
})
self.state_timer = vim.defer_fn(function() self:render_state() end, 160)
end
function Sidebar:compact_history_messages(args, cb)
local history_memory = self.chat_history.memory
local messages = Utils.get_history_messages(self.chat_history)
self.current_state = "compacting"
self:render_state()
self:update_content(
"compacting history messsages",
{ focus = false, scroll = true, callback = function() self:focus_input() end }
)
Llm.summarize_memory(history_memory and history_memory.content, messages, function(memory)
if memory then
self.chat_history.memory = memory
Path.history.save(self.code.bufnr, self.chat_history)
end
self:update_content("compacted!", { focus = false, scroll = true, callback = function() self:focus_input() end })
self.current_state = "compacted"
self:clear_state()
if cb then cb(args) end
end)
end
function Sidebar:new_chat(args, cb)
local history = Path.history.new(self.code.bufnr)
Path.history.save(self.code.bufnr, history)
self:reload_chat_history()
self.current_state = nil
self:update_content("New chat", { focus = false, scroll = false, callback = function() self:focus_input() end })
if cb then cb(args) end
vim.schedule(function() self:create_todos_container() end)
end
function Sidebar:save_history() Path.history.save(self.code.bufnr, self.chat_history) end
---@param uuids string[]
function Sidebar:delete_history_messages(uuids)
local history_messages = Utils.get_history_messages(self.chat_history)
for _, msg in ipairs(history_messages) do
if vim.list_contains(uuids, msg.uuid) then msg.is_deleted = true end
end
Path.history.save(self.code.bufnr, self.chat_history)
end
---@param todos avante.TODO[]
function Sidebar:update_todos(todos)
if self.chat_history == nil then self:reload_chat_history() end
if self.chat_history == nil then return end
self.chat_history.todos = todos
Path.history.save(self.code.bufnr, self.chat_history)
self:create_todos_container()
end
---@param messages avante.HistoryMessage | avante.HistoryMessage[]
function Sidebar:add_history_messages(messages)
local history_messages = Utils.get_history_messages(self.chat_history)
messages = vim.islist(messages) and messages or { messages }
for _, message in ipairs(messages) do
if message.is_user_submission then
message.provider = Config.provider
message.model = Config.get_provider_config(Config.provider).model
end
local idx = nil
for idx_, message_ in ipairs(history_messages) do
if message_.uuid == message.uuid then
idx = idx_
break
end
end
if idx ~= nil then
history_messages[idx] = message
else
table.insert(history_messages, message)
end
end
self.chat_history.messages = history_messages
self._history_cache_invalidated = true
self:save_history()
if
self.chat_history.title == "untitled"
and #messages > 0
and messages[1].just_for_display ~= true
and messages[1].state == "generated"
then
local first_msg_text = Utils.message_to_text(messages[1], messages)
local lines_ = vim.split(first_msg_text, "\n")
if #lines_ > 0 then
self.chat_history.title = lines_[1]
self:save_history()
end
end
local last_message = messages[#messages]
if last_message then
local content = last_message.message.content
if type(content) == "table" and content[1].type == "tool_use" then
self.current_state = "tool calling"
elseif type(content) == "table" and content[1].type == "thinking" then
self.current_state = "thinking"
elseif type(content) == "table" and content[1].type == "redacted_thinking" then
self.current_state = "thinking"
else
self.current_state = "generating"
end
end
xpcall(function() self:update_content("") end, function(err)
Utils.debug("Failed to update content:", err)
return nil
end)
end
---@param messages AvanteLLMMessage | AvanteLLMMessage[]
---@param options {visible?: boolean}
function Sidebar:add_chat_history(messages, options)
options = options or {}
messages = vim.islist(messages) and messages or { messages }
local is_first_user = true
local history_messages = {}
for _, message in ipairs(messages) do
local content = message.content
if message.role == "system" and type(content) == "string" then
---@cast content string
self.chat_history.system_prompt = content
goto continue
end
local history_message = HistoryMessage:new(message)
if message.role == "user" and is_first_user then
is_first_user = false
history_message.is_user_submission = true
history_message.provider = Config.provider
history_message.model = Config.get_provider_config(Config.provider).model
end
table.insert(history_messages, history_message)
::continue::
end
if options.visible ~= nil then
for _, history_message in ipairs(history_messages) do
history_message.visible = options.visible
end
end
self:add_history_messages(history_messages)
end
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 height = self:get_selected_code_container_height()
if self.code.selection ~= nil then
self.selected_code_container = Split({
enter = false,
relative = {
type = "win",
winid = self.input_container.winid,
},
buf_options = buf_options,
win_options = vim.tbl_deep_extend("force", base_win_options, {}),
size = {
height = height,
},
position = "top",
})
self.selected_code_container:mount()
self:adjust_layout()
end
end
function Sidebar:close_input_hint()
if self.input_hint_window and api.nvim_win_is_valid(self.input_hint_window) then
local buf = api.nvim_win_get_buf(self.input_hint_window)
if INPUT_HINT_NAMESPACE then api.nvim_buf_clear_namespace(buf, INPUT_HINT_NAMESPACE, 0, -1) end
api.nvim_win_close(self.input_hint_window, true)
api.nvim_buf_delete(buf, { force = true })
self.input_hint_window = nil
end
end
function Sidebar:get_input_float_window_row()
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
-- Create a floating window as a hint
function Sidebar:show_input_hint()
self:close_input_hint() -- Close the existing hint window
local hint_text = (fn.mode() ~= "i" and Config.mappings.submit.normal or Config.mappings.submit.insert) .. ": submit"
local function show()
local buf = api.nvim_create_buf(false, true)
api.nvim_buf_set_lines(buf, 0, -1, false, { hint_text })
api.nvim_buf_set_extmark(buf, INPUT_HINT_NAMESPACE, 0, 0, { hl_group = "AvantePopupHint", end_col = #hint_text })
-- Get the current window size
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_container.winid,
width = width,
height = 1,
row = self:get_input_float_window_row(),
col = math.max(win_width - width, 0), -- Display in the bottom right corner
style = "minimal",
border = "none",
focusable = false,
zindex = 100,
}
-- Create the floating window
self.input_hint_window = api.nvim_open_win(buf, false, win_opts)
end
if Config.behaviour.enable_token_counting then
local input_value = table.concat(api.nvim_buf_get_lines(self.input_container.bufnr, 0, -1, false), "\n")
self:get_generate_prompts_options(input_value, function(generate_prompts_options)
local tokens = Llm.calculate_tokens(generate_prompts_options) + Utils.tokens.calculate_tokens(input_value)
hint_text = "Tokens: " .. tostring(tokens) .. "; " .. hint_text
show()
end)
else
show()
end
end
function Sidebar:close_selected_files_hint()
if self.selected_files_container and api.nvim_win_is_valid(self.selected_files_container.winid) then
pcall(api.nvim_buf_clear_namespace, self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1)
end
end
function Sidebar:show_selected_files_hint()
self:close_selected_files_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_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
function Sidebar:reload_chat_history()
if not self.code.bufnr or not api.nvim_buf_is_valid(self.code.bufnr) then return end
self.chat_history = Path.history.load(self.code.bufnr)
self._history_cache_invalidated = true
end
---@param opts? {all?: boolean}
---@return avante.HistoryMessage[]
function Sidebar:get_history_messages_for_api(opts)
opts = opts or {}
local history_messages0 = Utils.get_history_messages(self.chat_history)
self.chat_history.messages = history_messages0
history_messages0 = vim
.iter(history_messages0)
:filter(function(message) return not message.just_for_display and not message.is_compacted end)
:totable()
if opts.all then return history_messages0 end
history_messages0 = vim
.iter(history_messages0)
:filter(function(message) return message.state ~= "generating" end)
:totable()
if self.chat_history and self.chat_history.memory then
local picked_messages = {}
for idx = #history_messages0, 1, -1 do
local message = history_messages0[idx]
if message.uuid == self.chat_history.memory.last_message_uuid then break end
table.insert(picked_messages, 1, message)
end
history_messages0 = picked_messages
end
local tool_id_to_tool_name = {}
local tool_id_to_path = {}
local tool_id_to_start_line = {}
local tool_id_to_end_line = {}
local viewed_files = {}
local last_modified_files = {}
local history_messages = {}
local failed_edit_tool_ids = {}
for idx, message in ipairs(history_messages0) do
if Utils.is_tool_result_message(message) then
local tool_use_message = Utils.get_tool_use_message(message, history_messages0)
local is_edit_func_call, _, _, path = Utils.is_edit_func_call_message(tool_use_message)
local tool_result = message.message.content[1]
-- Only track as failed if it's an error AND not user-declined
if is_edit_func_call and tool_result.is_error and not tool_result.is_user_declined then
failed_edit_tool_ids[tool_result.tool_use_id] = true
end
-- Only track as successful modification if not an error AND not user-declined
if
is_edit_func_call
and path
and not message.message.content[1].is_error
and not message.message.content[1].is_user_declined
then
local uniformed_path = Utils.uniform_path(path)
last_modified_files[uniformed_path] = idx
end
end
end
for idx, message in ipairs(history_messages0) do
if Utils.is_tool_use_message(message) then
if failed_edit_tool_ids[message.message.content[1].id] then goto continue end
end
table.insert(history_messages, message)
if Utils.is_tool_result_message(message) then
local tool_use_message = Utils.get_tool_use_message(message, history_messages0)
local is_edit_func_call, is_str_replace_editor_func_call, is_str_replace_based_edit_tool_func_call, path =
Utils.is_edit_func_call_message(tool_use_message)
--- For models like gpt-4o, the input parameter of replace_in_file is treated as the latest file content, so here we need to insert a fake view tool call to ensure it uses the latest file content
if is_edit_func_call and path and not message.message.content[1].is_error then
local uniformed_path = Utils.uniform_path(path)
local view_result, view_error = require("avante.llm_tools.view").func({ path = path }, nil, nil, nil)
if view_error then view_result = "Error: " .. view_error end
local get_diagnostics_tool_use_id = Utils.uuid()
local view_tool_use_id = Utils.uuid()
local view_tool_name = "view"
local view_tool_input = { path = path }
if is_str_replace_editor_func_call then
view_tool_name = "str_replace_editor"
view_tool_input = { command = "view", path = path }
end
if is_str_replace_based_edit_tool_func_call then
view_tool_name = "str_replace_based_edit_tool"
view_tool_input = { command = "view", path = path }
end
history_messages = vim.list_extend(history_messages, {
HistoryMessage:new({
role = "assistant",
content = string.format("Viewing file %s to get the latest content", path),
}, {
is_dummy = true,
}),
HistoryMessage:new({
role = "assistant",
content = {
{
type = "tool_use",
id = view_tool_use_id,
name = view_tool_name,
input = view_tool_input,
},
},
}, {
is_dummy = true,
}),
HistoryMessage:new({
role = "user",
content = {
{
type = "tool_result",
tool_use_id = view_tool_use_id,
content = view_result,
is_error = view_error ~= nil,
is_user_declined = false,
},
},
}, {
is_dummy = true,
}),
})
if last_modified_files[uniformed_path] == idx and Config.behaviour.auto_check_diagnostics then
local diagnostics = Utils.lsp.get_diagnostics_from_filepath(path)
history_messages = vim.list_extend(history_messages, {
HistoryMessage:new({
role = "assistant",
content = string.format(
"The file %s has been modified, let me check if there are any errors in the changes.",
path
),
}, {
is_dummy = true,
}),
HistoryMessage:new({
role = "assistant",
content = {
{
type = "tool_use",
id = get_diagnostics_tool_use_id,
name = "get_diagnostics",
input = { path = path },
},
},
}, {
is_dummy = true,
}),
HistoryMessage:new({
role = "user",
content = {
{
type = "tool_result",
tool_use_id = get_diagnostics_tool_use_id,
content = vim.json.encode(diagnostics),
is_error = false,
is_user_declined = false,
},
},
}, {
is_dummy = true,
}),
})
end
end
end
::continue::
end
for _, message in ipairs(history_messages) do
local content = message.message.content
if type(content) ~= "table" then goto continue end
for _, item in ipairs(content) do
if type(item) ~= "table" then goto continue1 end
if item.type ~= "tool_use" then goto continue1 end
local tool_name = item.name
if tool_name ~= "view" then goto continue1 end
local path = item.input.path
tool_id_to_tool_name[item.id] = tool_name
if path then
local uniform_path = Utils.uniform_path(path)
tool_id_to_path[item.id] = uniform_path
tool_id_to_start_line[item.id] = item.input.start_line
tool_id_to_end_line[item.id] = item.input.end_line
viewed_files[uniform_path] = item.id
end
::continue1::
end
::continue::
end
for _, message in ipairs(history_messages) do
local content = message.message.content
if type(content) == "table" then
for _, item in ipairs(content) do
if type(item) ~= "table" then goto continue end
if item.type ~= "tool_result" then goto continue end
local tool_name = tool_id_to_tool_name[item.tool_use_id]
if tool_name ~= "view" then goto continue end
if item.is_error then goto continue end
local path = tool_id_to_path[item.tool_use_id]
local latest_tool_id = viewed_files[path]
if not latest_tool_id then goto continue end
if latest_tool_id ~= item.tool_use_id then
item.content = string.format("The file %s has been updated. Please use the latest `view` tool result!", path)
else
local start_line = tool_id_to_start_line[item.tool_use_id]
local end_line = tool_id_to_end_line[item.tool_use_id]
local view_result, view_error = require("avante.llm_tools.view").func(
{ path = path, start_line = start_line, end_line = end_line },
nil,
nil,
nil
)
if view_error then view_result = "Error: " .. view_error end
item.content = view_result
item.is_error = view_error ~= nil
end
::continue::
end
end
end
local picked_messages = {}
local max_tool_use_count = 25
local tool_use_count = 0
for idx = #history_messages, 1, -1 do
local msg = history_messages[idx]
if tool_use_count > max_tool_use_count then
if Utils.is_tool_result_message(msg) then
local tool_use_message = Utils.get_tool_use_message(msg, history_messages)
if tool_use_message then
table.insert(
picked_messages,
1,
HistoryMessage:new({
role = "user",
content = {
{
type = "text",
text = string.format(
"Tool use [%s] is successful: %s",
tool_use_message.message.content[1].name,
tostring(not msg.message.content[1].is_error)
),
},
},
}, { is_dummy = true })
)
local msg_content = {}
table.insert(msg_content, {
type = "text",
text = string.format(
"Tool use %s(%s)",
tool_use_message.message.content[1].name,
vim.json.encode(tool_use_message.message.content[1].input)
),
})
table.insert(
picked_messages,
1,
HistoryMessage:new({ role = "assistant", content = msg_content }, { is_dummy = true })
)
end
elseif Utils.is_tool_use_message(msg) then
tool_use_count = tool_use_count + 1
goto continue
else
table.insert(picked_messages, 1, msg)
end
else
if Utils.is_tool_use_message(msg) then tool_use_count = tool_use_count + 1 end
table.insert(picked_messages, 1, msg)
end
::continue::
end
history_messages = picked_messages
local final_history_messages = {}
for _, msg in ipairs(history_messages) do
local tool_result_message
if Utils.is_tool_use_message(msg) then
tool_result_message = Utils.get_tool_result_message(msg, history_messages)
if not tool_result_message then goto continue end
end
if Utils.is_tool_result_message(msg) then goto continue end
table.insert(final_history_messages, msg)
if tool_result_message then table.insert(final_history_messages, tool_result_message) end
::continue::
end
return final_history_messages
end
---@param request string
---@param cb fun(opts: AvanteGeneratePromptsOptions): nil
function Sidebar:get_generate_prompts_options(request, cb)
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
local file_ext = nil
-- Get file extension safely
local buf_name = api.nvim_buf_get_name(self.code.bufnr)
if buf_name and buf_name ~= "" then file_ext = vim.fn.fnamemodify(buf_name, ":e") end
---@type AvanteSelectedCode | nil
local selected_code = nil
if self.code.selection ~= nil then
selected_code = {
path = self.code.selection.filepath,
file_type = self.code.selection.filetype,
content = self.code.selection.content,
}
end
local mentions = Utils.extract_mentions(request)
request = mentions.new_content
local project_context = mentions.enable_project_context and file_ext and RepoMap.get_repo_map(file_ext) or nil
local diagnostics = nil
if mentions.enable_diagnostics then
if self.code ~= nil and self.code.bufnr ~= nil and self.code.selection ~= nil then
diagnostics = Utils.lsp.get_current_selection_diagnostics(self.code.bufnr, self.code.selection)
else
diagnostics = Utils.lsp.get_diagnostics(self.code.bufnr)
end
end
local history_messages = self:get_history_messages_for_api()
local tools = vim.deepcopy(LLMTools.get_tools(request, history_messages))
table.insert(tools, {
name = "add_file_to_context",
description = "Add a file to the context",
---@type AvanteLLMToolFunc<{ rel_path: string }>
func = function(input)
self.file_selector:add_selected_file(input.rel_path)
return "Added file to context", nil
end,
param = {
type = "table",
fields = { { name = "rel_path", description = "Relative path to the file", type = "string" } },
},
returns = {},
})
table.insert(tools, {
name = "remove_file_from_context",
description = "Remove a file from the context",
---@type AvanteLLMToolFunc<{ rel_path: string }>
func = function(input)
self.file_selector:remove_selected_file(input.rel_path)
return "Removed file from context", nil
end,
param = {
type = "table",
fields = { { name = "rel_path", description = "Relative path to the file", type = "string" } },
},
returns = {},
})
local selected_filepaths = self.file_selector.selected_filepaths or {}
local ask = self.ask_opts.ask
if ask == nil then ask = true end
---@type AvanteGeneratePromptsOptions
local prompts_opts = {
ask = ask,
project_context = vim.json.encode(project_context),
selected_filepaths = selected_filepaths,
recently_viewed_files = Utils.get_recent_filepaths(),
diagnostics = vim.json.encode(diagnostics),
history_messages = history_messages,
code_lang = filetype,
selected_code = selected_code,
tools = tools,
}
if self.chat_history.system_prompt then
prompts_opts.prompt_opts = {
system_prompt = self.chat_history.system_prompt,
messages = history_messages,
}
end
if self.chat_history.memory then prompts_opts.memory = self.chat_history.memory.content end
cb(prompts_opts)
end
function Sidebar:create_input_container()
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
if self.chat_history == nil then self:reload_chat_history() end
---@param request string
local function handle_submit(request)
if Config.prompt_logger.enabled then PromptLogger.log_prompt(request) end
if self.is_generating then
self:add_history_messages({
HistoryMessage:new({ role = "user", content = request }),
})
return
end
if request:match("@codebase") and not vim.fn.expand("%:e") then
self:update_content("Please open a file first before using @codebase", { focus = false, scroll = false })
return
end
if request:sub(1, 1) == "/" then
local command, args = request:match("^/(%S+)%s*(.*)")
if command == nil then
self:update_content("Invalid command", { focus = false, scroll = false })
return
end
local cmds = Utils.get_commands()
---@type AvanteSlashCommand
local cmd = vim.iter(cmds):filter(function(cmd) return cmd.name == command end):totable()[1]
if cmd then
if command == "lines" then
cmd.callback(self, args, function(args_)
local _, _, question = args_:match("(%d+)-(%d+)%s+(.*)")
request = question
end)
elseif command == "commit" then
cmd.callback(self, args, function(question) request = question end)
else
cmd.callback(self, args)
return
end
else
self:update_content("Unknown command: " .. command, { focus = false, scroll = false })
return
end
end
local selected_filepaths = self.file_selector:get_selected_filepaths()
---@type AvanteSelectedCode | nil
local selected_code = nil
if self.code.selection ~= nil then
selected_code = {
path = self.code.selection.filepath,
file_type = self.code.selection.filetype,
content = self.code.selection.content,
}
end
--- HACK: we need to set focus to true and scroll to false to
--- prevent the cursor from jumping to the bottom of the
--- buffer at the beginning
self:update_content("", { focus = true, scroll = false })
---stop scroll when user presses j/k keys
local function on_j()
self.scroll = false
---perform scroll
vim.cmd("normal! j")
end
local function on_k()
self.scroll = false
---perform scroll
vim.cmd("normal! k")
end
local function on_G()
self.scroll = true
---perform scroll
vim.cmd("normal! G")
end
vim.keymap.set("n", "j", on_j, { buffer = self.result_container.bufnr })
vim.keymap.set("n", "k", on_k, { buffer = self.result_container.bufnr })
vim.keymap.set("n", "G", on_G, { buffer = self.result_container.bufnr })
---@type AvanteLLMStartCallback
local function on_start(_) end
---@param messages avante.HistoryMessage[]
local function on_messages_add(messages) self:add_history_messages(messages) end
---@param state avante.GenerateState
local function on_state_change(state)
self:clear_state()
self.current_state = state
self:render_state()
end
---@param tool_id string
---@param tool_name string
---@param log string
---@param state AvanteLLMToolUseState
local function on_tool_log(tool_id, tool_name, log, state)
if state == "generating" then on_state_change("tool calling") end
local tool_use_message = nil
for idx = #self.chat_history.messages, 1, -1 do
local message = self.chat_history.messages[idx]
local content = message.message.content
if type(content) == "table" and content[1].type == "tool_use" and content[1].id == tool_id then
tool_use_message = message
break
end
end
if not tool_use_message then
-- Utils.debug("tool_use message not found", tool_id, tool_name)
return
end
local tool_use_logs = tool_use_message.tool_use_logs or {}
local content = string.format("[%s]: %s", tool_name, log)
table.insert(tool_use_logs, content)
tool_use_message.tool_use_logs = tool_use_logs
self:save_history()
self:update_content("")
end
---@type AvanteLLMStopCallback
local function on_stop(stop_opts)
self.is_generating = false
pcall(function()
---remove keymaps
vim.keymap.del("n", "j", { buffer = self.result_container.bufnr })
vim.keymap.del("n", "k", { buffer = self.result_container.bufnr })
vim.keymap.del("n", "G", { buffer = self.result_container.bufnr })
end)
if stop_opts.error ~= nil then
local msg_content = stop_opts.error
if type(msg_content) ~= "string" then msg_content = vim.inspect(msg_content) end
self:add_history_messages({
HistoryMessage:new({
role = "assistant",
content = "\n\nError: " .. msg_content,
}, {
just_for_display = true,
}),
})
on_state_change("failed")
return
end
on_state_change("succeeded")
self:update_content("", {
callback = function() api.nvim_exec_autocmds("User", { pattern = VIEW_BUFFER_UPDATED_PATTERN }) end,
})
vim.defer_fn(function()
if Utils.is_valid_container(self.result_container, true) and Config.behaviour.jump_result_buffer_on_finish 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)
Path.history.save(self.code.bufnr, self.chat_history)
end
if request and request ~= "" then
self:add_history_messages({
HistoryMessage:new({
role = "user",
content = request,
}, {
is_user_submission = true,
selected_filepaths = selected_filepaths,
selected_code = selected_code,
}),
})
end
self:get_generate_prompts_options(request, function(generate_prompts_options)
---@type AvanteLLMStreamOptions
---@diagnostic disable-next-line: assign-type-mismatch
local stream_options = vim.tbl_deep_extend("force", generate_prompts_options, {
on_start = on_start,
on_stop = on_stop,
on_tool_log = on_tool_log,
on_messages_add = on_messages_add,
on_state_change = on_state_change,
get_history_messages = function(opts) return self:get_history_messages_for_api(opts) end,
get_todos = function()
local history = Path.history.load(self.code.bufnr)
return history and history.todos or {}
end,
session_ctx = {},
update_tokens_usage = function(usage)
self.chat_history.tokens_usage = usage
self:save_history()
end,
get_tokens_usage = function() return self.chat_history.tokens_usage end,
})
---@param pending_compaction_history_messages avante.HistoryMessage[]
local function on_memory_summarize(pending_compaction_history_messages)
local history_memory = self.chat_history.memory
Llm.summarize_memory(
history_memory and history_memory.content,
pending_compaction_history_messages,
function(memory)
if memory then
self.chat_history.memory = memory
Path.history.save(self.code.bufnr, self.chat_history)
stream_options.memory = memory.content
end
stream_options.history_messages = self:get_history_messages_for_api()
Llm.stream(stream_options)
end
)
end
stream_options.on_memory_summarize = on_memory_summarize
on_state_change("generating")
Llm.stream(stream_options)
end)
end
local function get_position()
if self:get_layout() == "vertical" then return "bottom" end
return "right"
end
local function get_size()
if self:get_layout() == "vertical" then return {
height = Config.windows.input.height,
} end
local selected_code_container_height = self:get_selected_code_container_height()
return {
width = "40%",
height = math.max(1, api.nvim_win_get_height(self.result_container.winid) - selected_code_container_height),
}
end
self.input_container = Split({
enter = false,
relative = {
type = "win",
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(),
size = get_size(),
})
local function on_submit()
if not vim.g.avante_login then
Utils.warn("Sending message to fast!, API key is not yet set", { title = "Avante" })
return
end
if not Utils.is_valid_container(self.input_container) 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_container.bufnr, 0, -1, false, {})
api.nvim_win_set_cursor(self.input_container.winid, { 1, 0 })
handle_submit(request)
end
self.handle_submit = handle_submit
self.input_container:mount()
local function place_sign_at_first_line(bufnr)
local group = "avante_input_prompt_group"
fn.sign_unplace(group, { buffer = bufnr })
fn.sign_place(0, group, "AvanteInputPromptSign", bufnr, { lnum = 1 })
end
place_sign_at_first_line(self.input_container.bufnr)
if Utils.in_visual_mode() then
-- Exit visual mode
vim.cmd("noautocmd stopinsert")
end
self.input_container:map("n", Config.mappings.submit.normal, on_submit)
self.input_container:map("i", Config.mappings.submit.insert, on_submit)
self.input_container:map("n", Config.prompt_logger.next_prompt.normal, PromptLogger.on_log_retrieve(-1))
self.input_container:map("i", Config.prompt_logger.next_prompt.insert, PromptLogger.on_log_retrieve(-1))
self.input_container:map("n", Config.prompt_logger.prev_prompt.normal, PromptLogger.on_log_retrieve(1))
self.input_container:map("i", Config.prompt_logger.prev_prompt.insert, PromptLogger.on_log_retrieve(1))
if Config.mappings.sidebar.close_from_input ~= nil then
if Config.mappings.sidebar.close_from_input.normal ~= nil then
self.input_container:map("n", Config.mappings.sidebar.close_from_input.normal, function() self:shutdown() end)
end
if Config.mappings.sidebar.close_from_input.insert ~= nil then
self.input_container:map("i", Config.mappings.sidebar.close_from_input.insert, function() self:shutdown() end)
end
end
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_container.bufnr,
once = true,
desc = "Setup the completion of helpers in the input buffer",
callback = function() end,
})
api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "VimResized" }, {
group = self.augroup,
buffer = self.input_container.bufnr,
callback = function()
self:show_input_hint()
place_sign_at_first_line(self.input_container.bufnr)
end,
})
api.nvim_create_autocmd("QuitPre", {
group = self.augroup,
buffer = self.input_container.bufnr,
callback = function() self:close_input_hint() end,
})
api.nvim_create_autocmd("WinClosed", {
group = self.augroup,
callback = function(args)
local closed_winid = tonumber(args.match)
if closed_winid == self.input_container.winid then self:close_input_hint() end
end,
})
api.nvim_create_autocmd("BufEnter", {
group = self.augroup,
buffer = self.input_container.bufnr,
callback = function()
if Config.windows.ask.start_insert then vim.cmd("noautocmd startinsert!") end
end,
})
api.nvim_create_autocmd("BufLeave", {
group = self.augroup,
buffer = self.input_container.bufnr,
callback = function() vim.cmd("noautocmd stopinsert") end,
})
-- Show hint in insert mode
api.nvim_create_autocmd("ModeChanged", {
group = self.augroup,
pattern = "*:i",
callback = function()
local cur_buf = api.nvim_get_current_buf()
if self.input_container and cur_buf == self.input_container.bufnr then self:show_input_hint() end
end,
})
-- Close hint when exiting insert mode
api.nvim_create_autocmd("ModeChanged", {
group = self.augroup,
pattern = "i:*",
callback = function()
local cur_buf = api.nvim_get_current_buf()
if self.input_container and cur_buf == self.input_container.bufnr then self:show_input_hint() end
end,
})
api.nvim_create_autocmd("WinEnter", {
group = self.augroup,
callback = function()
local cur_win = api.nvim_get_current_win()
if self.input_container and cur_win == self.input_container.winid then
self:show_input_hint()
else
self:close_input_hint()
end
end,
})
api.nvim_create_autocmd("User", {
group = self.augroup,
pattern = "AvanteInputSubmitted",
callback = function(ev)
if ev.data and ev.data.request then handle_submit(ev.data.request) end
end,
})
-- Clear hint when leaving the window
self.input_container:on(event.BufLeave, function() self:close_input_hint() end, {})
self:refresh_winids()
end
---@param value string
function Sidebar:set_input_value(value)
if not self.input_container then return end
if not value then return end
api.nvim_buf_set_lines(self.input_container.bufnr, 0, -1, false, vim.split(value, "\n"))
end
---@return string
function Sidebar:get_input_value()
if not self.input_container then return "" end
local lines = api.nvim_buf_get_lines(self.input_container.bufnr, 0, -1, false)
return table.concat(lines, "\n")
end
function Sidebar:get_selected_code_container_height()
local selected_code_max_lines_count = 5
local selected_code_size = 0
if self.code.selection ~= nil then
local selected_code_lines = vim.split(self.code.selection.content, "\n")
local selected_code_lines_count = #selected_code_lines + 1
selected_code_size = math.min(selected_code_lines_count, selected_code_max_lines_count)
end
return selected_code_size
end
function Sidebar:get_todos_container_height()
local history = Path.history.load(self.code.bufnr)
if not history or not history.todos or #history.todos == 0 then return 0 end
return 3
end
function Sidebar:get_result_container_height()
local todos_container_height = self:get_todos_container_height()
local selected_code_container_height = self:get_selected_code_container_height()
local selected_files_container_height = self:get_selected_files_container_height()
if self:get_layout() == "horizontal" then return math.floor(Config.windows.height / 100 * vim.o.lines) end
return math.max(
1,
api.nvim_win_get_height(self.code.winid)
- selected_files_container_height
- selected_code_container_height
- todos_container_height
- 6
)
end
function Sidebar:get_result_container_width()
if self:get_layout() == "vertical" then return math.floor(Config.windows.width / 100 * vim.o.columns) end
return math.max(1, api.nvim_win_get_width(self.code.winid))
end
function Sidebar:adjust_result_container_layout()
local width = self:get_result_container_width()
local height = self:get_result_container_height()
api.nvim_win_set_width(self.result_container.winid, width)
api.nvim_win_set_height(self.result_container.winid, height)
end
---@param opts AskOptions
function Sidebar:render(opts)
self.ask_opts = opts
local function get_position()
return (opts and opts.win and opts.win.position) and opts.win.position or calculate_config_window_position()
end
self.result_container = Split({
enter = false,
relative = "editor",
position = get_position(),
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,
fillchars = Config.windows.fillchars,
}),
size = {
width = self:get_result_container_width(),
height = self:get_result_container_height(),
},
})
self.result_container:mount()
self.augroup = api.nvim_create_augroup("avante_sidebar_" .. self.id .. self.result_container.winid, { clear = true })
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_container:map("n", Config.mappings.sidebar.close, function() self:shutdown() end)
self:create_input_container()
self:create_selected_files_container()
self:update_content_with_history()
if self.code.bufnr and api.nvim_buf_is_valid(self.code.bufnr) then
-- reset states when buffer is closed
api.nvim_buf_attach(self.code.bufnr, false, {
on_detach = function(_, _)
vim.schedule(function()
local bufnr = api.nvim_win_get_buf(self.code.winid)
self.code.bufnr = bufnr
self:reload_chat_history()
end)
end,
})
end
self:create_selected_code_container()
self:create_todos_container()
self:on_mount(opts)
self:setup_colors()
return self
end
function Sidebar:get_selected_files_container_height()
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
return math.min(vim.o.lines - 2, #selected_filepaths_ + 1)
end
function Sidebar:adjust_selected_files_container_layout()
if not Utils.is_valid_container(self.selected_files_container, true) then return end
local win_height = self:get_selected_files_container_height()
api.nvim_win_set_height(self.selected_files_container.winid, win_height)
end
function Sidebar:adjust_selected_code_container_layout()
if not Utils.is_valid_container(self.selected_code_container, true) then return end
local win_height = self:get_selected_code_container_height()
api.nvim_win_set_height(self.selected_code_container.winid, win_height)
end
function Sidebar:adjust_todos_container_layout()
if not Utils.is_valid_container(self.todos_container, true) then return end
local win_height = self:get_todos_container_height()
api.nvim_win_set_height(self.todos_container.winid, win_height)
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 = "AvanteSelectedFiles",
}),
win_options = vim.tbl_deep_extend("force", base_win_options, {
fillchars = Config.windows.fillchars,
}),
position = "top",
size = {
height = 2,
},
})
self.selected_files_container:mount()
local function render()
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
if #selected_filepaths_ == 0 then
if Utils.is_valid_container(self.selected_files_container) then
self.selected_files_container:unmount()
self:refresh_winids()
end
return
end
if not Utils.is_valid_container(self.selected_files_container, true) then
self:create_selected_files_container()
self:refresh_winids()
if not Utils.is_valid_container(self.selected_files_container, true) then
Utils.warn("Failed to create or find selected files container window.")
return
end
end
local lines_to_set = {}
local highlights_to_apply = {}
local project_path = Utils.root.get()
for i, filepath in ipairs(selected_filepaths_) do
local icon, hl = Utils.file.get_file_icon(filepath)
local renderpath = PPath:new(filepath):normalize(project_path)
local formatted_line = string.format("%s %s", icon, renderpath)
table.insert(lines_to_set, formatted_line)
if hl and hl ~= "" then table.insert(highlights_to_apply, { line_nr = i, icon = icon, hl = hl }) end
end
local selected_files_count = #lines_to_set ---@type integer
local selected_files_buf = api.nvim_win_get_buf(self.selected_files_container.winid)
Utils.unlock_buf(selected_files_buf)
api.nvim_buf_clear_namespace(selected_files_buf, SELECTED_FILES_ICON_NAMESPACE, 0, -1)
api.nvim_buf_set_lines(selected_files_buf, 0, -1, true, lines_to_set)
for _, highlight_info in ipairs(highlights_to_apply) do
local line_idx = highlight_info.line_nr - 1
local icon_bytes = #highlight_info.icon
pcall(api.nvim_buf_set_extmark, selected_files_buf, SELECTED_FILES_ICON_NAMESPACE, line_idx, 0, {
end_col = icon_bytes,
hl_group = highlight_info.hl,
priority = PRIORITY,
})
end
Utils.lock_buf(selected_files_buf)
local win_height = self:get_selected_files_container_height()
api.nvim_win_set_height(self.selected_files_container.winid, win_height)
self:render_header(
self.selected_files_container.winid,
selected_files_buf,
string.format(
"%sSelected (%d file%s)",
Utils.icon(""),
selected_files_count,
selected_files_count > 1 and "s" or ""
),
Highlights.SUBTITLE,
Highlights.REVERSED_SUBTITLE
)
self:adjust_layout()
end
self.file_selector:on("update", render)
local function remove_file(line_number) self.file_selector:remove_selected_filepaths_with_index(line_number) 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("x", Config.mappings.sidebar.remove_file, function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
local start_line = math.min(vim.fn.line("v"), vim.fn.line("."))
local end_line = math.max(vim.fn.line("v"), vim.fn.line("."))
for _ = start_line, end_line do
remove_file(start_line)
end
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 }, function() self:show_selected_files_hint() end, {})
-- Clear hint when leaving the window
self.selected_files_container:on(event.BufLeave, function() self:close_selected_files_hint() end, {})
render()
end
function Sidebar:create_todos_container()
local history = Path.history.load(self.code.bufnr)
if not history or not history.todos or #history.todos == 0 then
if self.todos_container then self.todos_container:unmount() end
self.todos_container = nil
self:adjust_layout()
self:refresh_winids()
return
end
if not self.todos_container then
self.todos_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 = "AvanteTodos",
}),
win_options = vim.tbl_deep_extend("force", base_win_options, {
fillchars = Config.windows.fillchars,
}),
position = "top",
size = {
height = 3,
},
})
self.todos_container:mount()
end
local done_count = 0
local total_count = #history.todos
local focused_idx = 1
local todos_content_lines = {}
for idx, todo in ipairs(history.todos) do
local status_content = "[ ]"
if todo.status == "done" then
done_count = done_count + 1
status_content = "[x]"
end
if todo.status == "doing" then status_content = "[-]" end
local line = string.format("%s %d. %s", status_content, idx, todo.content)
if todo.status == "cancelled" then line = "~~" .. line .. "~~" end
if todo.status ~= "todo" then focused_idx = idx + 1 end
table.insert(todos_content_lines, line)
end
if focused_idx > #todos_content_lines then focused_idx = #todos_content_lines end
local todos_buf = api.nvim_win_get_buf(self.todos_container.winid)
Utils.unlock_buf(todos_buf)
api.nvim_buf_set_lines(todos_buf, 0, -1, false, todos_content_lines)
api.nvim_win_set_cursor(self.todos_container.winid, { focused_idx, 0 })
Utils.lock_buf(todos_buf)
self:render_header(
self.todos_container.winid,
todos_buf,
Utils.icon("") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")",
Highlights.SUBTITLE,
Highlights.REVERSED_SUBTITLE
)
self:adjust_layout()
self:refresh_winids()
end
function Sidebar:adjust_layout()
self:adjust_result_container_layout()
self:adjust_todos_container_layout()
self:adjust_selected_code_container_layout()
self:adjust_selected_files_container_layout()
end
return Sidebar