Skip to content

Commit

Permalink
feat(context): add a ui for selecting and adding files to the sidebar…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>
  • Loading branch information
brewinski and yetone authored Dec 11, 2024
1 parent 3b33170 commit 78dd9b0
Show file tree
Hide file tree
Showing 13 changed files with 695 additions and 224 deletions.
13 changes: 9 additions & 4 deletions crates/avante-templates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ impl<'a> State<'a> {
}
}

#[derive(Debug, Serialize, Deserialize)]
struct SelectedFile {
path: String,
content: String,
file_type: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct TemplateContext {
use_xml_format: bool,
ask: bool,
code_lang: String,
filepath: String,
file_content: String,
selected_files: Vec<SelectedFile>,
selected_code: Option<String>,
project_context: Option<String>,
diagnostics: Option<String>,
Expand All @@ -44,8 +50,7 @@ fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult<
use_xml_format => context.use_xml_format,
ask => context.ask,
code_lang => context.code_lang,
filepath => context.filepath,
file_content => context.file_content,
selected_files => context.selected_files,
selected_code => context.selected_code,
project_context => context.project_context,
diagnostics => context.diagnostics,
Expand Down
12 changes: 6 additions & 6 deletions lua/avante/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ M.refresh = function(opts)
if not sidebar:is_open() then return end
local curbuf = vim.api.nvim_get_current_buf()

local focused = sidebar.result.bufnr == curbuf or sidebar.input.bufnr == curbuf
local focused = sidebar.result_container.bufnr == curbuf or sidebar.input_container.bufnr == curbuf
if focused or not sidebar:is_open() then return end
local listed = vim.api.nvim_get_option_value("buflisted", { buf = curbuf })

Expand All @@ -185,19 +185,19 @@ M.focus = function(opts)
local curwin = vim.api.nvim_get_current_win()

if sidebar:is_open() then
if curbuf == sidebar.input.bufnr then
if curbuf == sidebar.input_container.bufnr then
if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end
elseif curbuf == sidebar.result.bufnr then
elseif curbuf == sidebar.result_container.bufnr then
if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end
else
if sidebar.input.winid and sidebar.input.winid ~= curwin then
vim.api.nvim_set_current_win(sidebar.input.winid)
if sidebar.input_container.winid and sidebar.input_container.winid ~= curwin then
vim.api.nvim_set_current_win(sidebar.input_container.winid)
end
end
else
if sidebar.code.winid then vim.api.nvim_set_current_win(sidebar.code.winid) end
sidebar:open(opts)
if sidebar.input.winid then vim.api.nvim_set_current_win(sidebar.input.winid) end
if sidebar.input_container.winid then vim.api.nvim_set_current_win(sidebar.input_container.winid) end
end
end

Expand Down
2 changes: 2 additions & 0 deletions lua/avante/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ M.defaults = {
apply_cursor = "a",
switch_windows = "<Tab>",
reverse_switch_windows = "<S-Tab>",
remove_file = "d",
add_file = "@",
},
},
windows = {
Expand Down
5 changes: 2 additions & 3 deletions lua/avante/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ local api = vim.api

local Config = require("avante.config")
local Utils = require("avante.utils")
local Highlights = require("avante.highlights")

local H = {}
local M = {}
Expand Down Expand Up @@ -558,7 +557,7 @@ end
---@param enable_autojump boolean
function M.process_position(bufnr, side, position, enable_autojump)
local lines = {}
if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS, SIDES.BASE }, side) then
if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS }, side) then
local data = position[name_map[side]]
lines = Utils.get_buf_lines(data.content_start, data.content_end + 1)
elseif side == SIDES.BOTH then
Expand All @@ -569,7 +568,7 @@ function M.process_position(bufnr, side, position, enable_autojump)
lines = {}
elseif side == SIDES.CURSOR then
local cursor_line = Utils.get_cursor_pos()
for _, pos in ipairs({ SIDES.OURS, SIDES.THEIRS, SIDES.BASE }) do
for _, pos in ipairs({ SIDES.OURS, SIDES.THEIRS }) do
local data = position[name_map[pos]] or {}
if data.range_start and data.range_start + 1 <= cursor_line and data.range_end + 1 >= cursor_line then
side = pos
Expand Down
146 changes: 146 additions & 0 deletions lua/avante/file_selector.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
local Utils = require("avante.utils")
local Path = require("plenary.path")
local scan = require("plenary.scandir")

--- @class FileSelector
local FileSelector = {}

--- @class FileSelector
--- @field id integer
--- @field selected_filepaths string[]
--- @field file_cache string[]
--- @field event_handlers table<string, function[]>

---@param id integer
---@return FileSelector
function FileSelector:new(id)
return setmetatable({
id = id,
selected_files = {},
file_cache = {},
event_handlers = {},
}, { __index = self })
end

function FileSelector:reset()
self.selected_filepaths = {}
self.event_handlers = {}
end

function FileSelector:add_selected_file(filepath) table.insert(self.selected_filepaths, Utils.uniform_path(filepath)) end

function FileSelector:on(event, callback)
local handlers = self.event_handlers[event]
if not handlers then
handlers = {}
self.event_handlers[event] = handlers
end

table.insert(handlers, callback)
end

function FileSelector:emit(event, ...)
local handlers = self.event_handlers[event]
if not handlers then return end

for _, handler in ipairs(handlers) do
handler(...)
end
end

function FileSelector:off(event, callback)
if not callback then
self.event_handlers[event] = {}
return
end
local handlers = self.event_handlers[event]
if not handlers then return end

for i, handler in ipairs(handlers) do
if handler == callback then
table.remove(handlers, i)
break
end
end
end

---@return nil
function FileSelector:open()
self:update_file_cache()
self:show_select_ui()
end

---@return nil
function FileSelector:update_file_cache()
local project_root = Path:new(Utils.get_project_root()):absolute()

local filepaths = scan.scan_dir(project_root, {
respect_gitignore = true,
})

-- Sort buffer names alphabetically
table.sort(filepaths, function(a, b) return a < b end)

self.file_cache = vim
.iter(filepaths)
:map(function(filepath) return Path:new(filepath):make_relative(project_root) end)
:totable()
end

---@return nil
function FileSelector:show_select_ui()
vim.schedule(function()
local filepaths = vim
.iter(self.file_cache)
:filter(function(filepath) return not vim.tbl_contains(self.selected_filepaths, filepath) end)
:totable()

vim.ui.select(filepaths, {
prompt = "(Avante) Add a file:",
format_item = function(item) return item end,
}, function(filepath)
if not filepath then return end
table.insert(self.selected_filepaths, Utils.uniform_path(filepath))
self:emit("update")
end)
end)

-- unlist the current buffer as vim.ui.select will be listed
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
vim.api.nvim_set_option_value("buflisted", false, { buf = bufnr })
vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = bufnr })
end

---@param idx integer
---@return boolean
function FileSelector:remove_selected_filepaths(idx)
if idx > 0 and idx <= #self.selected_filepaths then
table.remove(self.selected_filepaths, idx)
self:emit("update")
return true
end
return false
end

---@return { path: string, content: string, file_type: string }[]
function FileSelector:get_selected_files_contents()
local contents = {}
for _, file_path in ipairs(self.selected_filepaths) do
local file = io.open(file_path, "r")
if file then
local content = file:read("*all")
file:close()

-- Detect the file type
local filetype = vim.filetype.match({ filename = file_path, contents = contents }) or "unknown"

table.insert(contents, { path = file_path, content = content, file_type = filetype })
end
end
return contents
end

function FileSelector:get_selected_filepaths() return vim.deepcopy(self.selected_filepaths) end

return FileSelector
30 changes: 23 additions & 7 deletions lua/avante/llm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,11 @@ M._stream = function(opts, Provider)

Path.prompts.initialize(Path.prompts.get(opts.bufnr))

local filepath = Utils.relative_path(api.nvim_buf_get_name(opts.bufnr))

local template_opts = {
use_xml_format = Provider.use_xml_format,
ask = opts.ask, -- TODO: add mode without ask instruction
code_lang = opts.code_lang,
filepath = filepath,
file_content = opts.file_content,
selected_files = opts.selected_files,
selected_code = opts.selected_code,
project_context = opts.project_context,
diagnostics = opts.diagnostics,
Expand Down Expand Up @@ -335,14 +332,19 @@ end

---@alias LlmMode "planning" | "editing" | "suggesting"
---
---@class SelectedFiles
---@field path string
---@field content string
---@field file_type string
---
---@class TemplateOptions
---@field use_xml_format boolean
---@field ask boolean
---@field question string
---@field code_lang string
---@field file_content string
---@field selected_code string | nil
---@field project_context string | nil
---@field selected_files SelectedFiles[] | nil
---@field diagnostics string | nil
---@field history_messages AvanteLLMMessage[]
---
Expand All @@ -357,8 +359,22 @@ end

---@param opts StreamOptions
M.stream = function(opts)
if opts.on_chunk ~= nil then opts.on_chunk = vim.schedule_wrap(opts.on_chunk) end
if opts.on_complete ~= nil then opts.on_complete = vim.schedule_wrap(opts.on_complete) end
local is_completed = false
if opts.on_chunk ~= nil then
local original_on_chunk = opts.on_chunk
opts.on_chunk = vim.schedule_wrap(function(chunk)
if is_completed then return end
return original_on_chunk(chunk)
end)
end
if opts.on_complete ~= nil then
local original_on_complete = opts.on_complete
opts.on_complete = vim.schedule_wrap(function(err)
if is_completed then return end
is_completed = true
return original_on_complete(err)
end)
end
local Provider = opts.provider or P[Config.provider]
if Config.dual_boost.enabled then
M._dual_boost_stream(opts, Provider, P[Config.dual_boost.first_provider], P[Config.dual_boost.second_provider])
Expand Down
1 change: 1 addition & 0 deletions lua/avante/path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ local Config = require("avante.config")
---@field selected_file {filepath: string}?
---@field selected_code {filetype: string, content: string}?
---@field reset_memory boolean?
---@field selected_filepaths string[] | nil

---@class avante.Path
---@field history_path Path
Expand Down
2 changes: 1 addition & 1 deletion lua/avante/selection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function Selection:create_editing_input()
ask = true,
project_context = vim.json.encode(project_context),
diagnostics = vim.json.encode(diagnostics),
file_content = code_content,
selected_files = { { content = code_content, file_type = filetype, path = "" } },
code_lang = filetype,
selected_code = self.selection.content,
instructions = input,
Expand Down
Loading

0 comments on commit 78dd9b0

Please sign in to comment.