If one is using avante.nvim with a non streaming LLM API the API response will only have a single chunk. So that if we don't update the displayed_response in the sidebar component, the output of the LLM will be removed from the sidebar when we complete the interaction with the LLM. This commit makes sure that the displayed_response is updated for the first chunk as well as subsequent ones.
3222 lines
107 KiB
Lua
3222 lines
107 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 Highlights = require("avante.highlights")
|
|
local RepoMap = require("avante.repo_map")
|
|
local FileSelector = require("avante.file_selector")
|
|
local LLMTools = require("avante.llm_tools")
|
|
|
|
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 PRIORITY = vim.highlight.priorities.user
|
|
|
|
local RESP_SEPARATOR = "-------"
|
|
|
|
---@class avante.Sidebar
|
|
local Sidebar = {}
|
|
|
|
---@class avante.CodeState
|
|
---@field winid integer
|
|
---@field bufnr integer
|
|
---@field selection avante.SelectionResult | nil
|
|
|
|
---@class avante.Sidebar
|
|
---@field id integer
|
|
---@field augroup integer
|
|
---@field code avante.CodeState
|
|
---@field winids table<string, integer>
|
|
---@field result_container NuiSplit | nil
|
|
---@field selected_code_container NuiSplit | nil
|
|
---@field selected_files_container NuiSplit | nil
|
|
---@field input_container NuiSplit | nil
|
|
---@field file_selector FileSelector
|
|
---@field chat_history avante.ChatHistory | nil
|
|
|
|
---@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 },
|
|
winids = {
|
|
result_container = 0,
|
|
selected_files_container = 0,
|
|
selected_code_container = 0,
|
|
input_container = 0,
|
|
},
|
|
result_container = nil,
|
|
selected_code_container = nil,
|
|
selected_files_container = nil,
|
|
input_container = nil,
|
|
file_selector = FileSelector:new(id),
|
|
is_generating = false,
|
|
chat_history = nil,
|
|
}, { __index = self })
|
|
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.selected_code_container = nil
|
|
self.selected_files_container = nil
|
|
self.input_container = nil
|
|
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
|
|
|
|
vim.cmd("wincmd =")
|
|
return self
|
|
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
|
|
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
|
|
|
|
vim.cmd("wincmd =")
|
|
end
|
|
|
|
function Sidebar:shutdown()
|
|
Llm.cancel_inflight_request()
|
|
self:close()
|
|
vim.cmd("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 self.input_container and self.input_container.winid and api.nvim_win_is_valid(self.input_container.winid) then
|
|
api.nvim_set_current_win(self.input_container.winid)
|
|
end
|
|
end
|
|
|
|
function Sidebar:is_open()
|
|
return self.result_container
|
|
and self.result_container.bufnr
|
|
and api.nvim_buf_is_valid(self.result_container.bufnr)
|
|
and self.result_container.winid
|
|
and api.nvim_win_is_valid(self.result_container.winid)
|
|
end
|
|
|
|
function Sidebar:in_code_win() return self.code.winid == api.nvim_get_current_win() end
|
|
|
|
---@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 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 selected_files {path: string, content: string, file_type: string | nil}[]
|
|
---@param result_content string
|
|
---@param prev_filepath string
|
|
---@return AvanteReplacementResult
|
|
local function transform_result_content(selected_files, 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 i = 1
|
|
while true do
|
|
if i > #result_lines then break end
|
|
local line_content = result_lines[i]
|
|
if line_content:match("<[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>.+</[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>") then
|
|
local filepath = line_content:match("<[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>(.+)</[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]>")
|
|
if filepath then
|
|
current_filepath = filepath
|
|
table.insert(transformed_lines, string.format("Filepath: %s", filepath))
|
|
goto continue
|
|
end
|
|
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 start_line = 0
|
|
local end_line = 0
|
|
local match_filetype = nil
|
|
local filepath = current_filepath or prev_filepath or ""
|
|
---@type {path: string, content: string, file_type: string | nil} | nil
|
|
local the_matched_file = nil
|
|
for _, file in ipairs(selected_files) do
|
|
if Utils.is_same_file(file.path, filepath) then
|
|
the_matched_file = file
|
|
break
|
|
end
|
|
end
|
|
|
|
if not the_matched_file then
|
|
if not PPath:new(filepath):exists() then
|
|
the_matched_file = {
|
|
filepath = filepath,
|
|
content = "",
|
|
file_type = nil,
|
|
}
|
|
else
|
|
if not PPath:new(filepath):is_file() then
|
|
Utils.warn("Not a file: " .. filepath)
|
|
goto continue
|
|
end
|
|
local lines = Utils.read_file_from_buf_or_disk(filepath)
|
|
if lines == nil then
|
|
Utils.warn("Failed to read file: " .. filepath)
|
|
goto continue
|
|
end
|
|
local content = table.concat(lines, "\n")
|
|
the_matched_file = {
|
|
filepath = filepath,
|
|
content = content,
|
|
file_type = nil,
|
|
}
|
|
end
|
|
end
|
|
|
|
local file_content = vim.split(the_matched_file.content, "\n")
|
|
if start_line ~= 0 or end_line ~= 0 then break end
|
|
for j = 1, #file_content - (search_end - search_start) + 1 do
|
|
local match = true
|
|
for k = 0, search_end - search_start - 1 do
|
|
if
|
|
Utils.remove_indentation(file_content[j + k]) ~= Utils.remove_indentation(result_lines[search_start + k])
|
|
then
|
|
match = false
|
|
break
|
|
end
|
|
end
|
|
if match then
|
|
start_line = j
|
|
end_line = j + (search_end - search_start) - 1
|
|
match_filetype = the_matched_file.file_type
|
|
break
|
|
end
|
|
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
|
|
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
|
|
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
|
|
end
|
|
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"),
|
|
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
|
|
|
|
local spinner_chars = {
|
|
"⡀",
|
|
"⠄",
|
|
"⠂",
|
|
"⠁",
|
|
"⠈",
|
|
"⠐",
|
|
"⠠",
|
|
"⢀",
|
|
"⣀",
|
|
"⢄",
|
|
"⢂",
|
|
"⢁",
|
|
"⢈",
|
|
"⢐",
|
|
"⢠",
|
|
"⣠",
|
|
"⢤",
|
|
"⢢",
|
|
"⢡",
|
|
"⢨",
|
|
"⢰",
|
|
"⣰",
|
|
"⢴",
|
|
"⢲",
|
|
"⢱",
|
|
"⢸",
|
|
"⣸",
|
|
"⢼",
|
|
"⢺",
|
|
"⢹",
|
|
"⣹",
|
|
"⢽",
|
|
"⢻",
|
|
"⣻",
|
|
"⢿",
|
|
"⣿",
|
|
"⣶",
|
|
"⣤",
|
|
"⣀",
|
|
}
|
|
local spinner_index = 1
|
|
|
|
local function get_searching_hint()
|
|
spinner_index = (spinner_index % #spinner_chars) + 1
|
|
local spinner = spinner_chars[spinner_index]
|
|
return "\n" .. spinner .. " Searching..."
|
|
end
|
|
|
|
local thinking_spinner_chars = {
|
|
Utils.icon("🤯", "?"),
|
|
Utils.icon("🙄", "¿"),
|
|
}
|
|
local thinking_spinner_index = 1
|
|
|
|
local function get_thinking_spinner()
|
|
thinking_spinner_index = thinking_spinner_index + 1
|
|
if thinking_spinner_index > #thinking_spinner_chars then thinking_spinner_index = 1 end
|
|
local spinner = thinking_spinner_chars[thinking_spinner_index]
|
|
return "\n\n" .. spinner .. " Thinking..."
|
|
end
|
|
|
|
local function get_display_content_suffix(replacement)
|
|
if replacement.is_searching then return get_searching_hint() end
|
|
if replacement.is_thinking then return get_thinking_spinner() end
|
|
return ""
|
|
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
|
|
|
|
---@return string | nil filepath
|
|
---@return boolean skip_next_line
|
|
local function obtain_filepath_from_codeblock(lines, line_number)
|
|
local line = lines[line_number]
|
|
local filepath = line:match("^%s*```%w+:(.+)$")
|
|
if not filepath then
|
|
local next_line = lines[line_number + 1]
|
|
if next_line then
|
|
local filepath2 = next_line:match("[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]:%s*(.+)%s*")
|
|
if filepath2 then return filepath2, true end
|
|
local filepath3 = next_line:match("[Ff][Ii][Ll][Ee]:%s*(.+)%s*")
|
|
if filepath3 then return filepath3, true end
|
|
end
|
|
for i = line_number - 1, line_number - 2, -1 do
|
|
if i < 1 then break end
|
|
local line_ = lines[i]
|
|
local filepath4 = line_:match("[Ff][Ii][Ll][Ee][Pp][Aa][Tt][Hh]:%s*`?(.-)`?%s*$")
|
|
if filepath4 then return filepath4, false end
|
|
local filepath5 = line_:match("[Ff][Ii][Ll][Ee]:%s*`?(.-)`?%s*$")
|
|
if filepath5 then return filepath5, false end
|
|
end
|
|
end
|
|
return filepath, false
|
|
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
|
|
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_cursor_planning_code_snippets_map(response_content, current_filepath, current_filetype)
|
|
local snippets = {}
|
|
local lines = vim.split(response_content, "\n")
|
|
local cumulated_content = ""
|
|
|
|
-- use tree-sitter-markdown to parse all code blocks in response_content
|
|
local lang = "unknown"
|
|
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)
|
|
lang = vim.split(lang, ":")[1]
|
|
elseif node:type() == "code_fence_content" then
|
|
local start_line, _ = node:start()
|
|
local end_line, _ = node:end_()
|
|
local filepath, skip_next_line = obtain_filepath_from_codeblock(lines, start_line)
|
|
if filepath == nil or filepath == "" then
|
|
if lang == current_filetype then
|
|
filepath = current_filepath
|
|
else
|
|
Utils.warn(
|
|
string.format(
|
|
"Failed to parse filepath from code block, and current_filetype `%s` is not the same as the filetype `%s` of the current code block, so ignore this code block",
|
|
current_filetype,
|
|
lang
|
|
)
|
|
)
|
|
lang = "unknown"
|
|
goto continue
|
|
end
|
|
end
|
|
if skip_next_line then start_line = start_line + 1 end
|
|
local this_content = table.concat(vim.list_slice(lines, start_line + 1, end_line), "\n")
|
|
cumulated_content = cumulated_content .. "\n" .. this_content
|
|
table.insert(snippets, {
|
|
range = { 0, 0 },
|
|
content = cumulated_content,
|
|
lang = lang,
|
|
filepath = filepath,
|
|
start_line_in_response_buf = start_line,
|
|
end_line_in_response_buf = end_line + 1,
|
|
})
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
local snippets_map = {}
|
|
for _, snippet in ipairs(snippets) do
|
|
snippets_map[snippet.filepath] = snippets_map[snippet.filepath] or {}
|
|
table.insert(snippets_map[snippet.filepath], snippet)
|
|
end
|
|
|
|
return snippets_map
|
|
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
|
|
snippets_map[snippet.filepath] = snippets_map[snippet.filepath] or {}
|
|
table.insert(snippets_map[snippet.filepath], snippet)
|
|
end
|
|
|
|
return snippets_map
|
|
end
|
|
|
|
---@param snippets_map table<string, AvanteCodeSnippet[]>
|
|
---@return table<string, AvanteCodeSnippet[]>
|
|
local function ensure_snippets_no_overlap(snippets_map)
|
|
local new_snippets_map = {}
|
|
|
|
for filepath, snippets in pairs(snippets_map) do
|
|
table.sort(snippets, function(a, b) return a.range[1] < b.range[1] end)
|
|
|
|
local original_lines = {}
|
|
local file_exists = Utils.file.exists(filepath)
|
|
if file_exists then
|
|
local original_lines_ = Utils.read_file_from_buf_or_disk(filepath)
|
|
if original_lines_ then original_lines = original_lines_ end
|
|
end
|
|
|
|
local new_snippets = {}
|
|
local last_end_line = 0
|
|
for _, snippet in ipairs(snippets) do
|
|
if snippet.range[1] > last_end_line then
|
|
table.insert(new_snippets, snippet)
|
|
last_end_line = snippet.range[2]
|
|
elseif not file_exists and #snippets <= 1 then
|
|
-- if the file doesn't exist, and we only have 1 snippet, then we don't have to check for overlaps.
|
|
table.insert(new_snippets, snippet)
|
|
last_end_line = snippet.range[2]
|
|
else
|
|
local snippet_lines = vim.split(snippet.content, "\n")
|
|
-- Trim the overlapping part
|
|
local new_start_line = nil
|
|
for i = snippet.range[1], math.min(snippet.range[2], last_end_line) do
|
|
if
|
|
Utils.remove_indentation(original_lines[i])
|
|
== Utils.remove_indentation(snippet_lines[i - snippet.range[1] + 1])
|
|
then
|
|
new_start_line = i + 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
if new_start_line ~= nil then
|
|
snippet.content = table.concat(vim.list_slice(snippet_lines, new_start_line - snippet.range[1] + 1), "\n")
|
|
snippet.range[1] = new_start_line
|
|
table.insert(new_snippets, snippet)
|
|
last_end_line = snippet.range[2]
|
|
else
|
|
Utils.error("Failed to ensure snippets no overlap", { once = true, title = "Avante" })
|
|
end
|
|
end
|
|
end
|
|
new_snippets_map[filepath] = new_snippets
|
|
end
|
|
|
|
return new_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)
|
|
if start_line > end_line then
|
|
start_line = start_line + 1
|
|
end_line = end_line + 1
|
|
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")
|
|
|
|
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, current_filepath, current_filetype)
|
|
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" then
|
|
local end_line, _ = node:start()
|
|
if Config.behaviour.enable_cursor_planning_mode then
|
|
local filepath = obtain_filepath_from_codeblock(lines, start_line)
|
|
if not filepath and lang == current_filetype then filepath = current_filepath end
|
|
if filepath then valid = true end
|
|
else
|
|
if lines[start_line - 1]:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)") then
|
|
valid = true
|
|
end
|
|
end
|
|
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 = { start_line + start_a - 1, start_line + start_a + count_a - 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 self.input_container and self.input_container.bufnr and api.nvim_buf_is_valid(self.input_container.bufnr) then
|
|
api.nvim_buf_set_lines(self.input_container.bufnr, 0, -1, false, vim.split(block.content, "\n"))
|
|
api.nvim_set_current_win(self.input_container.winid)
|
|
api.nvim_win_set_cursor(self.input_container.winid, { 1, 0 })
|
|
end
|
|
end
|
|
|
|
---@param current_cursor boolean
|
|
function Sidebar:apply(current_cursor)
|
|
local buf_path = api.nvim_buf_get_name(self.code.bufnr)
|
|
local current_filepath = Utils.file.is_in_cwd(buf_path) and Utils.relative_path(buf_path) or buf_path
|
|
local current_filetype = Utils.get_filetype(current_filepath)
|
|
|
|
local response, response_start_line = self:get_content_between_separators()
|
|
local all_snippets_map = Config.behaviour.enable_cursor_planning_mode
|
|
and extract_cursor_planning_code_snippets_map(response, current_filepath, current_filetype)
|
|
or extract_code_snippets_map(response)
|
|
if not Config.behaviour.enable_cursor_planning_mode then
|
|
all_snippets_map = ensure_snippets_no_overlap(all_snippets_map)
|
|
end
|
|
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
|
|
|
|
if Config.behaviour.enable_cursor_planning_mode then
|
|
for filepath, snippets in pairs(selected_snippets_map) do
|
|
local original_code_lines = Utils.read_file_from_buf_or_disk(filepath)
|
|
if not original_code_lines then
|
|
Utils.error("Failed to read file: " .. filepath)
|
|
return
|
|
end
|
|
local formated_snippets = vim.iter(snippets):map(function(snippet) return snippet.content end):totable()
|
|
local original_code = table.concat(original_code_lines, "\n")
|
|
local resp_content = ""
|
|
local filetype = Utils.get_filetype(filepath)
|
|
local cursor_applying_provider_name = Config.cursor_applying_provider or Config.provider
|
|
Utils.debug(string.format("Use %s for cursor applying", cursor_applying_provider_name))
|
|
local cursor_applying_provider = Provider[cursor_applying_provider_name]
|
|
if not cursor_applying_provider then
|
|
Utils.error("Failed to find cursor_applying_provider provider: " .. cursor_applying_provider_name, {
|
|
once = true,
|
|
title = "Avante",
|
|
})
|
|
end
|
|
if self.code.winid ~= nil and api.nvim_win_is_valid(self.code.winid) then
|
|
api.nvim_set_current_win(self.code.winid)
|
|
end
|
|
local bufnr = Utils.get_or_create_buffer_with_filepath(filepath)
|
|
local path_ = PPath:new(filepath)
|
|
path_:parent():mkdir({ parents = true, exists_ok = true })
|
|
|
|
local ns_id = api.nvim_create_namespace("avante_live_diff")
|
|
|
|
local function clear_highlights() api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) end
|
|
|
|
-- Create loading indicator float window
|
|
local loading_buf = nil
|
|
local loading_win = nil
|
|
local spinner_frames = { "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" }
|
|
local spinner_idx = 1
|
|
local loading_timer = nil
|
|
|
|
local function update_loading_indicator()
|
|
if not loading_win or not loading_buf or not api.nvim_win_is_valid(loading_win) then return end
|
|
spinner_idx = (spinner_idx % #spinner_frames) + 1
|
|
local text = spinner_frames[spinner_idx] .. " Applying changes..."
|
|
api.nvim_buf_set_lines(loading_buf, 0, -1, false, { text })
|
|
end
|
|
|
|
local function create_loading_window()
|
|
local winid = self.input_container.winid
|
|
local win_height = api.nvim_win_get_height(winid)
|
|
local win_width = api.nvim_win_get_width(winid)
|
|
|
|
-- Calculate position for center of window
|
|
local width = 30
|
|
local height = 1
|
|
local row = win_height - height - 1
|
|
local col = win_width - width
|
|
|
|
local opts = {
|
|
relative = "win",
|
|
win = winid,
|
|
width = width,
|
|
height = height,
|
|
row = row,
|
|
col = col,
|
|
anchor = "NW",
|
|
style = "minimal",
|
|
border = "none",
|
|
focusable = false,
|
|
zindex = 101,
|
|
}
|
|
|
|
loading_buf = api.nvim_create_buf(false, true)
|
|
loading_win = api.nvim_open_win(loading_buf, false, opts)
|
|
|
|
-- Start timer to update spinner
|
|
loading_timer = vim.loop.new_timer()
|
|
if loading_timer then loading_timer:start(0, 100, vim.schedule_wrap(update_loading_indicator)) end
|
|
end
|
|
|
|
local function close_loading_window()
|
|
if loading_timer then
|
|
loading_timer:stop()
|
|
loading_timer:close()
|
|
loading_timer = nil
|
|
end
|
|
if loading_win and api.nvim_win_is_valid(loading_win) then
|
|
api.nvim_win_close(loading_win, true)
|
|
loading_win = nil
|
|
end
|
|
|
|
if loading_buf then
|
|
api.nvim_buf_delete(loading_buf, { force = true })
|
|
loading_buf = nil
|
|
end
|
|
end
|
|
|
|
clear_highlights()
|
|
create_loading_window()
|
|
|
|
local last_processed_line = 0
|
|
local last_orig_diff_end_line = 1
|
|
local last_resp_diff_end_line = 1
|
|
local cleaned = false
|
|
local prev_patch = {}
|
|
|
|
local function get_stable_patch(patch)
|
|
local new_patch = {}
|
|
for _, hunk in ipairs(patch) do
|
|
local start_a, count_a, start_b, count_b = unpack(hunk)
|
|
start_a = start_a + last_orig_diff_end_line - 1
|
|
start_b = start_b + last_resp_diff_end_line - 1
|
|
local has = vim.iter(prev_patch):find(function(hunk_)
|
|
local start_a_, count_a_, start_b_, count_b_ = unpack(hunk_)
|
|
return start_a == start_a_ and start_b == start_b_ and count_a == count_a_ and count_b == count_b_
|
|
end)
|
|
if has ~= nil then table.insert(new_patch, hunk) end
|
|
end
|
|
return new_patch
|
|
end
|
|
|
|
local extmark_id_map = {}
|
|
local virt_lines_map = {}
|
|
|
|
Llm.stream({
|
|
ask = true,
|
|
provider = cursor_applying_provider,
|
|
code_lang = filetype,
|
|
mode = "cursor-applying",
|
|
original_code = original_code,
|
|
update_snippets = formated_snippets,
|
|
on_start = function(_) end,
|
|
on_chunk = function(chunk)
|
|
if not chunk then return end
|
|
|
|
resp_content = resp_content .. chunk
|
|
|
|
if not cleaned then
|
|
resp_content = resp_content:gsub("<updated%-code>\n*", ""):gsub("</updated%-code>\n*", "")
|
|
resp_content = resp_content:gsub(".*```%w+\n", ""):gsub("\n```\n.*", "")
|
|
end
|
|
|
|
local resp_lines = vim.split(resp_content, "\n")
|
|
|
|
local complete_lines_count = #resp_lines - 1
|
|
if complete_lines_count > 2 then cleaned = true end
|
|
|
|
if complete_lines_count <= last_processed_line then return end
|
|
|
|
local original_lines_to_process =
|
|
vim.list_slice(original_code_lines, last_orig_diff_end_line, complete_lines_count)
|
|
local resp_lines_to_process = vim.list_slice(resp_lines, last_resp_diff_end_line, complete_lines_count)
|
|
|
|
local resp_lines_content = table.concat(resp_lines_to_process, "\n")
|
|
local original_lines_content = table.concat(original_lines_to_process, "\n")
|
|
|
|
---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
|
|
local patch = vim.diff(original_lines_content, resp_lines_content, { ---@type integer[][]
|
|
algorithm = "histogram",
|
|
result_type = "indices",
|
|
ctxlen = vim.o.scrolloff,
|
|
})
|
|
|
|
local stable_patch = get_stable_patch(patch)
|
|
|
|
for _, hunk in ipairs(stable_patch) do
|
|
local start_a, count_a, start_b, count_b = unpack(hunk)
|
|
|
|
start_a = last_orig_diff_end_line + start_a - 1
|
|
|
|
if count_a > 0 then
|
|
api.nvim_buf_set_extmark(bufnr, ns_id, start_a - 1, 0, {
|
|
hl_group = Highlights.TO_BE_DELETED_WITHOUT_STRIKETHROUGH,
|
|
hl_eol = true,
|
|
hl_mode = "combine",
|
|
end_row = start_a + count_a - 1,
|
|
})
|
|
end
|
|
|
|
if count_b == 0 then goto continue end
|
|
|
|
local new_lines = vim.list_slice(resp_lines_to_process, start_b, start_b + count_b - 1)
|
|
local max_col = vim.o.columns
|
|
local virt_lines = vim
|
|
.iter(new_lines)
|
|
:map(function(line)
|
|
--- append spaces to the end of the line
|
|
local line_ = line .. string.rep(" ", max_col - #line)
|
|
return { { line_, Highlights.INCOMING } }
|
|
end)
|
|
:totable()
|
|
local extmark_line
|
|
if count_a > 0 then
|
|
extmark_line = math.max(0, start_a + count_a - 2)
|
|
else
|
|
extmark_line = math.max(0, start_a + count_a - 1)
|
|
end
|
|
local old_extmark_id = extmark_id_map[extmark_line]
|
|
if old_extmark_id ~= nil then
|
|
local old_virt_lines = virt_lines_map[old_extmark_id] or {}
|
|
virt_lines = vim.list_extend(old_virt_lines, virt_lines)
|
|
api.nvim_buf_del_extmark(bufnr, ns_id, old_extmark_id)
|
|
end
|
|
local extmark_id = api.nvim_buf_set_extmark(bufnr, ns_id, extmark_line, 0, {
|
|
virt_lines = virt_lines,
|
|
hl_eol = true,
|
|
hl_mode = "combine",
|
|
})
|
|
extmark_id_map[extmark_line] = extmark_id
|
|
virt_lines_map[extmark_id] = virt_lines
|
|
::continue::
|
|
end
|
|
|
|
prev_patch = vim
|
|
.iter(patch)
|
|
:map(function(hunk)
|
|
local start_a, count_a, start_b, count_b = unpack(hunk)
|
|
return { last_orig_diff_end_line + start_a - 1, count_a, last_resp_diff_end_line + start_b - 1, count_b }
|
|
end)
|
|
:totable()
|
|
|
|
if #stable_patch > 0 then
|
|
local start_a, count_a, start_b, count_b = unpack(stable_patch[#stable_patch])
|
|
last_orig_diff_end_line = last_orig_diff_end_line + start_a + math.max(count_a, 1) - 1
|
|
last_resp_diff_end_line = last_resp_diff_end_line + start_b + math.max(count_b, 1) - 1
|
|
end
|
|
|
|
if #patch == 0 then
|
|
last_orig_diff_end_line = complete_lines_count + 1
|
|
last_resp_diff_end_line = complete_lines_count + 1
|
|
end
|
|
|
|
last_processed_line = complete_lines_count
|
|
|
|
local winid = Utils.get_winid(bufnr)
|
|
|
|
if winid == nil then return end
|
|
|
|
--- goto window winid
|
|
api.nvim_set_current_win(winid)
|
|
--- goto the last line
|
|
pcall(function() api.nvim_win_set_cursor(winid, { complete_lines_count, 0 }) end)
|
|
vim.cmd("normal! zz")
|
|
end,
|
|
on_stop = function(stop_opts)
|
|
clear_highlights()
|
|
close_loading_window()
|
|
|
|
if stop_opts.error ~= nil then
|
|
Utils.error(string.format("applying failed: %s", vim.inspect(stop_opts.error)))
|
|
return
|
|
end
|
|
|
|
resp_content = resp_content:gsub("<updated%-code>\n*", ""):gsub("</updated%-code>\n*", "")
|
|
|
|
resp_content = resp_content:gsub(".*```%w+\n", ""):gsub("\n```\n.*", ""):gsub("\n```$", "")
|
|
|
|
local resp_lines = vim.split(resp_content, "\n")
|
|
|
|
if #resp_lines > 0 and resp_lines[#resp_lines] == "" then
|
|
resp_lines = vim.list_slice(resp_lines, 0, #resp_lines - 1)
|
|
resp_content = table.concat(resp_lines, "\n")
|
|
end
|
|
|
|
if require("avante.config").debug then
|
|
local resp_content_file = fn.tempname() .. ".txt"
|
|
fn.writefile(vim.split(resp_content, "\n"), resp_content_file)
|
|
Utils.debug("cursor applying response content written to: " .. resp_content_file)
|
|
end
|
|
|
|
if resp_content == original_code then return end
|
|
|
|
---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
|
|
local patch = vim.diff(original_code, resp_content, { ---@type integer[][]
|
|
algorithm = "histogram",
|
|
result_type = "indices",
|
|
ctxlen = vim.o.scrolloff,
|
|
})
|
|
|
|
local new_lines = {}
|
|
local prev_start_a = 1
|
|
for _, hunk in ipairs(patch) do
|
|
local start_a, count_a, start_b, count_b = unpack(hunk)
|
|
if count_a > 0 then
|
|
vim.list_extend(new_lines, vim.list_slice(original_code_lines, prev_start_a, start_a - 1))
|
|
else
|
|
vim.list_extend(new_lines, vim.list_slice(original_code_lines, prev_start_a, start_a))
|
|
end
|
|
prev_start_a = start_a + count_a
|
|
if count_a == 0 then prev_start_a = prev_start_a + 1 end
|
|
table.insert(new_lines, "<<<<<<< HEAD")
|
|
if count_a > 0 then
|
|
vim.list_extend(new_lines, vim.list_slice(original_code_lines, start_a, start_a + count_a - 1))
|
|
end
|
|
table.insert(new_lines, "=======")
|
|
if count_b > 0 then
|
|
vim.list_extend(new_lines, vim.list_slice(resp_lines, start_b, start_b + count_b - 1))
|
|
end
|
|
table.insert(new_lines, ">>>>>>> Snippet")
|
|
end
|
|
|
|
local remaining_lines = vim.list_slice(original_code_lines, prev_start_a, #original_code_lines)
|
|
new_lines = vim.list_extend(new_lines, remaining_lines)
|
|
|
|
api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
|
|
|
|
local function process(winid)
|
|
api.nvim_set_current_win(winid)
|
|
api.nvim_feedkeys(api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
|
|
Diff.add_visited_buffer(bufnr)
|
|
Diff.process(bufnr)
|
|
api.nvim_win_set_cursor(winid, { 1, 0 })
|
|
vim.defer_fn(function()
|
|
Diff.find_next(Config.windows.ask.focus_on_apply)
|
|
vim.cmd("normal! zz")
|
|
end, 100)
|
|
end
|
|
|
|
local winid = Utils.get_winid(bufnr)
|
|
if winid then
|
|
process(winid)
|
|
else
|
|
api.nvim_create_autocmd("BufWinEnter", {
|
|
buffer = bufnr,
|
|
once = true,
|
|
callback = function()
|
|
local winid_ = Utils.get_winid(bufnr)
|
|
if winid_ then process(winid_) end
|
|
end,
|
|
})
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
return
|
|
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.get_or_create_buffer_with_filepath(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)
|
|
api.nvim_feedkeys(api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
|
|
Diff.add_visited_buffer(bufnr)
|
|
Diff.process(bufnr)
|
|
api.nvim_win_set_cursor(winid, { 1, 0 })
|
|
vim.defer_fn(function()
|
|
Diff.find_next(Config.windows.ask.focus_on_apply)
|
|
vim.cmd("normal! zz")
|
|
end, 100)
|
|
end
|
|
local winid = Utils.get_winid(bufnr)
|
|
if winid then
|
|
process(winid)
|
|
else
|
|
api.nvim_create_autocmd("BufWinEnter", {
|
|
buffer = bufnr,
|
|
once = true,
|
|
callback = function()
|
|
local winid_ = Utils.get_winid(bufnr)
|
|
if winid_ then process(winid_) end
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
end, 10)
|
|
end
|
|
|
|
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,
|
|
winhl = "",
|
|
linebreak = true,
|
|
breakindent = true,
|
|
wrap = false,
|
|
cursorline = false,
|
|
fillchars = "eob: ",
|
|
winhighlight = "CursorLine:Normal,CursorColumn: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
|
|
|
|
if not Config.windows.sidebar_header.enabled then return end
|
|
|
|
if not Config.windows.sidebar_header.rounded then header_text = " " .. header_text .. " " end
|
|
|
|
local winbar_text = "%#Normal#"
|
|
|
|
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 .. "#" .. "" .. "%#" .. 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 .. "#" end
|
|
winbar_text = winbar_text .. "%#Normal#"
|
|
if Config.windows.sidebar_header.align == "center" then winbar_text = winbar_text .. "%=" end
|
|
api.nvim_set_option_value("winbar", winbar_text, { win = winid })
|
|
end
|
|
|
|
function Sidebar:render_result()
|
|
if
|
|
not self.result_container
|
|
or not self.result_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.result_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
local header_text = 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 self.input_container
|
|
or not self.input_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.input_container.bufnr)
|
|
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 self.selected_code_container
|
|
or not self.selected_code_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.selected_code_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
|
|
local selected_code_lines_count = 0
|
|
local selected_code_max_lines_count = 12
|
|
|
|
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 self.result_container and self.result_container.bufnr and api.nvim_buf_is_valid(self.result_container.bufnr) then
|
|
pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_all, { buffer = self.result_container.bufnr })
|
|
pcall(vim.keymap.del, "n", Config.mappings.jump.next, { buffer = self.result_container.bufnr })
|
|
pcall(vim.keymap.del, "n", Config.mappings.jump.prev, { buffer = self.result_container.bufnr })
|
|
end
|
|
end
|
|
|
|
---@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" }, {
|
|
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,
|
|
})
|
|
|
|
local buf_path = api.nvim_buf_get_name(self.code.bufnr)
|
|
local current_filepath = Utils.file.is_in_cwd(buf_path) and Utils.relative_path(buf_path) or buf_path
|
|
local current_filetype = Utils.get_filetype(current_filepath)
|
|
|
|
api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, {
|
|
buffer = self.result_container.bufnr,
|
|
callback = function(ev)
|
|
codeblocks = parse_codeblocks(ev.buf, current_filepath, current_filetype)
|
|
self:bind_sidebar_keys(codeblocks)
|
|
end,
|
|
})
|
|
|
|
api.nvim_create_autocmd("User", {
|
|
pattern = VIEW_BUFFER_UPDATED_PATTERN,
|
|
callback = function()
|
|
if
|
|
not self.result_container
|
|
or not self.result_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.result_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
codeblocks = parse_codeblocks(self.result_container.bufnr, current_filepath, current_filetype)
|
|
self:bind_sidebar_keys(codeblocks)
|
|
end,
|
|
})
|
|
|
|
api.nvim_create_autocmd("BufLeave", {
|
|
buffer = self.result_container.bufnr,
|
|
callback = function() self:unbind_sidebar_keys() end,
|
|
})
|
|
|
|
self:render_result()
|
|
self:render_input(opts.ask)
|
|
self:render_selected_code()
|
|
|
|
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
|
|
|
|
if self.selected_code_container ~= nil then
|
|
local selected_code_buf = self.selected_code_container.bufnr
|
|
if selected_code_buf ~= nil then
|
|
if self.code.selection ~= nil then
|
|
Utils.unlock_buf(selected_code_buf)
|
|
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
|
|
api.nvim_set_option_value("filetype", filetype, { buf = selected_code_buf })
|
|
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
|
|
self.input_container
|
|
and self.input_container.winid
|
|
and api.nvim_win_is_valid(self.input_container.winid)
|
|
then
|
|
api.nvim_set_current_win(self.input_container.winid)
|
|
vim.defer_fn(function()
|
|
if Config.windows.ask.start_insert then
|
|
Utils.debug("starting insert")
|
|
vim.cmd("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 not self:is_focused_on(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.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()
|
|
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
|
|
|
|
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_cwd(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()
|
|
self.file_selector:add_selected_file(filepath)
|
|
|
|
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_focused_on(winid)
|
|
for _, stored_winid in pairs(self.winids) do
|
|
if stored_winid == winid then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function delete_last_n_chars(bufnr, n)
|
|
bufnr = bufnr or api.nvim_get_current_buf()
|
|
|
|
local line_count = api.nvim_buf_line_count(bufnr)
|
|
|
|
while n > 0 and line_count > 0 do
|
|
local last_line = api.nvim_buf_get_lines(bufnr, line_count - 1, line_count, false)[1]
|
|
|
|
local total_chars_in_line = #last_line + 1
|
|
|
|
if total_chars_in_line > n then
|
|
local chars_to_keep = total_chars_in_line - n - 1 - 1
|
|
local new_last_line = last_line:sub(1, chars_to_keep)
|
|
if new_last_line == "" then
|
|
api.nvim_buf_set_lines(bufnr, line_count - 1, line_count, false, {})
|
|
line_count = line_count - 1
|
|
else
|
|
api.nvim_buf_set_lines(bufnr, line_count - 1, line_count, false, { new_last_line })
|
|
end
|
|
n = 0
|
|
else
|
|
n = n - total_chars_in_line
|
|
api.nvim_buf_set_lines(bufnr, line_count - 1, line_count, false, {})
|
|
line_count = line_count - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param content string concatenated content of the buffer
|
|
---@param opts? {focus?: boolean, scroll?: boolean, backspace?: integer, ignore_history?: boolean, callback?: fun(): nil} whether to focus the result view
|
|
function Sidebar:update_content(content, opts)
|
|
if not self.result_container or not self.result_container.bufnr then return end
|
|
opts = vim.tbl_deep_extend("force", { focus = false, scroll = true, stream = false, callback = nil }, opts or {})
|
|
if not opts.ignore_history then
|
|
local chat_history = Path.history.load(self.code.bufnr)
|
|
content = self:render_history_content(chat_history) .. "-------\n\n" .. content
|
|
end
|
|
if opts.stream then
|
|
local function scroll_to_bottom()
|
|
local last_line = api.nvim_buf_line_count(self.result_container.bufnr)
|
|
|
|
local current_lines = Utils.get_buf_lines(last_line - 1, last_line, self.result_container.bufnr)
|
|
|
|
if #current_lines > 0 then
|
|
local last_line_content = current_lines[1]
|
|
local last_col = #last_line_content
|
|
xpcall(
|
|
function() api.nvim_win_set_cursor(self.result_container.winid, { last_line, last_col }) end,
|
|
function(err) return err end
|
|
)
|
|
end
|
|
end
|
|
|
|
vim.schedule(function()
|
|
if
|
|
not self.result_container
|
|
or not self.result_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.result_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
Utils.unlock_buf(self.result_container.bufnr)
|
|
if opts.backspace ~= nil and opts.backspace > 0 then
|
|
delete_last_n_chars(self.result_container.bufnr, opts.backspace)
|
|
end
|
|
scroll_to_bottom()
|
|
local lines = vim.split(content, "\n")
|
|
api.nvim_buf_call(self.result_container.bufnr, function() api.nvim_put(lines, "c", true, true) end)
|
|
Utils.lock_buf(self.result_container.bufnr)
|
|
api.nvim_set_option_value("filetype", "Avante", { buf = self.result_container.bufnr })
|
|
if opts.scroll then scroll_to_bottom() end
|
|
if opts.callback ~= nil then opts.callback() end
|
|
end)
|
|
else
|
|
vim.defer_fn(function()
|
|
if
|
|
not self.result_container
|
|
or not self.result_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.result_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
local lines = vim.split(content, "\n")
|
|
Utils.unlock_buf(self.result_container.bufnr)
|
|
Utils.update_buffer_content(self.result_container.bufnr, lines)
|
|
Utils.lock_buf(self.result_container.bufnr)
|
|
api.nvim_set_option_value("filetype", "Avante", { buf = self.result_container.bufnr })
|
|
if opts.focus and not self:is_focused_on_result() then
|
|
--- set cursor to bottom of result view
|
|
xpcall(function() api.nvim_set_current_win(self.result_container.winid) end, function(err) return err end)
|
|
end
|
|
|
|
if opts.scroll then Utils.buf_scroll_to_end(self.result_container.bufnr) end
|
|
|
|
if opts.callback ~= nil then opts.callback() end
|
|
end, 0)
|
|
end
|
|
return self
|
|
end
|
|
|
|
-- Function to get current timestamp
|
|
local function get_timestamp() return os.date("%Y-%m-%d %H:%M:%S") 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`") .. "\n\n"
|
|
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
|
|
|
|
return position
|
|
end
|
|
|
|
function Sidebar:get_layout()
|
|
return vim.tbl_contains({ "left", "right" }, calculate_config_window_position()) and "vertical" or "horizontal"
|
|
end
|
|
|
|
---@param history avante.ChatHistory
|
|
---@return string
|
|
function Sidebar:render_history_content(history)
|
|
local content = ""
|
|
for idx, entry in ipairs(history.entries) do
|
|
if entry.visible == false then goto continue end
|
|
if entry.reset_memory then
|
|
content = content .. "***MEMORY RESET***\n\n"
|
|
if idx < #history.entries then content = content .. "-------\n\n" end
|
|
goto continue
|
|
end
|
|
local selected_filepaths = entry.selected_filepaths
|
|
if not selected_filepaths and entry.selected_file ~= nil then
|
|
selected_filepaths = { entry.selected_file.filepath }
|
|
end
|
|
local prefix = render_chat_record_prefix(
|
|
entry.timestamp,
|
|
entry.provider,
|
|
entry.model,
|
|
entry.request or "",
|
|
selected_filepaths or {},
|
|
entry.selected_code
|
|
)
|
|
content = content .. prefix
|
|
content = content .. entry.response .. "\n\n"
|
|
if idx < #history.entries then content = content .. "-------\n\n" end
|
|
::continue::
|
|
end
|
|
return content
|
|
end
|
|
|
|
function Sidebar:update_content_with_history(history)
|
|
local content = self:render_history_content(history)
|
|
self:update_content(content, { ignore_history = true })
|
|
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)
|
|
local chat_history = Path.history.load(self.code.bufnr)
|
|
if next(chat_history) ~= nil then
|
|
chat_history.entries = {}
|
|
Path.history.save(self.code.bufnr, chat_history)
|
|
self:update_content(
|
|
"Chat history cleared",
|
|
{ ignore_history = true, 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:new_chat(args, cb)
|
|
Path.history.new(self.code.bufnr)
|
|
self:reload_chat_history()
|
|
self:update_content(
|
|
"New chat",
|
|
{ ignore_history = true, focus = false, scroll = false, callback = function() self:focus_input() end }
|
|
)
|
|
if cb then cb(args) end
|
|
end
|
|
|
|
---@param message AvanteLLMMessage
|
|
---@param options {visible?: boolean}
|
|
function Sidebar:add_chat_history(message, options)
|
|
local timestamp = get_timestamp()
|
|
self:reload_chat_history()
|
|
table.insert(self.chat_history.entries, {
|
|
timestamp = timestamp,
|
|
provider = Config.provider,
|
|
model = Config.get_provider_config(Config.provider).model,
|
|
request = message.role == "user" and message.content or "",
|
|
response = message.role == "assistant" and message.content or "",
|
|
original_response = "",
|
|
selected_filepaths = nil,
|
|
selected_code = nil,
|
|
reset_memory = false,
|
|
visible = options.visible,
|
|
})
|
|
Path.history.save(self.code.bufnr, self.chat_history)
|
|
end
|
|
|
|
function Sidebar:reset_memory(args, cb)
|
|
local chat_history = Path.history.load(self.code.bufnr)
|
|
if next(chat_history) ~= nil then
|
|
table.insert(chat_history, {
|
|
timestamp = get_timestamp(),
|
|
provider = Config.provider,
|
|
model = Config.get_provider_config(Config.provider).model,
|
|
request = "",
|
|
response = "",
|
|
original_response = "",
|
|
selected_file = nil,
|
|
selected_code = nil,
|
|
reset_memory = true,
|
|
})
|
|
Path.history.save(self.code.bufnr, chat_history)
|
|
self:reload_chat_history()
|
|
local history_content = self:render_history_content(chat_history)
|
|
self:update_content(history_content, {
|
|
focus = false,
|
|
scroll = true,
|
|
callback = function() self:focus_input() end,
|
|
})
|
|
if cb then cb(args) end
|
|
else
|
|
self:reload_chat_history()
|
|
self:update_content(
|
|
"Chat history is already empty",
|
|
{ focus = false, scroll = false, callback = function() self:focus_input() end }
|
|
)
|
|
end
|
|
end
|
|
|
|
---@alias AvanteSlashCommandType "clear" | "help" | "lines" | "reset" | "commit" | "new"
|
|
---@alias AvanteSlashCommandCallback fun(args: string, cb?: fun(args: string): nil): nil
|
|
---@alias AvanteSlashCommand {description: string, command: AvanteSlashCommandType, details: string, shorthelp?: string, callback?: AvanteSlashCommandCallback}
|
|
---@return AvanteSlashCommand[]
|
|
function Sidebar:get_commands()
|
|
---@param items_ {command: string, description: string, shorthelp?: string}[]
|
|
---@return string
|
|
local function get_help_text(items_)
|
|
local help_text = ""
|
|
for _, item in ipairs(items_) do
|
|
help_text = help_text .. "- " .. item.command .. ": " .. (item.shorthelp or item.description) .. "\n"
|
|
end
|
|
return help_text
|
|
end
|
|
|
|
---@type AvanteSlashCommand[]
|
|
local items = {
|
|
{ description = "Show help message", command = "help" },
|
|
{ description = "Clear chat history", command = "clear" },
|
|
{ description = "Reset memory", command = "reset" },
|
|
{ description = "New chat", command = "new" },
|
|
{
|
|
shorthelp = "Ask a question about specific lines",
|
|
description = "/lines <start>-<end> <question>",
|
|
command = "lines",
|
|
},
|
|
{ description = "Commit the changes", command = "commit" },
|
|
}
|
|
|
|
---@type {[AvanteSlashCommandType]: AvanteSlashCommandCallback}
|
|
local cbs = {
|
|
help = function(args, cb)
|
|
local help_text = get_help_text(items)
|
|
self:update_content(help_text, { focus = false, scroll = false })
|
|
if cb then cb(args) end
|
|
end,
|
|
clear = function(args, cb) self:clear_history(args, cb) end,
|
|
reset = function(args, cb) self:reset_memory(args, cb) end,
|
|
new = function(args, cb) self:new_chat(args, cb) end,
|
|
lines = function(args, cb)
|
|
if cb then cb(args) end
|
|
end,
|
|
commit = function(_, cb)
|
|
local question = "Please commit the changes"
|
|
if cb then cb(question) end
|
|
end,
|
|
}
|
|
|
|
return vim
|
|
.iter(items)
|
|
:map(
|
|
---@param item AvanteSlashCommand
|
|
function(item)
|
|
return {
|
|
command = item.command,
|
|
description = item.description,
|
|
callback = cbs[item.command],
|
|
details = item.shorthelp and table.concat({ item.shorthelp, item.description }, "\n") or item.description,
|
|
}
|
|
end
|
|
)
|
|
:totable()
|
|
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 selected_code_size = self:get_selected_code_size()
|
|
|
|
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,
|
|
size = {
|
|
height = selected_code_size + 3,
|
|
},
|
|
position = "top",
|
|
})
|
|
self.selected_code_container:mount()
|
|
if self:get_layout() == "horizontal" then
|
|
api.nvim_win_set_height(
|
|
self.result_container.winid,
|
|
api.nvim_win_get_height(self.result_container.winid) - selected_code_size - 3
|
|
)
|
|
end
|
|
self:adjust_result_container_layout()
|
|
self:adjust_selected_files_container_layout()
|
|
end
|
|
end
|
|
|
|
local generating_text = "**Generating response ...**\n"
|
|
|
|
local hint_window = nil
|
|
|
|
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)
|
|
end
|
|
|
|
---@param opts AskOptions
|
|
function Sidebar:create_input_container(opts)
|
|
if self.input_container then self.input_container:unmount() end
|
|
|
|
if not self.code.bufnr or not api.nvim_buf_is_valid(self.code.bufnr) then return end
|
|
|
|
if self.chat_history == nil then self:reload_chat_history() end
|
|
|
|
---@param request string
|
|
---@param summarize_memory boolean
|
|
---@param cb fun(opts: AvanteGeneratePromptsOptions): nil
|
|
local function get_generate_prompts_options(request, summarize_memory, 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 selected_files_contents = self.file_selector:get_selected_files_contents() or {}
|
|
|
|
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.get_current_selection_diagnostics(self.code.bufnr, self.code.selection)
|
|
else
|
|
diagnostics = Utils.get_diagnostics(self.code.bufnr)
|
|
end
|
|
end
|
|
|
|
local entries = Utils.history.filter_active_entries(self.chat_history.entries)
|
|
|
|
if self.chat_history.memory then
|
|
entries = vim
|
|
.iter(entries)
|
|
:filter(function(entry) return entry.timestamp > self.chat_history.memory.last_summarized_timestamp end)
|
|
:totable()
|
|
end
|
|
|
|
local history_messages = Utils.history.entries_to_llm_messages(entries)
|
|
|
|
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 = {},
|
|
})
|
|
|
|
---@type AvanteGeneratePromptsOptions
|
|
local prompts_opts = {
|
|
ask = opts.ask or true,
|
|
project_context = vim.json.encode(project_context),
|
|
selected_files = selected_files_contents,
|
|
recently_viewed_files = Utils.get_recent_filepaths(),
|
|
diagnostics = vim.json.encode(diagnostics),
|
|
history_messages = history_messages,
|
|
code_lang = filetype,
|
|
selected_code = selected_code,
|
|
instructions = request,
|
|
mode = Config.behaviour.enable_cursor_planning_mode and "cursor-planning" or "planning",
|
|
tools = tools,
|
|
}
|
|
|
|
if self.chat_history.memory then prompts_opts.memory = self.chat_history.memory.content end
|
|
|
|
if not summarize_memory or #history_messages < 12 then
|
|
cb(prompts_opts)
|
|
return
|
|
end
|
|
|
|
prompts_opts.history_messages = vim.list_slice(prompts_opts.history_messages, 7)
|
|
|
|
Llm.summarize_memory(self.code.bufnr, self.chat_history, function(memory)
|
|
if memory then prompts_opts.memory = memory.content end
|
|
cb(prompts_opts)
|
|
end)
|
|
end
|
|
|
|
---@param request string
|
|
local function handle_submit(request)
|
|
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
|
|
|
|
local model = Config.has_provider(Config.provider) and Config.get_provider_config(Config.provider).model
|
|
or "default"
|
|
|
|
local timestamp = get_timestamp()
|
|
|
|
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
|
|
|
|
local content_prefix =
|
|
render_chat_record_prefix(timestamp, Config.provider, model, request, selected_filepaths, selected_code)
|
|
|
|
--- HACK: we need to set focus to true and scroll to false to
|
|
--- prevent the cursor from jumping to the bottom of the
|
|
--- buffer at the beginning
|
|
self:update_content("", { focus = true, scroll = false })
|
|
self:update_content(content_prefix .. generating_text)
|
|
|
|
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 = self:get_commands()
|
|
---@type AvanteSlashCommand
|
|
local cmd = vim.iter(cmds):filter(function(_) return _.command == command end):totable()[1]
|
|
if cmd then
|
|
if command == "lines" then
|
|
cmd.callback(args, function(args_)
|
|
local _, _, question = args_:match("(%d+)-(%d+)%s+(.*)")
|
|
request = question
|
|
end)
|
|
elseif command == "commit" then
|
|
cmd.callback(args, function(question) request = question end)
|
|
else
|
|
cmd.callback(args)
|
|
return
|
|
end
|
|
else
|
|
self:update_content("Unknown command: " .. command, { focus = false, scroll = false })
|
|
return
|
|
end
|
|
end
|
|
|
|
local original_response = ""
|
|
local transformed_response = ""
|
|
local displayed_response = ""
|
|
local current_path = ""
|
|
|
|
local is_first_chunk = true
|
|
local scroll = true
|
|
|
|
---stop scroll when user presses j/k keys
|
|
local function on_j()
|
|
scroll = false
|
|
---perform scroll
|
|
vim.cmd("normal! j")
|
|
end
|
|
|
|
local function on_k()
|
|
scroll = false
|
|
---perform scroll
|
|
vim.cmd("normal! k")
|
|
end
|
|
|
|
local function on_G()
|
|
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
|
|
|
|
---@type AvanteLLMChunkCallback
|
|
local function on_chunk(chunk)
|
|
self.is_generating = true
|
|
|
|
local remove_line = [[\033[1A\033[K]]
|
|
if chunk:sub(1, #remove_line) == remove_line then
|
|
chunk = chunk:sub(#remove_line + 1)
|
|
local lines = vim.split(transformed_response, "\n")
|
|
local idx = #lines
|
|
while idx > 0 and lines[idx] == "" do
|
|
idx = idx - 1
|
|
end
|
|
if idx == 1 then
|
|
lines = {}
|
|
else
|
|
lines = vim.list_slice(lines, 1, idx - 1)
|
|
end
|
|
transformed_response = table.concat(lines, "\n")
|
|
else
|
|
original_response = original_response .. chunk
|
|
end
|
|
|
|
local selected_files = self.file_selector:get_selected_files_contents()
|
|
|
|
local transformed = transform_result_content(selected_files, transformed_response .. chunk, current_path)
|
|
transformed_response = transformed.content
|
|
if transformed.current_filepath and transformed.current_filepath ~= "" then
|
|
current_path = transformed.current_filepath
|
|
end
|
|
local cur_displayed_response = generate_display_content(transformed)
|
|
if is_first_chunk then
|
|
is_first_chunk = false
|
|
self:update_content(content_prefix .. chunk, { scroll = scroll })
|
|
displayed_response = cur_displayed_response
|
|
return
|
|
end
|
|
local suffix = get_display_content_suffix(transformed)
|
|
self:update_content(content_prefix .. cur_displayed_response .. suffix, { scroll = scroll })
|
|
vim.schedule(function() vim.cmd("redraw") end)
|
|
displayed_response = cur_displayed_response
|
|
end
|
|
|
|
local function on_tool_log(tool_name, log)
|
|
if transformed_response:sub(-1) ~= "\n" then transformed_response = transformed_response .. "\n" end
|
|
transformed_response = transformed_response .. "[" .. tool_name .. "]: " .. log .. "\n"
|
|
local breakline = ""
|
|
if displayed_response:sub(-1) ~= "\n" then breakline = "\n" end
|
|
displayed_response = displayed_response .. breakline .. "[" .. tool_name .. "]: " .. log .. "\n"
|
|
self:update_content(content_prefix .. displayed_response, {
|
|
scroll = scroll,
|
|
})
|
|
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
|
|
self:update_content(
|
|
content_prefix .. displayed_response .. "\n\nError: " .. vim.inspect(stop_opts.error),
|
|
{ scroll = scroll }
|
|
)
|
|
return
|
|
end
|
|
|
|
self:update_content(
|
|
content_prefix
|
|
.. displayed_response
|
|
.. "\n\n**Generation complete!** Please review the code suggestions above.\n",
|
|
{
|
|
scroll = scroll,
|
|
callback = function() api.nvim_exec_autocmds("User", { pattern = VIEW_BUFFER_UPDATED_PATTERN }) end,
|
|
}
|
|
)
|
|
|
|
vim.defer_fn(function()
|
|
if
|
|
self.result_container
|
|
and self.result_container.winid
|
|
and api.nvim_win_is_valid(self.result_container.winid)
|
|
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)
|
|
|
|
-- Save chat history
|
|
self.chat_history.entries = self.chat_history.entries or {}
|
|
table.insert(self.chat_history.entries, {
|
|
timestamp = timestamp,
|
|
provider = Config.provider,
|
|
model = model,
|
|
request = request,
|
|
response = displayed_response,
|
|
original_response = original_response,
|
|
selected_filepaths = selected_filepaths,
|
|
selected_code = selected_code,
|
|
tool_histories = stop_opts.tool_histories,
|
|
})
|
|
Path.history.save(self.code.bufnr, self.chat_history)
|
|
end
|
|
|
|
get_generate_prompts_options(request, true, 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_chunk = on_chunk,
|
|
on_stop = on_stop,
|
|
on_tool_log = on_tool_log,
|
|
})
|
|
|
|
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_size = self:get_selected_code_size()
|
|
|
|
return {
|
|
width = "40%",
|
|
height = math.max(1, api.nvim_win_get_height(self.result_container.winid) - selected_code_size),
|
|
}
|
|
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 self.input_container
|
|
or not self.input_container.bufnr
|
|
or not api.nvim_buf_is_valid(self.input_container.bufnr)
|
|
then
|
|
return
|
|
end
|
|
local lines = api.nvim_buf_get_lines(self.input_container.bufnr, 0, -1, false)
|
|
local request = table.concat(lines, "\n")
|
|
if request == "" then return end
|
|
api.nvim_buf_set_lines(self.input_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
|
|
api.nvim_feedkeys(api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
|
|
end
|
|
|
|
self.input_container:map("n", Config.mappings.submit.normal, on_submit)
|
|
self.input_container:map("i", Config.mappings.submit.insert, on_submit)
|
|
|
|
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()
|
|
local has_cmp, cmp = pcall(require, "cmp")
|
|
if has_cmp then
|
|
local mentions = Utils.get_mentions()
|
|
|
|
table.insert(mentions, {
|
|
description = "file",
|
|
command = "file",
|
|
details = "add files...",
|
|
callback = function() self.file_selector:open() end,
|
|
})
|
|
|
|
table.insert(mentions, {
|
|
description = "quickfix",
|
|
command = "quickfix",
|
|
details = "add files in quickfix list to chat context",
|
|
callback = function() self.file_selector:add_quickfix_files() end,
|
|
})
|
|
|
|
table.insert(mentions, {
|
|
description = "buffers",
|
|
command = "buffers",
|
|
details = "add open buffers to the chat context",
|
|
callback = function() self.file_selector:add_buffer_files() end,
|
|
})
|
|
|
|
cmp.register_source(
|
|
"avante_commands",
|
|
require("cmp_avante.commands"):new(self:get_commands(), self.input_container.bufnr)
|
|
)
|
|
cmp.register_source(
|
|
"avante_mentions",
|
|
require("cmp_avante.mentions"):new(mentions, self.input_container.bufnr)
|
|
)
|
|
|
|
cmp.setup.buffer({
|
|
enabled = true,
|
|
sources = {
|
|
{ name = "avante_commands" },
|
|
{ name = "avante_mentions" },
|
|
{ name = "avante_files" },
|
|
},
|
|
})
|
|
end
|
|
end,
|
|
})
|
|
|
|
-- Close the floating window
|
|
local function close_hint()
|
|
if hint_window and api.nvim_win_is_valid(hint_window) then
|
|
local buf = api.nvim_win_get_buf(hint_window)
|
|
api.nvim_win_close(hint_window, true)
|
|
api.nvim_buf_delete(buf, { force = true })
|
|
hint_window = nil
|
|
end
|
|
end
|
|
|
|
local function get_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
|
|
local function show_hint()
|
|
close_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_add_highlight(buf, 0, "AvantePopupHint", 0, 0, -1)
|
|
|
|
-- 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 = get_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
|
|
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")
|
|
get_generate_prompts_options(input_value, false, function(generate_prompts_options)
|
|
local tokens = Llm.calculate_tokens(generate_prompts_options)
|
|
hint_text = "Tokens: " .. tostring(tokens) .. "; " .. hint_text
|
|
show()
|
|
end)
|
|
else
|
|
show()
|
|
end
|
|
end
|
|
|
|
api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "VimResized" }, {
|
|
group = self.augroup,
|
|
buffer = self.input_container.bufnr,
|
|
callback = function()
|
|
show_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() close_hint() 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("startinsert") end
|
|
end,
|
|
})
|
|
|
|
api.nvim_create_autocmd("BufLeave", {
|
|
group = self.augroup,
|
|
buffer = self.input_container.bufnr,
|
|
callback = function() vim.cmd("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 show_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 show_hint() end
|
|
end,
|
|
})
|
|
|
|
api.nvim_create_autocmd("WinEnter", {
|
|
callback = function()
|
|
local cur_win = api.nvim_get_current_win()
|
|
if self.input_container and cur_win == self.input_container.winid then
|
|
show_hint()
|
|
else
|
|
close_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,
|
|
})
|
|
|
|
self:refresh_winids()
|
|
end
|
|
|
|
function Sidebar:get_selected_code_size()
|
|
local selected_code_max_lines_count = 10
|
|
|
|
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
|
|
selected_code_size = math.min(selected_code_lines_count, selected_code_max_lines_count)
|
|
end
|
|
|
|
return selected_code_size
|
|
end
|
|
|
|
function Sidebar:get_selected_files_size()
|
|
if not self.file_selector then return 0 end
|
|
|
|
local selected_files_max_lines_count = 10
|
|
|
|
local selected_files = self.file_selector:get_selected_filepaths()
|
|
local selected_files_size = #selected_files
|
|
selected_files_size = math.min(selected_files_size, selected_files_max_lines_count)
|
|
|
|
return selected_files_size
|
|
end
|
|
|
|
function Sidebar:get_result_container_height()
|
|
local selected_code_size = self:get_selected_code_size()
|
|
local selected_files_size = self:get_selected_files_size()
|
|
|
|
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_size - selected_code_size - 3 - 8)
|
|
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)
|
|
local chat_history = Path.history.load(self.code.bufnr)
|
|
|
|
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,
|
|
}),
|
|
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(opts)
|
|
|
|
self:create_selected_files_container()
|
|
|
|
self:update_content_with_history(chat_history)
|
|
|
|
-- reset states when buffer is closed
|
|
api.nvim_buf_attach(self.code.bufnr, false, {
|
|
on_detach = function(_, _)
|
|
if self and self.reset then vim.schedule(function() self:reset() end) end
|
|
end,
|
|
})
|
|
|
|
self:create_selected_code_container()
|
|
|
|
self:on_mount(opts)
|
|
|
|
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 self.selected_files_container then return end
|
|
if not self.selected_files_container.winid or not api.nvim_win_is_valid(self.selected_files_container.winid) 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: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, {
|
|
wrap = Config.windows.wrap,
|
|
}),
|
|
position = "top",
|
|
size = {
|
|
width = "40%",
|
|
height = 2,
|
|
},
|
|
})
|
|
|
|
self.selected_files_container:mount()
|
|
|
|
local function render()
|
|
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
|
|
|
|
if #selected_filepaths_ == 0 then
|
|
self.selected_files_container:unmount()
|
|
return
|
|
end
|
|
|
|
local selected_filepaths_with_icon = {}
|
|
for _, filepath in ipairs(selected_filepaths_) do
|
|
local icon = Utils.file.get_file_icon(filepath)
|
|
table.insert(selected_filepaths_with_icon, string.format("%s %s", icon, filepath))
|
|
end
|
|
|
|
local selected_files_buf = api.nvim_win_get_buf(self.selected_files_container.winid)
|
|
Utils.unlock_buf(selected_files_buf)
|
|
api.nvim_buf_set_lines(selected_files_buf, 0, -1, true, selected_filepaths_with_icon)
|
|
Utils.lock_buf(selected_files_buf)
|
|
local win_height = 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,
|
|
Utils.icon(" ") .. "Selected Files",
|
|
Highlights.SUBTITLE,
|
|
Highlights.REVERSED_SUBTITLE
|
|
)
|
|
self:adjust_result_container_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
|
|
|
|
-- Function to show hint
|
|
local function show_hint()
|
|
local cursor_pos = api.nvim_win_get_cursor(self.selected_files_container.winid)
|
|
local line_number = cursor_pos[1]
|
|
local col_number = cursor_pos[2]
|
|
|
|
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
|
|
local hint
|
|
if #selected_filepaths_ == 0 then
|
|
hint = string.format(" [%s: add] ", Config.mappings.sidebar.add_file)
|
|
else
|
|
hint =
|
|
string.format(" [%s: delete, %s: add] ", Config.mappings.sidebar.remove_file, Config.mappings.sidebar.add_file)
|
|
end
|
|
|
|
api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1)
|
|
|
|
api.nvim_buf_set_extmark(
|
|
self.selected_files_container.bufnr,
|
|
SELECTED_FILES_HINT_NAMESPACE,
|
|
line_number - 1,
|
|
col_number,
|
|
{
|
|
virt_text = { { hint, "AvanteInlineHint" } },
|
|
virt_text_pos = "right_align",
|
|
hl_group = "AvanteInlineHint",
|
|
priority = PRIORITY,
|
|
}
|
|
)
|
|
end
|
|
|
|
-- Set up keybinding to remove files
|
|
self.selected_files_container:map("n", Config.mappings.sidebar.remove_file, function()
|
|
local line_number = api.nvim_win_get_cursor(self.selected_files_container.winid)[1]
|
|
remove_file(line_number)
|
|
end, { noremap = true, silent = true })
|
|
|
|
self.selected_files_container:map(
|
|
"n",
|
|
Config.mappings.sidebar.add_file,
|
|
function() self.file_selector:open() end,
|
|
{ noremap = true, silent = true }
|
|
)
|
|
|
|
-- Set up autocmd to show hint on cursor move
|
|
self.selected_files_container:on({ event.CursorMoved }, show_hint, {})
|
|
|
|
-- Clear hint when leaving the window
|
|
self.selected_files_container:on(
|
|
event.BufLeave,
|
|
function() api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1) end,
|
|
{}
|
|
)
|
|
|
|
render()
|
|
end
|
|
|
|
return Sidebar
|