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 8, 2024
1 parent 4a3b278 commit 7ffaaa2
Show file tree
Hide file tree
Showing 13 changed files with 1,008 additions and 43 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ For building binary if you wish to build from source, then `cargo` is required.
build = "make",
-- build = "powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false" -- for windows
dependencies = {
"nvim-treesitter/nvim-treesitter",
"stevearc/dressing.nvim",
"nvim-lua/plenary.nvim",
"MunifTanjim/nui.nvim",
Expand Down
2 changes: 2 additions & 0 deletions crates/avante-templates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct TemplateContext {
ask: bool,
question: String,
code_lang: String,
filepath: String,
file_content: String,
selected_code: Option<String>,
project_context: Option<String>,
Expand All @@ -45,6 +46,7 @@ fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult<
ask => context.ask,
question => context.question,
code_lang => context.code_lang,
filepath => context.filepath,
file_content => context.file_content,
selected_code => context.selected_code,
project_context => context.project_context,
Expand Down
1 change: 1 addition & 0 deletions lua/avante/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ You are an excellent programming expert.
---3. auto_set_highlight_group : Whether to automatically set the highlight group for the current line. Default to true.
---4. support_paste_from_clipboard : Whether to support pasting image from clipboard. This will be determined automatically based whether img-clip is available or not.
behaviour = {
enable_project_context = false,
auto_suggestions = false, -- Experimental stage
auto_set_highlight_group = true,
auto_set_keymaps = true,
Expand Down
3 changes: 3 additions & 0 deletions lua/avante/llm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ M.stream = function(opts)

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
question = original_instructions,
code_lang = opts.code_lang,
filepath = filepath,
file_content = opts.file_content,
selected_code = opts.selected_code,
project_context = opts.project_context,
Expand Down
81 changes: 58 additions & 23 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 @@ -85,19 +85,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 @@ -106,22 +106,53 @@ 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 path string
---@param opts TemplateOptions
N.render_file = function(path, opts) return templates.render(path, opts) end
Prompt.render_file = function(path, opts) return templates.render(path, opts) end

---@param mode LlmMode
---@param opts TemplateOptions
N.render_mode = function(mode, opts) return templates.render(N.get_file(mode), opts) end
Prompt.render_mode = 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 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)
Expand All @@ -132,6 +163,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
2 changes: 2 additions & 0 deletions lua/avante/selection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,12 @@ function Selection:create_editing_input()
end

local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr })
local project_context = Utils.repo_map.get_repo_map()

Llm.stream({
bufnr = code_bufnr,
ask = true,
project_context = vim.json.encode(project_context),
file_content = code_content,
code_lang = filetype,
selected_code = self.selection.content,
Expand Down
3 changes: 3 additions & 0 deletions lua/avante/sidebar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1270,9 +1270,12 @@ function Sidebar:create_input(opts)
Path.history.save(self.code.bufnr, chat_history)
end

local project_context = Utils.repo_map.get_repo_map()

Llm.stream({
bufnr = self.code.bufnr,
ask = opts.ask,
project_context = vim.json.encode(project_context),
file_content = content_with_line_numbers,
code_lang = filetype,
selected_code = selected_code_content_with_line_numbers,
Expand Down
42 changes: 24 additions & 18 deletions lua/avante/suggestion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ function Suggestion:suggest()

local full_response = ""

local project_context = Utils.repo_map.get_repo_map()
print("project_context", vim.inspect(project_context))

Llm.stream({
bufnr = bufnr,
ask = true,
project_context = vim.json.encode(project_context),
file_content = code_content,
code_lang = filetype,
instructions = vim.json.encode(doc),
Expand All @@ -79,24 +83,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
4 changes: 4 additions & 0 deletions lua/avante/templates/_context.avanterules
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{%- if use_xml_format -%}
<filepath>{{filepath}}</filepath>

{%- if selected_code -%}
<context>
```{{code_lang}}
Expand All @@ -19,6 +21,8 @@
</code>
{%- endif %}
{% else %}
FILEPATH: {{filepath}}

{%- if selected_code -%}
CONTEXT:
```{{code_lang}}
Expand Down
36 changes: 36 additions & 0 deletions lua/avante/utils/file.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
local LRUCache = require("avante.utils.lru_cache")

---@class avante.utils.file
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
30 changes: 28 additions & 2 deletions lua/avante/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local lsp = vim.lsp
---@class avante.utils: LazyUtilCore
---@field tokens avante.utils.tokens
---@field root avante.utils.root
---@field repo_map avante.utils.repo_map
local M = {}

setmetatable(M, {
Expand Down Expand Up @@ -444,7 +445,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 @@ -462,7 +463,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 @@ -520,4 +521,29 @@ function M.winline(winid)
return line
end

function M.get_project_root() return M.root.get() 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 7ffaaa2

Please sign in to comment.