3333 lines
112 KiB
Lua
3333 lines
112 KiB
Lua
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
|