Files
avante.nvim/lua/avante/sidebar.lua
Christopher Brewin 78dd9b0a6d feat(context): add a ui for selecting and adding files to the sidebar as context (#912)
* feat(sidebar): supports select files

chore (context) update add type annotations to context functions

chore (sidebar) remove unused notify function call

refactor (sidebar) remove setting search file to file path

chore (sidebar) remove nvim_notify debugging api call

* feat (files) allow selecting a file by string via cmp suggestion menu

* chore (context) refactor to allow context using @file with a context view

* refactor (context) refactor seletected file types as an array of path and content

* refactor (config) remove unused configuration options

* refactor (sidebar) remove unused unbild key

* refactor (context) remove unused imports

* refactor (mentions) update mentions to support items with callback functions and removal of the underlying selection.

* fix (sidebar) add file context as a window that is visitable via the tab key

* refactor (file_content) remove file content as an input to llm

* feat (sidebar) support suggesting and applying code in all languages that are in the context

* feat (sidebar) configurable mapping for removing a file from the context.

* feat (context_view) configure hints for the context view for adding and deleting a file.

* feat (context) add hints for the context view.

* fix (sidebar) type when scrolling the results buffer.

* refactor (selected files) refactor llm stream to accept an array of selected file metadata

* refactor: context => selected_files

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2024-12-12 01:29:10 +08:00

2136 lines
66 KiB
Lua

local api = vim.api
local fn = vim.fn
local Split = require("nui.split")
local event = require("nui.utils.autocmd").event
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 RESULT_BUF_NAME = "AVANTE_RESULT"
local VIEW_BUFFER_UPDATED_PATTERN = "AvanteViewBufferUpdated"
local CODEBLOCK_KEYBINDING_NAMESPACE = api.nvim_create_namespace("AVANTE_CODEBLOCK_KEYBINDING")
local SELECTED_FILES_HINT_NAMESPACE = api.nvim_create_namespace("AVANTE_SELECTED_FILES_HINT")
local PRIORITY = vim.highlight.priorities.user
---@class avante.Sidebar
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
---@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),
}, { __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()
self:delete_autocmds()
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)
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
---@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: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
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 last_search_tag_start_line integer
---@field last_replace_tag_start_line integer
---@param selected_files {path: string, content: string, file_type: string | nil}[]
---@param result_content 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 last_search_tag_start_line = 0
local last_replace_tag_start_line = 0
local search_start = 0
local current_filepath
local i = 1
while i <= #result_lines do
local line_content = result_lines[i]
if line_content:match("<FILEPATH>.+</FILEPATH>") then
local filepath = line_content:match("<FILEPATH>(.+)</FILEPATH>")
if filepath then
current_filepath = filepath
table.insert(transformed_lines, string.format("Filepath: %s", filepath))
goto continue
end
end
if line_content == "<SEARCH>" then
is_searching = true
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 == "</SEARCH>" then
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
for _, file in ipairs(selected_files) do
if not Utils.is_same_file(file.path, prev_filepath or "") then goto continue1 end
local file_content = vim.split(file.content, "\n")
if start_line ~= 0 or end_line ~= 0 then break end
for j = 1, #file_content - (search_end - search_start) + 1 do
local match = true
for k = 0, search_end - search_start - 1 do
if
Utils.remove_indentation(file_content[j + k]) ~= Utils.remove_indentation(result_lines[search_start + k])
then
match = false
break
end
end
if match then
start_line = j
end_line = j + (search_end - search_start) - 1
match_filetype = file.file_type
break
end
end
::continue1::
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 == "<REPLACE>" then
is_replacing = true
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 == "</REPLACE>" then
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
end
table.insert(transformed_lines, line_content)
::continue::
i = i + 1
end
return {
current_filepath = current_filepath,
content = table.concat(transformed_lines, "\n"),
is_searching = is_searching,
is_replacing = is_replacing,
last_search_tag_start_line = last_search_tag_start_line,
last_replace_tag_start_line = last_replace_tag_start_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 function get_display_content_suffix(replacement)
if replacement.is_searching then return get_searching_hint() 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
return replacement.content
end
---@class AvanteCodeSnippet
---@field range integer[]
---@field content string
---@field lang string
---@field explanation string
---@field start_line_in_response_buf integer
---@field end_line_in_response_buf integer
---@field filepath string
---@param response_content string
---@return table<string, AvanteCodeSnippet[]>
local function extract_code_snippets_map(response_content)
local snippets = {}
local current_snippet = {}
local in_code_block = false
local lang, start_line, end_line, start_line_in_response_buf
local explanation = ""
local lines = vim.split(response_content, "\n")
for idx, line in ipairs(lines) do
local _, start_line_str, end_line_str =
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 = 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 = 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
if line:match("^%s*```") then
if in_code_block then
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 snippet = {
range = { start_line, end_line },
content = table.concat(current_snippet, "\n"),
lang = lang,
explanation = explanation,
start_line_in_response_buf = start_line_in_response_buf,
end_line_in_response_buf = idx,
filepath = filepath,
}
table.insert(snippets, snippet)
end
current_snippet = {}
start_line, end_line = nil, nil
explanation = ""
in_code_block = false
else
lang = line:match("^%s*```(%w+)")
if not lang or lang == "" then lang = "text" end
in_code_block = true
start_line_in_response_buf = idx
end
elseif in_code_block then
table.insert(current_snippet, line)
else
explanation = explanation .. line .. "\n"
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_content = ""
if Utils.file.exists(filepath) then original_content = Utils.file.read_content(filepath) or "" end
local original_lines = vim.split(original_content, "\n")
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]
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 content = table.concat(Utils.get_buf_lines(0, -1, bufnr), "\n")
local lines = vim.split(content, "\n")
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 need_prepend_indentation = false
local start_line_indentation = ""
local original_start_line_indentation = Utils.get_indentation(lines[start_line] or "")
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")
for idx, line in ipairs(snippet_lines) do
if idx == 1 then
start_line_indentation = Utils.get_indentation(line)
need_prepend_indentation = start_line_indentation ~= original_start_line_indentation
end
if need_prepend_indentation then
if line:sub(1, #start_line_indentation) == start_line_indentation then
line = line:sub(#start_line_indentation + 1)
end
line = original_start_line_indentation .. line
end
table.insert(result, line)
end
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()
cursor_line = cursor_line - 1 -- transform to 0-indexed line number
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 AvanteCodeblock
---@field start_line integer
---@field end_line integer
---@field lang string
---@param buf integer
---@return AvanteCodeblock[]
local function parse_codeblocks(buf)
local codeblocks = {}
local in_codeblock = false
local start_line = nil
local lang = nil
local lines = Utils.get_buf_lines(0, -1, buf)
for i, line in ipairs(lines) do
if line:match("^%s*```") then
-- parse language
local lang_ = line:match("^%s*```(%w+)")
if in_codeblock and not lang_ then
table.insert(codeblocks, { start_line = start_line, end_line = i - 1, lang = lang })
in_codeblock = false
elseif lang_ and lines[i - 1]:match("^%s*(%d*)[%.%)%s]*[Aa]?n?d?%s*[Rr]eplace%s+[Ll]ines:?%s*(%d+)%-(%d+)") then
lang = lang_
start_line = i - 1
in_codeblock = true
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 snippets_map table<string, AvanteCodeSnippet[]>
---@return table<string, AvanteCodeSnippet[]>
function Sidebar:minimize_snippets(snippets_map)
local original_lines = api.nvim_buf_get_lines(self.code.bufnr, 0, -1, false)
local results = {}
for filepath, snippets in pairs(snippets_map) do
for _, snippet in ipairs(snippets) do
local new_snippets = minimize_snippet(original_lines, snippet)
if new_snippets then
results[filepath] = results[filepath] or {}
for _, new_snippet in ipairs(new_snippets) do
table.insert(results[filepath], new_snippet)
end
end
end
end
return results
end
---@param current_cursor boolean
function Sidebar:apply(current_cursor)
local response, response_start_line = self:get_content_between_separators()
local all_snippets_map = extract_code_snippets_map(response)
all_snippets_map = ensure_snippets_no_overlap(all_snippets_map)
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.options.behaviour.minimize_diff then
selected_snippets_map = self:minimize_snippets(selected_snippets_map)
end
vim.defer_fn(function()
api.nvim_set_current_win(self.code.winid)
for filepath, snippets in pairs(selected_snippets_map) do
local bufnr = Utils.get_or_create_buffer_with_filepath(filepath)
insert_conflict_contents(bufnr, snippets)
local process = function(winid)
api.nvim_set_current_win(winid)
api.nvim_feedkeys(api.nvim_replace_termcodes("<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 = "󰭻 Avante"
self:render_header(
self.result_container.winid,
self.result_container.bufnr,
header_text,
Highlights.TITLE,
Highlights.REVERSED_TITLE
)
end
function Sidebar:get_file_icon(filepath)
local filetype = vim.filetype.match({ filename = filepath }) or "unknown"
---@type string
local icon
---@diagnostic disable-next-line: undefined-field
if _G.MiniIcons ~= nil then
---@diagnostic disable-next-line: undefined-global
icon, _, _ = MiniIcons.get("filetype", filetype) -- luacheck: ignore
else
local ok, devicons = pcall(require, "nvim-web-devicons")
if ok then
icon = devicons.get_icon_by_filetype(filetype, {})
else
icon = ""
end
end
return icon
end
---@param ask? boolean
function Sidebar:render_input(ask)
if ask == nil then ask = true end
if
not self.input_container
or not self.input_container.bufnr
or not api.nvim_buf_is_valid(self.input_container.bufnr)
then
return
end
local header_text = string.format(
"󱜸 %s (" .. Config.mappings.sidebar.switch_windows .. ": switch focus)",
ask and "Ask" or "Chat with"
)
if self.code.selection ~= nil then
header_text = string.format(
"󱜸 %s (%d:%d) (<Tab>: switch focus)",
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 = " 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
---@param opts AskOptions
function Sidebar:on_mount(opts)
self:refresh_winids()
api.nvim_set_option_value("wrap", Config.windows.wrap, { win = self.result_container.winid })
local current_apply_extmark_id = nil
local function show_apply_button(block)
if current_apply_extmark_id then
api.nvim_buf_del_extmark(self.result_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, {
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 function bind_apply_key()
vim.keymap.set(
"n",
Config.mappings.sidebar.apply_cursor,
function() self:apply(true) end,
{ buffer = self.result_container.bufnr, noremap = true, silent = true }
)
end
local function unbind_apply_key()
pcall(vim.keymap.del, "n", Config.mappings.sidebar.apply_cursor, { buffer = self.result_container.bufnr })
end
---@type AvanteCodeblock[]
local 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 + 1, 0 })
vim.cmd("normal! zz")
end
end
local function bind_sidebar_keys()
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
local function 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
api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
buffer = self.result_container.bufnr,
callback = function(ev)
local block = is_cursor_in_codeblock(codeblocks)
if block then
show_apply_button(block)
bind_apply_key()
else
api.nvim_buf_clear_namespace(ev.buf, CODEBLOCK_KEYBINDING_NAMESPACE, 0, -1)
unbind_apply_key()
end
end,
})
api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, {
buffer = self.result_container.bufnr,
callback = function(ev)
codeblocks = parse_codeblocks(ev.buf)
bind_sidebar_keys()
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)
bind_sidebar_keys()
end,
})
api.nvim_create_autocmd("BufLeave", {
buffer = self.result_container.bufnr,
callback = function() 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()
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)
if Config.windows.ask.start_insert then vim.cmd("startinsert") end
end
return true
end,
})
api.nvim_create_autocmd("WinClosed", {
group = self.augroup,
callback = function(args)
local closed_winid = tonumber(args.match)
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()
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 }
)
Utils.safe_keymap_set(
{ "n", "i" },
Config.mappings.sidebar.reverse_switch_windows,
function() reverse_switch_windows() end,
{ buffer = buf, noremap = true, silent = 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
self.file_selector:reset()
self.file_selector:add_selected_file(Utils.relative_path(api.nvim_buf_get_name(self.code.bufnr)))
return self
end
function Sidebar:is_focused_on_result()
return self:is_open() and self.result_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 = true, 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 scroll_to_bottom = function()
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
xpcall(function()
--- set cursor to bottom of result view
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 {filetype: string, content: string}?
---@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 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.filetype
.. "\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.ChatHistoryEntry[]
---@return string
function Sidebar:render_history_content(history)
local content = ""
for idx, entry in ipairs(history) do
if entry.reset_memory then
content = content .. "***MEMORY RESET***\n\n"
if idx < #history then content = content .. "---\n\n" end
goto continue
end
local selected_filepaths = entry.selected_filepaths
if not selected_filepaths then selected_filepaths = { entry.selected_file.filepath } end
local prefix = render_chat_record_prefix(
entry.timestamp,
entry.provider,
entry.model,
entry.request or "",
selected_filepaths,
entry.selected_code
)
content = content .. prefix
content = content .. entry.response .. "\n\n"
if idx < #history 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 = "---"
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
---@alias AvanteSlashCommandType "clear" | "help" | "lines" | "reset"
---@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" },
{
shorthelp = "Ask a question about specific lines",
description = "/lines <start>-<end> <question>",
command = "lines",
},
}
---@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)
local chat_history = Path.history.load(self.code.bufnr)
if next(chat_history) ~= nil then
chat_history = {}
Path.history.save(self.code.bufnr, chat_history)
self:update_content("Chat history cleared", { focus = false, scroll = false })
if cb then cb(args) end
else
self:update_content("Chat history is already empty", { focus = false, scroll = false })
end
end,
reset = function(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.provider).model,
request = "",
response = "",
original_response = "",
selected_file = nil,
selected_code = nil,
reset_memory = true,
})
Path.history.save(self.code.bufnr, chat_history)
local history_content = self:render_history_content(chat_history)
self:update_content(history_content, { focus = false, scroll = true })
if cb then cb(args) end
else
self:update_content("Chat history is already empty", { focus = false, scroll = false })
end
end,
lines = function(args, cb)
if cb then cb(args) 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
end
end
local generating_text = "**Generating response ...**\n"
local hint_window = nil
---@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
local chat_history = Path.history.load(self.code.bufnr)
---@param request string
local function handle_submit(request)
local model = Config.has_provider(Config.provider) and Config.get_provider(Config.provider).model or "default"
local timestamp = get_timestamp()
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
local selected_filepaths = self.file_selector:get_selected_filepaths()
local selected_code = nil
if self.code.selection ~= nil then
selected_code = {
filetype = 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)
local selected_code_content = nil
if self.code.selection ~= nil then selected_code_content = self.code.selection.content end
if request:sub(1, 1) == "/" then
local command, args = request:match("^/(%S+)%s*(.*)")
if command == nil then
self:update_content("Invalid command", { focus = false, scroll = false })
return
end
local cmds = 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 start_line, end_line, question = args_:match("(%d+)-(%d+)%s+(.*)")
---@cast start_line integer
start_line = tonumber(start_line)
---@cast end_line integer
end_line = tonumber(end_line)
if end_line == nil then
Utils.error("Invalid end line number", { once = true, title = "Avante" })
return
end
selected_code_content =
table.concat(api.nvim_buf_get_lines(self.code.bufnr, start_line - 1, end_line, false), "\n")
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
---@type AvanteChunkParser
local on_chunk = function(chunk)
original_response = original_response .. chunk
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 = true })
return
end
local suffix = get_display_content_suffix(transformed)
self:update_content(content_prefix .. cur_displayed_response .. suffix, { scroll = true })
vim.schedule(function() vim.cmd("redraw") end)
displayed_response = cur_displayed_response
end
---@type AvanteCompleteParser
local on_complete = function(err)
if err ~= nil then
self:update_content(
content_prefix .. displayed_response .. "\n\nError: " .. vim.inspect(err),
{ scroll = true }
)
return
end
-- Execute when the stream request is actually completed
self:update_content(
content_prefix
.. displayed_response
.. "\n\n**Generation complete!** Please review the code suggestions above.\n",
{
scroll = true,
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)
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
table.insert(chat_history or {}, {
timestamp = timestamp,
provider = Config.provider,
model = model,
request = request,
response = displayed_response,
original_response = original_response,
selected_filepaths = selected_filepaths,
selected_code = selected_code,
})
Path.history.save(self.code.bufnr, chat_history)
end
local mentions = Utils.extract_mentions(request)
request = mentions.new_content
local file_ext = api.nvim_buf_get_name(self.code.bufnr):match("^.+%.(.+)$")
local project_context = mentions.enable_project_context and RepoMap.get_repo_map(file_ext) or nil
local selected_files_contents = self.file_selector:get_selected_files_contents()
local diagnostics = nil
if mentions.enable_diagnostics then
if self.code ~= nil and self.code.bufnr ~= nil and self.code.selection ~= nil then
diagnostics = Utils.get_current_selection_diagnostics(self.code.bufnr, self.code.selection)
else
diagnostics = Utils.get_diagnostics(self.code.bufnr)
end
end
local history_messages = {}
for i = #chat_history, 1, -1 do
local entry = chat_history[i]
if entry.reset_memory then break end
if
entry.request == nil
or entry.original_response == nil
or entry.request == ""
or entry.original_response == ""
then
break
end
table.insert(history_messages, 1, { role = "assistant", content = entry.original_response })
local user_content = ""
if entry.selected_file ~= nil then
user_content = user_content .. "SELECTED FILE: " .. entry.selected_file.filepath .. "\n\n"
end
if entry.selected_code ~= nil then
user_content = user_content
.. "SELECTED CODE:\n\n```"
.. entry.selected_code.filetype
.. "\n"
.. entry.selected_code.content
.. "\n```\n\n"
end
user_content = user_content .. "USER PROMPT:\n\n" .. entry.request
table.insert(history_messages, 1, { role = "user", content = user_content })
end
Llm.stream({
bufnr = self.code.bufnr,
ask = opts.ask,
project_context = vim.json.encode(project_context),
selected_files = selected_files_contents,
diagnostics = vim.json.encode(diagnostics),
history_messages = history_messages,
code_lang = filetype,
selected_code = selected_code_content,
instructions = request,
mode = "planning",
on_chunk = on_chunk,
on_complete = on_complete,
})
end
local get_position = function()
if self:get_layout() == "vertical" then return "bottom" end
return "right"
end
local get_size = function()
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, {})
handle_submit(request)
end
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)
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,
})
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
api.nvim_win_close(hint_window, 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 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
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,
})
-- 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
---@param opts AskOptions
function Sidebar:render(opts)
local chat_history = Path.history.load(self.code.bufnr)
local get_position = function()
return (opts and opts.win and opts.win.position) and opts.win.position or calculate_config_window_position()
end
local get_height = function()
local selected_code_size = self:get_selected_code_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_code_size - 3 - 8)
end
local get_width = function()
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
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 = get_width(),
height = get_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", "q", function()
Llm.cancel_inflight_request()
self:close()
end)
self.result_container:map("n", "<Esc>", function()
Llm.cancel_inflight_request()
self:close()
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(_, _) self:reset() end,
})
self:create_selected_code_container()
self:on_mount(opts)
return self
end
function Sidebar:create_selected_files_container()
if self.selected_files_container then self.selected_files_container:unmount() end
local selected_filepaths = self.file_selector:get_selected_filepaths()
if #selected_filepaths == 0 then
self.file_selector:off("update")
self.file_selector:on("update", function() self:create_selected_files_container() end)
return
end
self.selected_files_container = Split({
enter = false,
relative = {
type = "win",
winid = self.input_container.winid,
},
buf_options = vim.tbl_deep_extend("force", buf_options, {
modifiable = false,
swapfile = false,
buftype = "nofile",
bufhidden = "wipe",
filetype = "Avante",
}),
win_options = vim.tbl_deep_extend("force", base_win_options, {
wrap = Config.windows.wrap,
}),
position = "top",
size = {
width = "40%",
height = 2,
},
})
self.selected_files_container:mount()
local render = function()
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
if #selected_filepaths_ == 0 then
self.selected_files_container:unmount()
return
end
local selected_filepaths_with_icon = {}
for _, filepath in ipairs(selected_filepaths_) do
local icon = self:get_file_icon(filepath)
table.insert(selected_filepaths_with_icon, string.format("%s %s", icon, filepath))
end
local selected_files_buf = api.nvim_win_get_buf(self.selected_files_container.winid)
Utils.unlock_buf(selected_files_buf)
api.nvim_buf_set_lines(selected_files_buf, 0, -1, true, selected_filepaths_with_icon)
Utils.lock_buf(selected_files_buf)
local win_height = math.min(vim.o.lines - 2, #selected_filepaths_ + 1)
api.nvim_win_set_height(self.selected_files_container.winid, win_height)
self:render_header(
self.selected_files_container.winid,
selected_files_buf,
" Selected Files",
Highlights.SUBTITLE,
Highlights.REVERSED_SUBTITLE
)
end
self.file_selector:on("update", render)
local remove_file = function(line_number)
if self.file_selector:remove_selected_filepaths(line_number) then render() end
end
-- Function to show hint
local function show_hint()
local cursor_pos = api.nvim_win_get_cursor(self.selected_files_container.winid)
local line_number = cursor_pos[1]
local col_number = cursor_pos[2]
local selected_filepaths_ = self.file_selector:get_selected_filepaths()
local hint
if #selected_filepaths_ == 0 then
hint = string.format(" [%s: add] ", Config.mappings.sidebar.add_file)
else
hint =
string.format(" [%s: delete, %s: add] ", Config.mappings.sidebar.remove_file, Config.mappings.sidebar.add_file)
end
api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1)
api.nvim_buf_set_extmark(
self.selected_files_container.bufnr,
SELECTED_FILES_HINT_NAMESPACE,
line_number - 1,
col_number,
{
virt_text = { { hint, "AvanteInlineHint" } },
virt_text_pos = "right_align",
hl_group = "AvanteInlineHint",
priority = PRIORITY,
}
)
end
-- Set up keybinding to remove files
self.selected_files_container:map("n", Config.mappings.sidebar.remove_file, function()
local line_number = api.nvim_win_get_cursor(self.selected_files_container.winid)[1]
remove_file(line_number)
end, { noremap = true, silent = true })
self.selected_files_container:map(
"n",
Config.mappings.sidebar.add_file,
function() self.file_selector:open() end,
{ noremap = true, silent = true }
)
-- Set up autocmd to show hint on cursor move
self.selected_files_container:on({ event.CursorMoved }, show_hint, {})
-- Clear hint when leaving the window
self.selected_files_container:on(
event.BufLeave,
function() api.nvim_buf_clear_namespace(self.selected_files_container.bufnr, SELECTED_FILES_HINT_NAMESPACE, 0, -1) end,
{}
)
render()
end
return Sidebar