
* 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>
214 lines
7.4 KiB
Lua
214 lines
7.4 KiB
Lua
local fn = vim.fn
|
|
local Utils = require("avante.utils")
|
|
local LRUCache = require("avante.utils.lru_cache")
|
|
local Path = require("plenary.path")
|
|
local Scan = require("plenary.scandir")
|
|
local Config = require("avante.config")
|
|
|
|
---@class avante.ChatHistoryEntry
|
|
---@field timestamp string
|
|
---@field provider string
|
|
---@field model string
|
|
---@field request string
|
|
---@field response string
|
|
---@field original_response string
|
|
---@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
|
|
---@field cache_path Path
|
|
local P = {}
|
|
|
|
local history_file_cache = LRUCache:new(12)
|
|
|
|
-- History path
|
|
local History = {}
|
|
|
|
-- Get a chat history file name given a buffer
|
|
---@param bufnr integer
|
|
---@return string
|
|
History.filename = function(bufnr)
|
|
local project_root = Utils.root.get({
|
|
buf = bufnr,
|
|
})
|
|
-- Replace path separators with double underscores
|
|
local path_with_separators = fn.substitute(project_root, "/", "__", "g")
|
|
-- Replace other non-alphanumeric characters with single underscores
|
|
return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. ".json"
|
|
end
|
|
|
|
-- Returns the Path to the chat history file for the given buffer.
|
|
---@param bufnr integer
|
|
---@return Path
|
|
History.get = function(bufnr) return Path:new(Config.history.storage_path):joinpath(History.filename(bufnr)) end
|
|
|
|
-- Loads the chat history for the given buffer.
|
|
---@param bufnr integer
|
|
---@return avante.ChatHistoryEntry[]
|
|
History.load = function(bufnr)
|
|
local history_file = History.get(bufnr)
|
|
local cached_key = tostring(history_file:absolute())
|
|
local cached_value = history_file_cache:get(cached_key)
|
|
if cached_value ~= nil then return cached_value end
|
|
local value = {}
|
|
if history_file:exists() then
|
|
local content = history_file:read()
|
|
value = content ~= nil and vim.json.decode(content) or {}
|
|
end
|
|
history_file_cache:set(cached_key, value)
|
|
return value
|
|
end
|
|
|
|
-- Saves the chat history for the given buffer.
|
|
---@param bufnr integer
|
|
---@param history avante.ChatHistoryEntry[]
|
|
History.save = vim.schedule_wrap(function(bufnr, history)
|
|
local history_file = History.get(bufnr)
|
|
local cached_key = tostring(history_file:absolute())
|
|
history_file:write(vim.json.encode(history), "w")
|
|
history_file_cache:set(cached_key, history)
|
|
end)
|
|
|
|
P.history = History
|
|
|
|
-- Prompt path
|
|
local Prompt = {}
|
|
|
|
-- Given a mode, return the file name for the custom prompt.
|
|
---@param mode LlmMode
|
|
Prompt.get_mode_file = function(mode) return string.format("custom.%s.avanterules", mode) end
|
|
|
|
---@class AvanteTemplates
|
|
---@field initialize fun(directory: string): nil
|
|
---@field render fun(template: string, context: TemplateOptions): string
|
|
local templates = nil
|
|
|
|
Prompt.templates = { planning = nil, editing = nil, suggesting = nil }
|
|
|
|
-- Creates a directory in the cache path for the given buffer and copies the custom prompts to it.
|
|
-- We need to do this beacuse the prompt template engine requires a given directory to load all required files.
|
|
-- PERF: Hmm instead of copy to cache, we can also load in globals context, but it requires some work on bindings. (eh maybe?)
|
|
---@param bufnr number
|
|
---@return string the resulted cache_directory to be loaded with avante_templates
|
|
Prompt.get = function(bufnr)
|
|
if not P.available() then error("Make sure to build avante (missing avante_templates)", 2) end
|
|
|
|
-- get root directory of given bufnr
|
|
local directory = Path:new(Utils.root.get({ buf = bufnr }))
|
|
if Utils.get_os_name() == "windows" then directory = Path:new(directory:absolute():gsub("^%a:", "")[1]) end
|
|
---@cast directory Path
|
|
---@type Path
|
|
local cache_prompt_dir = P.cache_path:joinpath(directory)
|
|
if not cache_prompt_dir:exists() then cache_prompt_dir:mkdir({ parents = true }) end
|
|
|
|
local scanner = Scan.scan_dir(directory:absolute(), { depth = 1, add_dirs = true })
|
|
for _, entry in ipairs(scanner) do
|
|
local file = Path:new(entry)
|
|
if file:is_file() then
|
|
if entry:find("planning") and Prompt.templates.planning == nil then
|
|
Prompt.templates.planning = file:read()
|
|
elseif entry:find("editing") and Prompt.templates.editing == nil then
|
|
Prompt.templates.editing = file:read()
|
|
elseif entry:find("suggesting") and Prompt.templates.suggesting == nil then
|
|
Prompt.templates.suggesting = file:read()
|
|
end
|
|
end
|
|
end
|
|
|
|
Path:new(debug.getinfo(1).source:match("@?(.*/)"):gsub("/lua/avante/path.lua$", "") .. "templates")
|
|
:copy({ destination = cache_prompt_dir, recursive = true })
|
|
|
|
vim.iter(Prompt.templates):filter(function(_, v) return v ~= nil end):each(function(k, v)
|
|
local f = cache_prompt_dir:joinpath(Prompt.get_mode_file(k))
|
|
f:write(v, "w")
|
|
end)
|
|
|
|
return cache_prompt_dir:absolute()
|
|
end
|
|
|
|
---@param mode LlmMode
|
|
Prompt.get_file = function(mode)
|
|
if Prompt.templates[mode] ~= nil then return Prompt.get_mode_file(mode) end
|
|
return string.format("%s.avanterules", mode)
|
|
end
|
|
|
|
---@param path string
|
|
---@param opts TemplateOptions
|
|
Prompt.render_file = function(path, opts) return templates.render(path, opts) end
|
|
|
|
---@param mode LlmMode
|
|
---@param opts TemplateOptions
|
|
Prompt.render_mode = function(mode, opts) return templates.render(Prompt.get_file(mode), opts) end
|
|
|
|
Prompt.initialize = function(directory) templates.initialize(directory) end
|
|
|
|
P.prompts = Prompt
|
|
|
|
local RepoMap = {}
|
|
|
|
-- Get a chat history file name given a buffer
|
|
---@param project_root string
|
|
---@param ext string
|
|
---@return string
|
|
RepoMap.filename = function(project_root, ext)
|
|
-- Replace path separators with double underscores
|
|
local path_with_separators = fn.substitute(project_root, "/", "__", "g")
|
|
-- Replace other non-alphanumeric characters with single underscores
|
|
return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. "." .. ext .. ".repo_map.json"
|
|
end
|
|
|
|
RepoMap.get = function(project_root, ext) return Path:new(P.data_path):joinpath(RepoMap.filename(project_root, ext)) end
|
|
|
|
RepoMap.save = function(project_root, ext, data)
|
|
local file = RepoMap.get(project_root, ext)
|
|
file:write(vim.json.encode(data), "w")
|
|
end
|
|
|
|
RepoMap.load = function(project_root, ext)
|
|
local file = RepoMap.get(project_root, ext)
|
|
if file:exists() then
|
|
local content = file:read()
|
|
return content ~= nil and vim.json.decode(content) or {}
|
|
end
|
|
return nil
|
|
end
|
|
|
|
P.repo_map = RepoMap
|
|
|
|
P.setup = function()
|
|
local history_path = Path:new(Config.history.storage_path)
|
|
if not history_path:exists() then history_path:mkdir({ parents = true }) end
|
|
P.history_path = history_path
|
|
|
|
local cache_path = Path:new(vim.fn.stdpath("cache") .. "/avante")
|
|
if not cache_path:exists() then cache_path:mkdir({ parents = true }) end
|
|
P.cache_path = cache_path
|
|
|
|
local data_path = Path:new(vim.fn.stdpath("data") .. "/avante")
|
|
if not data_path:exists() then data_path:mkdir({ parents = true }) end
|
|
P.data_path = data_path
|
|
|
|
vim.defer_fn(function()
|
|
local ok, module = pcall(require, "avante_templates")
|
|
---@cast module AvanteTemplates
|
|
---@cast ok boolean
|
|
if not ok then return end
|
|
if templates == nil then templates = module end
|
|
end, 1000)
|
|
end
|
|
|
|
P.available = function() return templates ~= nil end
|
|
|
|
P.clear = function()
|
|
P.cache_path:rm({ recursive = true })
|
|
P.history_path:rm({ recursive = true })
|
|
|
|
if not P.cache_path:exists() then P.cache_path:mkdir({ parents = true }) end
|
|
if not P.history_path:exists() then P.history_path:mkdir({ parents = true }) end
|
|
end
|
|
|
|
return P
|