Skip to content

Commit

Permalink
feat: repo map
Browse files Browse the repository at this point in the history
  • Loading branch information
yetone committed Sep 3, 2024
1 parent ff316f9 commit 6ec3362
Show file tree
Hide file tree
Showing 8 changed files with 691 additions and 42 deletions.
8 changes: 8 additions & 0 deletions lua/avante/api.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
local Config = require("avante.config")
local Utils = require("avante.utils")
local RepoMap = require("avante.utils.repo_map")
local Path = require("avante.path")

---@class avante.ApiToggle
---@operator call(): boolean
Expand All @@ -14,6 +16,7 @@ local Utils = require("avante.utils")
---@field switch_provider fun(target: string): nil
---@field toggle avante.ApiToggle
---@field get_suggestion fun(): avante.Suggestion | nil
---@field build_project_context fun(): nil

local M = {}

Expand Down Expand Up @@ -115,6 +118,11 @@ M.refresh = function()
sidebar:render()
end

M.build_project_context = function()
local repo_map = RepoMap.repo_map(Utils.get_project_root())
Path.repo_map.save(vim.api.nvim_get_current_buf(), repo_map)
end

return setmetatable(M, {
__index = function(t, k)
local module = require("avante")
Expand Down
5 changes: 5 additions & 0 deletions lua/avante/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ H.commands = function()
function(opts) require("avante.api").edit(vim.trim(opts.args)) end,
{ desc = "avante: edit selected block", nargs = "*" }
)
cmd(
"BuildProjectContext",
function() require("avante.api").build_project_context() end,
{ desc = "avante: build project context", nargs = "*" }
)
cmd("Refresh", function() require("avante.api").refresh() end, { desc = "avante: refresh windows" })
cmd("Build", function(opts)
local args = {}
Expand Down
79 changes: 57 additions & 22 deletions lua/avante/path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ end
H.get_mode_file = function(mode) return string.format("custom.%s.avanterules", mode) end

-- History path
local M = {}
local History = {}

-- Returns the Path to the chat history file for the given buffer.
---@param bufnr integer
---@return Path
M.get = function(bufnr) return Path:new(Config.history.storage_path):joinpath(H.filename(bufnr)) end
History.get = function(bufnr) return Path:new(Config.history.storage_path):joinpath(H.filename(bufnr)) end

-- Loads the chat history for the given buffer.
---@param bufnr integer
M.load = function(bufnr)
local history_file = M.get(bufnr)
History.load = function(bufnr)
local history_file = History.get(bufnr)
if history_file:exists() then
local content = history_file:read()
return content ~= nil and vim.json.decode(content) or {}
Expand All @@ -49,29 +49,29 @@ end
-- Saves the chat history for the given buffer.
---@param bufnr integer
---@param history table
M.save = function(bufnr, history)
local history_file = M.get(bufnr)
History.save = function(bufnr, history)
local history_file = History.get(bufnr)
history_file:write(vim.json.encode(history), "w")
end

P.history = M
P.history = History

-- Prompt path
local N = {}
local Prompt = {}

---@class AvanteTemplates
---@field initialize fun(directory: string): nil
---@field render fun(template: string, context: TemplateOptions): string
local templates = nil

N.templates = { planning = nil, editing = nil, suggesting = 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
N.get = function(bufnr)
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
Expand All @@ -84,19 +84,19 @@ N.get = function(bufnr)
local scanner = Scan.scan_dir(directory:absolute(), { depth = 1, add_dirs = true })
for _, entry in ipairs(scanner) do
local file = Path:new(entry)
if entry:find("planning") and N.templates.planning == nil then
N.templates.planning = file:read()
elseif entry:find("editing") and N.templates.editing == nil then
N.templates.editing = file:read()
elseif entry:find("suggesting") and N.templates.suggesting == nil then
N.templates.suggesting = file:read()
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

Path:new(debug.getinfo(1).source:match("@?(.*/)"):gsub("/lua/avante/path.lua$", "") .. "templates")
:copy({ destination = cache_prompt_dir, recursive = true })

vim.iter(N.templates):filter(function(_, v) return v ~= nil end):each(function(k, v)
vim.iter(Prompt.templates):filter(function(_, v) return v ~= nil end):each(function(k, v)
local f = cache_prompt_dir:joinpath(H.get_mode_file(k))
f:write(v, "w")
end)
Expand All @@ -105,18 +105,49 @@ N.get = function(bufnr)
end

---@param mode LlmMode
N.get_file = function(mode)
if N.templates[mode] ~= nil then return H.get_mode_file(mode) end
Prompt.get_file = function(mode)
if Prompt.templates[mode] ~= nil then return H.get_mode_file(mode) end
return string.format("%s.avanterules", mode)
end

---@param mode LlmMode
---@param opts TemplateOptions
N.render = function(mode, opts) return templates.render(N.get_file(mode), opts) end
Prompt.render = function(mode, opts) return templates.render(Prompt.get_file(mode), opts) end

N.initialize = function(directory) templates.initialize(directory) end
Prompt.initialize = function(directory) templates.initialize(directory) end

P.prompts = N
P.prompts = Prompt

local RepoMap = {}

-- Get a chat history file name given a buffer
---@param bufnr integer
---@return string
RepoMap.filename = function(bufnr)
local root = Utils.root.get({ buf = bufnr })
-- Replace path separators with double underscores
local path_with_separators = fn.substitute(root, "/", "__", "g")
-- Replace other non-alphanumeric characters with single underscores
return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. ".repo_map.json"
end

RepoMap.get = function(bufnr) return Path:new(P.data_path):joinpath(RepoMap.filename(bufnr)) end

RepoMap.save = function(bufnr, data)
local file = RepoMap.get(bufnr)
file:write(vim.json.encode(data), "w")
end

RepoMap.load = function(bufnr)
local file = RepoMap.get(bufnr)
if file:exists() then
local content = file:read()
return content ~= nil and vim.json.decode(content) or {}
end
return {}
end

P.repo_map = RepoMap

P.setup = function()
local history_path = Path:new(Config.history.storage_path)
Expand All @@ -127,6 +158,10 @@ P.setup = function()
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
Expand Down
42 changes: 24 additions & 18 deletions lua/avante/suggestion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local Utils = require("avante.utils")
local Llm = require("avante.llm")
local Highlights = require("avante.highlights")
local Config = require("avante.config")
local Path = require("avante.path")
local api = vim.api
local fn = vim.fn

Expand Down Expand Up @@ -100,8 +101,11 @@ function Suggestion:suggest()

local full_response = ""

local project_context = Path.repo_map.load(bufnr)

Llm.stream({
bufnr = bufnr,
project_context = vim.json.encode(project_context),
file_content = code_content,
code_lang = filetype,
instructions = vim.json.encode(doc),
Expand All @@ -113,24 +117,26 @@ function Suggestion:suggest()
return
end
Utils.debug("full_response: " .. vim.inspect(full_response))
local cursor_row, cursor_col = Utils.get_cursor_pos()
if cursor_row ~= doc.position.row or cursor_col ~= doc.position.col then return end
local ok, suggestions = pcall(vim.json.decode, full_response)
if not ok then
Utils.error("Error while decoding suggestions: " .. full_response, { once = true, title = "Avante" })
return
end
if not suggestions then
Utils.info("No suggestions found", { once = true, title = "Avante" })
return
end
suggestions = vim
.iter(suggestions)
:map(function(s) return { row = s.row, col = s.col, content = Utils.trim_all_line_numbers(s.content) } end)
:totable()
ctx.suggestions = suggestions
ctx.current_suggestion_idx = 1
self:show()
vim.schedule(function()
local cursor_row, cursor_col = Utils.get_cursor_pos()
if cursor_row ~= doc.position.row or cursor_col ~= doc.position.col then return end
local ok, suggestions = pcall(vim.json.decode, full_response)
if not ok then
Utils.error("Error while decoding suggestions: " .. full_response, { once = true, title = "Avante" })
return
end
if not suggestions then
Utils.info("No suggestions found", { once = true, title = "Avante" })
return
end
suggestions = vim
.iter(suggestions)
:map(function(s) return { row = s.row, col = s.col, content = Utils.trim_all_line_numbers(s.content) } end)
:totable()
ctx.suggestions = suggestions
ctx.current_suggestion_idx = 1
self:show()
end)
end,
})
end
Expand Down
35 changes: 35 additions & 0 deletions lua/avante/utils/file.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
local LRUCache = require("avante.utils.lru_cache")

local M = {}

local api = vim.api
local fn = vim.fn

local _file_content_lru_cache = LRUCache:new(60)

api.nvim_create_autocmd("BufWritePost", {
callback = function()
local filepath = api.nvim_buf_get_name(0)
local keys = _file_content_lru_cache:keys()
if vim.tbl_contains(keys, filepath) then
local content = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), "\n")
_file_content_lru_cache:set(filepath, content)
end
end,
})

function M.read_content(filepath)
local cached_content = _file_content_lru_cache:get(filepath)
if cached_content then return cached_content end

local content = fn.readfile(filepath)
if content then
content = table.concat(content, "\n")
_file_content_lru_cache:set(filepath, content)
return content
end

return nil
end

return M
34 changes: 32 additions & 2 deletions lua/avante/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ local api = vim.api
local fn = vim.fn
local lsp = vim.lsp

local RepoMap = require("avante.utils.repo_map")

---@class avante.utils: LazyUtilCore
---@field tokens avante.utils.tokens
---@field root avante.utils.root
Expand Down Expand Up @@ -446,7 +448,7 @@ function M.get_indentation(code) return code:match("^%s*") or "" end
--- remove indentation from code: spaces or tabs
function M.remove_indentation(code) return code:gsub("^%s*", "") end

local function relative_path(absolute)
function M.relative_path(absolute)
local relative = fn.fnamemodify(absolute, ":.")
if string.sub(relative, 0, 1) == "/" then return fn.fnamemodify(absolute, ":t") end
return relative
Expand All @@ -464,7 +466,7 @@ function M.get_doc()
local doc = {
uri = params.textDocument.uri,
version = api.nvim_buf_get_var(0, "changedtick"),
relativePath = relative_path(absolute),
relativePath = M.relative_path(absolute),
insertSpaces = vim.o.expandtab,
tabSize = fn.shiftwidth(),
indentSize = fn.shiftwidth(),
Expand Down Expand Up @@ -514,4 +516,32 @@ function M.debounce(func, delay)
end
end

function M.get_project_root()
local Root = require("avante.utils.root")
return Root.git() or Root.cwd()
end

-- Get recent filepaths in the same project and same file ext
function M.get_recent_filepaths(limit, filenames)
local project_root = M.get_project_root()
local file_ext = fn.expand("%:e")
local oldfiles = vim.v.oldfiles
local recent_files = {}

for _, file in ipairs(oldfiles) do
if vim.startswith(file, project_root) and fn.fnamemodify(file, ":e") == file_ext then
if filenames and #filenames > 0 then
for _, filename in ipairs(filenames) do
if file:find(filename) then table.insert(recent_files, file) end
end
else
table.insert(recent_files, file)
end
if #recent_files >= (limit or 10) then break end
end
end

return recent_files
end

return M
Loading

0 comments on commit 6ec3362

Please sign in to comment.