feat(templates): avanterules filetype support (closes #254) (#466)

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
This commit is contained in:
Aaron Pham 2024-09-03 04:09:13 -04:00 committed by GitHub
parent 054695cc63
commit 4ad913435c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 962 additions and 265 deletions

2
.gitattributes vendored
View File

@ -1,2 +1,4 @@
* text=auto eol=lf * text=auto eol=lf
**/*.lock linguist-generated=true **/*.lock linguist-generated=true
*.avanterules linguist-language=jinja
syntax/jinja.vim linguist-vendored

View File

@ -12,8 +12,10 @@ function Build-FromSource($feature) {
cargo build --release --features=$feature cargo build --release --features=$feature
$targetFile = "avante_tokenizers.dll" $targetTokenizerFile = "avante_tokenizers.dll"
Copy-Item (Join-Path "target\release\libavante_tokenizers.dll") (Join-Path $BuildDir $targetFile) $targetTemplatesFile = "avante_templates.dll"
Copy-Item (Join-Path "target\release\libavante_tokenizers.dll") (Join-Path $BuildDir $targetTokenizerFile)
Copy-Item (Join-Path "target\release\libavante_templates.dll") (Join-Path $BuildDir $targetTemplatesFile)
Remove-Item -Recurse -Force "target" Remove-Item -Recurse -Force "target"
} }

58
Cargo.lock generated
View File

@ -29,6 +29,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "avante-templates"
version = "0.1.0"
dependencies = [
"minijinja",
"mlua",
"serde",
]
[[package]] [[package]]
name = "avante-tokenizers" name = "avante-tokenizers"
version = "0.1.0" version = "0.1.0"
@ -546,6 +555,28 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memo-map"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
[[package]]
name = "minijinja"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7d3e3a3eece1fa4618237ad41e1de855ced47eab705cec1c9a920e1d1c5aad"
dependencies = [
"aho-corasick",
"memo-map",
"self_cell",
"serde",
"serde_json",
"unicase",
"unicode-ident",
"v_htmlescape",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -1047,6 +1078,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "self_cell"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.209" version = "1.0.209"
@ -1244,6 +1281,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"
@ -1328,12 +1374,24 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -10,6 +10,28 @@ version = "0.1.0"
[workspace.dependencies] [workspace.dependencies]
avante-tokenizers = { path = "crates/avante-tokenizers" } avante-tokenizers = { path = "crates/avante-tokenizers" }
avante-templates = { path = "crates/avante-templates" }
minijinja = { version = "2.2.0", features = [
"loader",
"json",
"fuel",
"unicode",
"speedups",
"custom_syntax",
"loop_controls",
] }
mlua = { version = "0.10.0-beta.1", features = [
"module",
"serialize",
], git = "https://github.com/mlua-rs/mlua.git", branch = "main" }
tiktoken-rs = { version = "0.5.9" }
tokenizers = { version = "0.20.0", features = [
"esaxx_fast",
"http",
"unstable_wasm",
"onig",
], default-features = false }
serde = { version = "1.0.209", features = ["derive"] }
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "warn" unsafe_code = "warn"

View File

@ -11,32 +11,40 @@ else
$(error Unsupported operating system: $(UNAME)) $(error Unsupported operating system: $(UNAME))
endif endif
LUA_VERSIONS := luajit lua51 LUA_VERSIONS := luajit lua51 lua52 lua53 lua54
BUILD_DIR := build BUILD_DIR := build
TARGET_LIBRARY ?= all
all: luajit all: luajit
luajit: $(BUILD_DIR)/libavante_tokenizers.$(EXT) define make_definitions
lua51: $(BUILD_DIR)/libavante_tokenizers-lua51.$(EXT) ifeq ($(TARGET_LIBRARY), all)
lua52: $(BUILD_DIR)/libavante_tokenizers-lua52.$(EXT) $1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT) $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT)
lua53: $(BUILD_DIR)/libavante_tokenizers-lua53.$(EXT) else ifeq ($(TARGET_LIBRARY), tokenizers)
lua54: $(BUILD_DIR)/libavante_tokenizers-lua54.$(EXT) $1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT)
else ifeq ($(TARGET_LIBRARY), templates)
define build_from_source $1: $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT)
cargo build --release --features=$1 else
cp target/release/libavante_tokenizers.$(EXT) $(BUILD_DIR)/avante_tokenizers.$(EXT) $$(error TARGET_LIBRARY must be one of all, tokenizers, templates)
endif
endef endef
$(BUILD_DIR)/libavante_tokenizers.$(EXT): $(BUILD_DIR) $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call make_definitions,$(lua_version))))
$(call build_from_source,luajit)
$(BUILD_DIR)/libavante_tokenizers-lua51.$(EXT): $(BUILD_DIR) define build_from_source
$(call build_from_source,lua51) cargo build --release --features=$1 -p avante-$2
$(BUILD_DIR)/libavante_tokenizers-lua52.$(EXT): $(BUILD_DIR) cp target/release/libavante_$2.$(EXT) $(BUILD_DIR)/avante_$2.$(EXT)
$(call build_from_source,lua52) endef
$(BUILD_DIR)/libavante_tokenizers-lua53.$(EXT): $(BUILD_DIR)
$(call build_from_source,lua53) define build_targets
$(BUILD_DIR)/libavante_tokenizers-lua54.$(EXT): $(BUILD_DIR) $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT): $(BUILD_DIR)
$(call build_from_source,lua54) $$(call build_from_source,$1,tokenizers)
$(BUILD_DIR)/libAvanteTemplates-$1.$(EXT): $(BUILD_DIR)
$$(call build_from_source,$1,templates)
endef
$(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_targets,$(lua_version))))
$(BUILD_DIR): $(BUILD_DIR):
mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)

View File

@ -32,7 +32,7 @@ https://github.com/user-attachments/assets/86140bfd-08b4-483d-a887-1b701d9e37dd
opts = { opts = {
-- add any opts here -- add any opts here
}, },
build = ":AvanteBuild", -- This is optional, recommended tho. Also note that this will block the startup for a bit since we are compiling bindings in Rust. build = ":AvanteBuild", -- Also note that this will block the startup for a bit since we are compiling bindings in Rust.
dependencies = { dependencies = {
"stevearc/dressing.nvim", "stevearc/dressing.nvim",
"nvim-lua/plenary.nvim", "nvim-lua/plenary.nvim",
@ -93,7 +93,7 @@ Plug 'yetone/avante.nvim', { 'branch': 'main', 'do': { -> avante#build() }, 'on'
> [!important] > [!important]
> >
> For `avante.tokenizers` to work, make sure to call `require('avante_lib').load()` somewhere when entering the editor. > For `avante.tokenizers` and templates to work, make sure to call `require('avante_lib').load()` somewhere when entering the editor.
> We will leave the users to decide where it fits to do this, as this varies among configurations. (But we do recommend running this after where you set your colorscheme) > We will leave the users to decide where it fits to do this, as this varies among configurations. (But we do recommend running this after where you set your colorscheme)
</details> </details>
@ -147,6 +147,7 @@ require('copilot').setup ({
require('render-markdown').setup ({ require('render-markdown').setup ({
-- use recommended settings from above -- use recommended settings from above
}) })
require('avante_lib').load()
require('avante').setup ({ require('avante').setup ({
-- Your config here! -- Your config here!
}) })
@ -341,6 +342,59 @@ The following key bindings are available for use with `avante.nvim`:
See [highlights.lua](./lua/avante/highlights.lua) for more information See [highlights.lua](./lua/avante/highlights.lua) for more information
## Custom prompts
By default, `avante.nvim` provides three different modes to interact with: `planning`, `editing`, and `suggesting`, followed with three different prompts per mode.
- `planning`: Used with `require("avante").toggle()` on sidebar
- `editing`: Used with `require("avante").edit()` on selection codeblock
- `suggesting`: Used with `require("avante").get_suggestion():suggest()` on Tab flow.
Users can customize the system prompts via `Config.system_prompt`. We recommend calling this in a custom Autocmds depending on your need:
```lua
vim.api.nvim_create_autocmd("User", {
pattern = "ToggleMyPrompt"
callback = function() require("avante.config").override({system_prompt = "MY CUSTOM SYSTEM PROMPT"}) end,
})
vim.keymap.set("n", "<leader>am", function() vim.api.nvim_exec_autocmds("User", { pattern = "ToggleMyPrompt" }) end, { desc = "avante: toggle my prompt" })
```
If one wish to custom prompts for each mode, `avante.nvim` will check for project root based on the given buffer whether it contains
the following patterns: `*.{mode}.avanterules`.
The rules for root hierarchy:
- lsp workspace folders
- lsp root_dir
- root pattern of filename of the current buffer
- root pattern of cwd
<details>
<summary>Example folder structure for custom prompt</summary>
If you have the following structure:
```bash
.
├── .git/
├── typescript.planning.avanterules
├── snippets.editing.avanterules
└── src/
```
- `typescript.planning.avanterules` will be used for `planning` mode
- `snippets.editing.avanterules`` will be used for `editing` mode
- the default `suggesting` prompt from `avante.nvim` will be used for `suggesting` mode.
</details>
> [!important]
>
> `*.avanterules` is a jinja template file, in which will be rendered using [minijinja](https://github.com/mitsuhiko/minijinja). See [templates](https://github.com/yetone/avante.nvim/blob/main/lua/avante/templates) for example on how to extend current templates.
## TODOs ## TODOs
- [x] Chat with current file - [x] Chat with current file
@ -348,7 +402,7 @@ See [highlights.lua](./lua/avante/highlights.lua) for more information
- [x] Chat with the selected block - [x] Chat with the selected block
- [x] Slash commands - [x] Slash commands
- [x] Edit the selected block - [x] Edit the selected block
- [ ] Smart Tab (Cursor Flow) - [x] Smart Tab (Cursor Flow)
- [ ] Chat with project - [ ] Chat with project
- [ ] Chat with selected files - [ ] Chat with selected files
@ -367,12 +421,13 @@ See [wiki](https://github.com/yetone/avante.nvim/wiki) for more recipes and tric
We would like to express our heartfelt gratitude to the contributors of the following open-source projects, whose code has provided invaluable inspiration and reference for the development of avante.nvim: We would like to express our heartfelt gratitude to the contributors of the following open-source projects, whose code has provided invaluable inspiration and reference for the development of avante.nvim:
| Nvim Plugin | License | Functionality | Where did we use | | Nvim Plugin | License | Functionality | Location |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | No License | Diff comparison functionality | https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua | | [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | No License | Diff comparison functionality | [lua/avante/diff.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua) |
| [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 License | Calculation of tokens count | https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua | | [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 License | Calculation of tokens count | [avante/utils/tokens.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua) |
| [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua | | [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | [avante/clipboard.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua) |
| [copilot.lua](https://github.com/zbirenbaum/copilot.lua) | MIT License | Copilot support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/copilot.lua | | [copilot.lua](https://github.com/zbirenbaum/copilot.lua) | MIT License | Copilot support | [avante/providers/copilot.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/copilot.lua) |
| [jinja.vim](https://github.com/HiPhish/jinja.vim) | MIT License | Template filetype support | [syntax/jinja.vim](https://github.com/yetone/avante.nvim/blob/main/syntax/jinja.vim) |
The high quality and ingenuity of these projects' source code have been immensely beneficial throughout our development process. We extend our sincere thanks and respect to the authors and contributors of these projects. It is the selfless dedication of the open-source community that drives projects like avante.nvim forward. The high quality and ingenuity of these projects' source code have been immensely beneficial throughout our development process. We extend our sincere thanks and respect to the authors and contributors of these projects. It is the selfless dedication of the open-source community that drives projects like avante.nvim forward.

View File

@ -0,0 +1,24 @@
[lib]
crate-type = ["cdylib"]
[package]
name = "avante-templates"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
mlua = { workspace = true }
minijinja = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[lints]
workspace = true
[features]
lua51 = ["mlua/lua51"]
lua52 = ["mlua/lua52"]
lua53 = ["mlua/lua53"]
lua54 = ["mlua/lua54"]
luajit = ["mlua/luajit"]

View File

@ -0,0 +1,91 @@
use minijinja::{context, path_loader, Environment};
use mlua::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
struct State<'a> {
environment: Mutex<Option<Environment<'a>>>,
}
impl<'a> State<'a> {
fn new() -> Self {
State {
environment: Mutex::new(None),
}
}
}
#[derive(Serialize, Deserialize)]
struct TemplateContext {
use_xml_format: bool,
ask: bool,
question: String,
code_lang: String,
file_content: String,
selected_code: Option<String>,
project_context: Option<String>,
memory_context: Option<String>,
}
// Given the file name registered after add, the context table in Lua, resulted in a formatted
// Lua string
fn render(state: &State, template: String, context: TemplateContext) -> LuaResult<String> {
let environment = state.environment.lock().unwrap();
match environment.as_ref() {
Some(environment) => {
let template = environment
.get_template(&template)
.map_err(LuaError::external)
.unwrap();
Ok(template
.render(context! {
use_xml_format => context.use_xml_format,
ask => context.ask,
question => context.question,
code_lang => context.code_lang,
file_content => context.file_content,
selected_code => context.selected_code,
project_context => context.project_context,
memory_context => context.memory_context,
})
.map_err(LuaError::external)
.unwrap())
}
None => Err(LuaError::RuntimeError(
"Environment not initialized".to_string(),
)),
}
}
fn initialize(state: &State, directory: String) {
let mut environment_mutex = state.environment.lock().unwrap();
// add directory as a base path for base directory template path
let mut env = Environment::new();
env.set_loader(path_loader(directory));
*environment_mutex = Some(env);
}
#[mlua::lua_module]
fn avante_templates(lua: &Lua) -> LuaResult<LuaTable> {
let core = State::new();
let state = Arc::new(core);
let state_clone = Arc::clone(&state);
let exports = lua.create_table()?;
exports.set(
"initialize",
lua.create_function(move |_, model: String| {
initialize(&state, model);
Ok(())
})?,
)?;
exports.set(
"render",
lua.create_function_mut(move |lua, (template, context): (String, LuaValue)| {
let ctx = lua.from_value(context)?;
render(&state_clone, template, ctx)
})?,
)?;
Ok(exports)
}

View File

@ -12,17 +12,9 @@ license = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.10.0-beta.1", features = [ mlua = { workspace = true }
"module", tiktoken-rs = { workspace = true }
"serialize", tokenizers = { workspace = true }
], git = "https://github.com/mlua-rs/mlua.git", branch = "main" }
tiktoken-rs = "0.5.9"
tokenizers = { version = "0.20.0", features = [
"esaxx_fast",
"http",
"unstable_wasm",
"onig",
], default-features = false }
[features] [features]
lua51 = ["mlua/lua51"] lua51 = ["mlua/lua51"]

View File

@ -68,13 +68,12 @@ fn encode(state: &State, text: String) -> LuaResult<(Vec<usize>, usize, usize)>
} }
} }
fn from_pretrained(state: &State, model: String) -> LuaResult<()> { fn from_pretrained(state: &State, model: String) {
let mut tokenizer_mutex = state.tokenizer.lock().unwrap(); let mut tokenizer_mutex = state.tokenizer.lock().unwrap();
*tokenizer_mutex = Some(match model.as_str() { *tokenizer_mutex = Some(match model.as_str() {
"gpt-4o" => TokenizerType::Tiktoken(Tiktoken::new(model)), "gpt-4o" => TokenizerType::Tiktoken(Tiktoken::new(model)),
_ => TokenizerType::HuggingFace(HuggingFaceTokenizer::new(model)), _ => TokenizerType::HuggingFace(HuggingFaceTokenizer::new(model)),
}); });
Ok(())
} }
#[mlua::lua_module] #[mlua::lua_module]
@ -86,7 +85,10 @@ fn avante_tokenizers(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?; let exports = lua.create_table()?;
exports.set( exports.set(
"from_pretrained", "from_pretrained",
lua.create_function(move |_, model: String| from_pretrained(&state, model))?, lua.create_function(move |_, model: String| {
from_pretrained(&state, model);
Ok(())
})?,
)?; )?;
exports.set( exports.set(
"encode", "encode",

View File

@ -18,6 +18,13 @@ M.defaults = {
-- For most providers that we support we will determine this automatically. -- For most providers that we support we will determine this automatically.
-- If you wish to use a given implementation, then you can override it here. -- If you wish to use a given implementation, then you can override it here.
tokenizer = "tiktoken", tokenizer = "tiktoken",
---@alias AvanteSystemPrompt string
-- Default system prompt. Users can override this with their own prompt
-- You can use `require('avante.config').override({system_prompt = "MY_SYSTEM_PROMPT"}) conditionally
-- in your own autocmds to do it per directory, or that fit your needs.
system_prompt = [[
You are an excellent programming expert.
]],
---@type AvanteSupportedProvider ---@type AvanteSupportedProvider
openai = { openai = {
endpoint = "https://api.openai.com/v1", endpoint = "https://api.openai.com/v1",
@ -309,6 +316,7 @@ M.BASE_PROVIDER_KEYS = {
"local", "local",
"_shellenv", "_shellenv",
"tokenizer_id", "tokenizer_id",
"use_xml_format",
} }
---@return {width: integer, height: integer} ---@return {width: integer, height: integer}

View File

@ -234,6 +234,15 @@ H.autocmds = function()
-- automatically setup Avante filetype to markdown -- automatically setup Avante filetype to markdown
vim.treesitter.language.register("markdown", "Avante") vim.treesitter.language.register("markdown", "Avante")
vim.filetype.add({
extension = {
["avanterules"] = "jinja",
},
pattern = {
["%.avanterules%.[%w_.-]+"] = "jinja",
},
})
end end
---@param current boolean? false to disable setting current, otherwise use this to track across tabs. ---@param current boolean? false to disable setting current, otherwise use this to track across tabs.
@ -359,16 +368,6 @@ end)
---@param opts? avante.Config ---@param opts? avante.Config
function M.setup(opts) function M.setup(opts)
if vim.fn.has("nvim-0.10") == 0 then
vim.api.nvim_echo({
{ "Avante requires at least nvim-0.10", "ErrorMsg" },
{ "Please upgrade your neovim version", "WarningMsg" },
{ "Press any key to exit", "ErrorMsg" },
}, true, {})
vim.fn.getchar()
vim.cmd([[quit]])
end
---PERF: we can still allow running require("avante").setup() multiple times to override config if users wish to ---PERF: we can still allow running require("avante").setup() multiple times to override config if users wish to
---but most of the other functionality will only be called once from lazy.nvim ---but most of the other functionality will only be called once from lazy.nvim
Config.setup(opts) Config.setup(opts)

View File

@ -4,6 +4,7 @@ local curl = require("plenary.curl")
local Utils = require("avante.utils") local Utils = require("avante.utils")
local Config = require("avante.config") local Config = require("avante.config")
local Path = require("avante.path")
local P = require("avante.providers") local P = require("avante.providers")
---@class avante.LLM ---@class avante.LLM
@ -13,136 +14,32 @@ M.CANCEL_PATTERN = "AvanteLLMEscape"
------------------------------Prompt and type------------------------------ ------------------------------Prompt and type------------------------------
---@alias AvanteSystemPrompt string
local system_prompt = [[
You are an excellent programming expert.
]]
-- Copy from: https://github.com/Doriandarko/claude-engineer/blob/15c94963cbf9d01b8ae7bbb5d42d7025aa0555d5/main.py#L276
---@alias AvanteBasePrompt string
local planning_mode_user_prompt_tpl = [[
Your primary task is to suggest code modifications with precise line number ranges. Follow these instructions meticulously:
1. Carefully analyze the original code, paying close attention to its structure and line numbers. Line numbers start from 1 and include ALL lines, even empty ones.
2. When suggesting modifications:
a. Use the language in the question to reply. If there are non-English parts in the question, use the language of those parts.
b. Explain why the change is necessary or beneficial.
c. If an image is provided, make sure to use the image in conjunction with the code snippet.
d. Provide the exact code snippet to be replaced using this format:
Replace lines: {{start_line}}-{{end_line}}
```{{language}}
{{suggested_code}}
```
3. Crucial guidelines for suggested code snippets:
- The content regarding line numbers MUST strictly follow the format "Replace lines: {{start_line}}-{{end_line}}". Do not be lazy!
- Only apply the change(s) suggested by the most recent assistant message (before your generation).
- Do not make any unrelated changes to the code.
- Produce a valid full rewrite of the entire original file without skipping any lines. Do not be lazy!
- Do not arbitrarily delete pre-existing comments/empty Lines.
- Do not omit large parts of the original file for no reason.
- Do not omit any needed changes from the requisite messages/code blocks.
- If there is a clicked code block, bias towards just applying that (and applying other changes implied).
- Please keep your suggested code changes minimal, and do not include irrelevant lines in the code snippet.
- Maintain the SAME indentation in the returned code as in the source code
4. Crucial guidelines for line numbers:
- The range {{start_line}}-{{end_line}} is INCLUSIVE. Both start_line and end_line are included in the replacement.
- Count EVERY line, including empty lines and comments lines, comments. Do not be lazy!
- Use the same number for start and end lines for single-line changes.
- For multi-line changes, ensure the range covers ALL affected lines, from first to last.
- Double-check that your line numbers align perfectly with the original code structure.
5. Final check:
- Review all suggestions, ensuring each line number is correct, especially the start_line and end_line.
- Confirm that no unrelated code is accidentally modified or deleted.
- Verify that the start_line and end_line correctly include all intended lines for replacement.
- Perform a final alignment check to ensure your line numbers haven't shifted, especially the start_line.
- Double-check that your line numbers align perfectly with the original code structure.
- DO NOT return the complete modified code with applied changes!
Remember: Accurate line numbers are CRITICAL. The range start_line to end_line must include ALL lines to be replaced, from the very first to the very last. Double-check every range before finalizing your response, paying special attention to the start_line to ensure it hasn't shifted down. Ensure your line numbers match the original code structure without any overall shift.
]]
local editing_mode_user_prompt_tpl = [[
Your task is to modify the provided code according to the user's request. Follow these instructions precisely:
1. Return ONLY the complete modified code.
2. Do not include any explanations, comments, or line numbers in your response.
3. Ensure the returned code is complete and can be directly used as a replacement for the original code.
4. Preserve the original structure, indentation, and formatting of the code as much as possible.
5. Do not omit any parts of the code, even if they are unchanged.
6. Maintain the SAME indentation in the returned code as in the source code
7. Do NOT include three backticks: ```
8. Only return the new code snippets to be updated, DO NOT return the entire file content.
Remember: Your response should contain ONLY the modified code, ready to be used as a direct replacement for the original file.
]]
local suggesting_mode_user_prompt_tpl = [[
Your task is to suggest code modifications at the cursor position. Follow these instructions meticulously:
1. Carefully analyze the original code, paying close attention to its structure and the cursor position.
2. You must follow this json format when suggesting modifications:
[
{
"row": ${row},
"col": ${column},
"content": "Your suggested code here"
}
]
3. When suggesting suggested code:
- Each element in the returned list is a COMPLETE and INDEPENDENT code snippet.
- MUST be a valid json format. Don't be lazy!
- Only return the new code to be inserted.
- Your returned code should not overlap with the original code in any way. Don't be lazy!
- Please strictly check the code around the position and ensure that the complete code after insertion is correct. Don't be lazy!
- Do not return the entire file content or any surrounding code.
- Do not include any explanations, comments, or line numbers in your response.
- Ensure the suggested code fits seamlessly with the existing code structure and indentation.
- If there are no recommended modifications, return an empty list.
Remember: Return ONLY the suggested code snippet, without any additional formatting or explanation.
]]
local group = api.nvim_create_augroup("avante_llm", { clear = true }) local group = api.nvim_create_augroup("avante_llm", { clear = true })
---@class StreamOptions ---@alias LlmMode "planning" | "editing" | "suggesting"
---@field file_content string ---
---@class TemplateOptions
---@field use_xml_format boolean
---@field ask boolean
---@field question string
---@field code_lang string ---@field code_lang string
---@field file_content string
---@field selected_code string | nil ---@field selected_code string | nil
---@field instructions string
---@field project_context string | nil ---@field project_context string | nil
---@field memory_context string | nil ---@field memory_context string | nil
---@field full_file_contents_context string | nil ---
---@field mode "planning" | "editing" | "suggesting" ---@class StreamOptions: TemplateOptions
---@field bufnr integer
---@field instructions string
---@field mode LlmMode
---@field on_chunk AvanteChunkParser ---@field on_chunk AvanteChunkParser
---@field on_complete AvanteCompleteParser ---@field on_complete AvanteCompleteParser
---@param opts StreamOptions ---@param opts StreamOptions
M.stream = function(opts) M.stream = function(opts)
local mode = opts.mode or "planning" local mode = opts.mode or "planning"
local provider = Config.provider ---@type AvanteProviderFunctor
local Provider = P[Config.provider]
local user_prompt_tpl = planning_mode_user_prompt_tpl
if mode == "editing" then
user_prompt_tpl = editing_mode_user_prompt_tpl
elseif mode == "suggesting" then
user_prompt_tpl = suggesting_mode_user_prompt_tpl
end
-- Check if the instructions contains an image path -- Check if the instructions contains an image path
local image_paths = {} local image_paths = {}
@ -159,52 +56,30 @@ M.stream = function(opts)
original_instructions = table.concat(lines, "\n") original_instructions = table.concat(lines, "\n")
end end
local user_prompts = {} Path.prompts.initialize(Path.prompts.get(opts.bufnr))
local user_prompt = Path.prompts.render(mode, {
use_xml_format = Provider.use_xml_format,
ask = true, -- TODO: add mode without ask instruction
question = original_instructions,
code_lang = opts.code_lang,
file_content = opts.file_content,
selected_code = opts.selected_code,
project_context = opts.project_context,
memory_context = opts.memory_context,
})
if opts.selected_code and opts.selected_code ~= "" then Utils.debug(user_prompt)
table.insert(
user_prompts,
string.format("<code_context>```%s\n%s\n```</code_context>", opts.code_lang, opts.file_content)
)
table.insert(user_prompts, string.format("<code>```%s\n%s\n```</code>", opts.code_lang, opts.selected_code))
else
table.insert(user_prompts, string.format("<code>```%s\n%s\n```</code>", opts.code_lang, opts.file_content))
end
if opts.project_context then
table.insert(user_prompts, string.format("<project_context>%s</project_context>", opts.project_context))
end
if opts.memory_context then
table.insert(user_prompts, string.format("<memory_context>%s</memory_context>", opts.memory_context))
end
if opts.full_file_contents_context then
table.insert(
user_prompts,
string.format("<full_file_contents_context>%s</full_file_contents_context>", opts.full_file_contents_context)
)
end
table.insert(user_prompts, "<question>" .. original_instructions .. "</question>")
local user_prompt = user_prompt_tpl:gsub("%${(.-)}", opts)
table.insert(user_prompts, user_prompt)
---@type AvantePromptOptions ---@type AvantePromptOptions
local code_opts = { local code_opts = {
system_prompt = system_prompt, system_prompt = Config.system_prompt,
user_prompts = user_prompts, user_prompt = user_prompt,
image_paths = image_paths, image_paths = image_paths,
} }
---@type string ---@type string
local current_event_state = nil local current_event_state = nil
---@type AvanteProviderFunctor
local Provider = P[provider]
---@type AvanteHandlerOptions ---@type AvanteHandlerOptions
local handler_opts = { on_chunk = opts.on_chunk, on_complete = opts.on_complete } local handler_opts = { on_chunk = opts.on_chunk, on_complete = opts.on_complete }
---@type AvanteCurlOutput ---@type AvanteCurlOutput
@ -244,7 +119,7 @@ M.stream = function(opts)
return return
end end
vim.schedule(function() vim.schedule(function()
if Config.options[provider] == nil and Provider.parse_stream_data ~= nil then if Config.options[Config.provider] == nil and Provider.parse_stream_data ~= nil then
if Provider.parse_response ~= nil then if Provider.parse_response ~= nil then
Utils.warn( Utils.warn(
"parse_stream_data and parse_response_data are mutually exclusive, and thus parse_response_data will be ignored. Make sure that you handle the incoming data correctly.", "parse_stream_data and parse_response_data are mutually exclusive, and thus parse_response_data will be ignored. Make sure that you handle the incoming data correctly.",

View File

@ -1,5 +1,7 @@
local fn, api = vim.fn, vim.api local fn, api = vim.fn, vim.api
local Utils = require("avante.utils")
local Path = require("plenary.path") local Path = require("plenary.path")
local Scan = require("plenary.scandir")
local Config = require("avante.config") local Config = require("avante.config")
---@class avante.Path ---@class avante.Path
@ -7,9 +9,10 @@ local Config = require("avante.config")
---@field cache_path Path ---@field cache_path Path
local P = {} local P = {}
local M = {} -- Helpers
local H = {} local H = {}
-- Get a chat history file name given a buffer
---@param bufnr integer ---@param bufnr integer
---@return string ---@return string
H.filename = function(bufnr) H.filename = function(bufnr)
@ -20,12 +23,23 @@ H.filename = function(bufnr)
return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. ".json" return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. ".json"
end end
-- Given a mode, return the file name for the custom prompt.
---@param mode LlmMode
H.get_mode_file = function(mode)
return string.format("custom.%s.avanterules", mode)
end
-- History path
local M = {}
-- Returns the Path to the chat history file for the given buffer.
---@param bufnr integer ---@param bufnr integer
---@return Path ---@return Path
M.get = function(bufnr) M.get = function(bufnr)
return Path:new(Config.history.storage_path):joinpath(H.filename(bufnr)) return Path:new(Config.history.storage_path):joinpath(H.filename(bufnr))
end end
-- Loads the chat history for the given buffer.
---@param bufnr integer ---@param bufnr integer
M.load = function(bufnr) M.load = function(bufnr)
local history_file = M.get(bufnr) local history_file = M.get(bufnr)
@ -36,6 +50,7 @@ M.load = function(bufnr)
return {} return {}
end end
-- Saves the chat history for the given buffer.
---@param bufnr integer ---@param bufnr integer
---@param history table ---@param history table
M.save = function(bufnr, history) M.save = function(bufnr, history)
@ -45,6 +60,83 @@ end
P.history = M P.history = M
-- Prompt path
local N = {}
---@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 }
-- 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)
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 }))
---@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 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()
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)
local f = cache_prompt_dir:joinpath(H.get_mode_file(k))
f:write(v, "w")
end)
return cache_prompt_dir:absolute()
end
---@param mode LlmMode
N.get_file = function(mode)
if N.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
N.initialize = function(directory)
templates.initialize(directory)
end
P.prompts = N
P.setup = function() P.setup = function()
local history_path = Path:new(Config.history.storage_path) local history_path = Path:new(Config.history.storage_path)
if not history_path:exists() then if not history_path:exists() then
@ -57,6 +149,26 @@ P.setup = function()
cache_path:mkdir({ parents = true }) cache_path:mkdir({ parents = true })
end end
P.cache_path = cache_path P.cache_path = cache_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 })
end end
return P return P

View File

@ -7,13 +7,13 @@ local M = {}
M.api_key_name = "ANTHROPIC_API_KEY" M.api_key_name = "ANTHROPIC_API_KEY"
M.tokenizer_id = "gpt-4o" M.tokenizer_id = "gpt-4o"
M.use_xml_format = true
---@param prompt_opts AvantePromptOptions M.parse_message = function(opts)
M.parse_message = function(prompt_opts)
local message_content = {} local message_content = {}
if Clipboard.support_paste_image() and prompt_opts.image_paths then if Clipboard.support_paste_image() and opts.image_paths then
for _, image_path in ipairs(prompt_opts.image_paths) do for _, image_path in ipairs(opts.image_paths) do
table.insert(message_content, { table.insert(message_content, {
type = "image", type = "image",
source = { source = {
@ -25,32 +25,15 @@ M.parse_message = function(prompt_opts)
end end
end end
local user_prompts_with_length = {} local user_prompt_obj = {
for idx, user_prompt in ipairs(prompt_opts.user_prompts) do type = "text",
table.insert(user_prompts_with_length, { idx = idx, length = Utils.tokens.calculate_tokens(user_prompt) }) text = opts.user_prompt,
}
if Utils.tokens.calculate_tokens(opts.user_prompt) then
user_prompt_obj.cache_control = { type = "ephemeral" }
end end
table.sort(user_prompts_with_length, function(a, b) table.insert(message_content, user_prompt_obj)
return a.length > b.length
end)
local top_three = {}
for i = 1, math.min(3, #user_prompts_with_length) do
top_three[user_prompts_with_length[i].idx] = true
end
for idx, prompt_data in ipairs(prompt_opts.user_prompts) do
local user_prompt_obj = {
type = "text",
text = prompt_data,
}
if top_three[idx] then
user_prompt_obj.cache_control = { type = "ephemeral" }
end
table.insert(message_content, user_prompt_obj)
end
return { return {
{ {

View File

@ -32,11 +32,9 @@ M.api_key_name = "CO_API_KEY"
M.tokenizer_id = "CohereForAI/c4ai-command-r-plus-08-2024" M.tokenizer_id = "CohereForAI/c4ai-command-r-plus-08-2024"
M.parse_message = function(opts) M.parse_message = function(opts)
local user_prompt = table.concat(opts.user_prompts, "\n\n")
return { return {
preamble = opts.system_prompt, preamble = opts.system_prompt,
message = user_prompt, message = opts.user_prompt,
} }
end end

View File

@ -132,7 +132,7 @@ M.tokenizer_id = "gpt-4o"
M.parse_message = function(opts) M.parse_message = function(opts)
return { return {
{ role = "system", content = opts.system_prompt }, { role = "system", content = opts.system_prompt },
{ role = "user", content = table.concat(opts.user_prompts, "\n\n") }, { role = "user", content = opts.user_prompt },
} }
end end

View File

@ -25,11 +25,7 @@ M.parse_message = function(opts)
end end
-- insert a part into parts -- insert a part into parts
for _, user_prompt in ipairs(opts.user_prompts) do table.insert(message_content, { text = opts.user_prompt })
table.insert(message_content, {
text = user_prompt,
})
end
return { return {
systemInstruction = { systemInstruction = {

View File

@ -10,7 +10,7 @@ local Dressing = require("avante.ui.dressing")
--- ---
---@class AvantePromptOptions: table<[string], string> ---@class AvantePromptOptions: table<[string], string>
---@field system_prompt string ---@field system_prompt string
---@field user_prompts string[] ---@field user_prompt string
---@field image_paths? string[] ---@field image_paths? string[]
--- ---
---@class AvanteBaseMessage ---@class AvanteBaseMessage
@ -70,6 +70,7 @@ local Dressing = require("avante.ui.dressing")
---@field has fun(): boolean ---@field has fun(): boolean
---@field api_key_name string ---@field api_key_name string
---@field tokenizer_id string | "gpt-4o" ---@field tokenizer_id string | "gpt-4o"
---@field use_xml_format boolean
---@field model? string ---@field model? string
---@field parse_api_key fun(): string | nil ---@field parse_api_key fun(): string | nil
---@field parse_stream_data? AvanteStreamParser ---@field parse_stream_data? AvanteStreamParser
@ -275,6 +276,10 @@ M = setmetatable(M, {
t[k].tokenizer_id = "gpt-4o" t[k].tokenizer_id = "gpt-4o"
end end
if t[k].use_xml_format == nil then
t[k].use_xml_format = false
end
if t[k].has == nil then if t[k].has == nil then
t[k].has = function() t[k].has = function()
return E.parse_envvar(t[k]) ~= nil return E.parse_envvar(t[k]) ~= nil

View File

@ -30,12 +30,10 @@ M.tokenizer_id = "gpt-4o"
---@param opts AvantePromptOptions ---@param opts AvantePromptOptions
M.get_user_message = function(opts) M.get_user_message = function(opts)
return table.concat(opts.user_prompts, "\n\n") return opts.user_prompt
end end
M.parse_message = function(opts) M.parse_message = function(opts)
local user_prompt = table.concat(opts.user_prompts, "\n\n")
---@type string | OpenAIMessage[] ---@type string | OpenAIMessage[]
local user_content local user_content
if Config.behaviour.support_paste_from_clipboard and opts.image_paths and #opts.image_paths > 0 then if Config.behaviour.support_paste_from_clipboard and opts.image_paths and #opts.image_paths > 0 then
@ -48,9 +46,9 @@ M.parse_message = function(opts)
}, },
}) })
end end
table.insert(user_content, { type = "text", text = user_prompt }) table.insert(user_content, { type = "text", text = opts.user_prompt })
else else
user_content = user_prompt user_content = opts.user_prompt
end end
return { return {

View File

@ -414,6 +414,7 @@ function Selection:create_editing_input()
local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr }) local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr })
Llm.stream({ Llm.stream({
bufnr = code_bufnr,
file_content = code_content, file_content = code_content,
code_lang = filetype, code_lang = filetype,
selected_code = self.selection.content, selected_code = self.selection.content,

View File

@ -1346,6 +1346,7 @@ function Sidebar:create_input()
end end
Llm.stream({ Llm.stream({
bufnr = self.code.bufnr,
file_content = content_with_line_numbers, file_content = content_with_line_numbers,
code_lang = filetype, code_lang = filetype,
selected_code = selected_code_content_with_line_numbers, selected_code = selected_code_content_with_line_numbers,

View File

@ -111,6 +111,7 @@ function Suggestion:suggest()
local full_response = "" local full_response = ""
Llm.stream({ Llm.stream({
bufnr = bufnr,
file_content = code_content, file_content = code_content,
code_lang = filetype, code_lang = filetype,
instructions = vim.json.encode(doc), instructions = vim.json.encode(doc),

View File

@ -0,0 +1,22 @@
{% extends "planning.avanterules" %}
{% block user_prompt %}
Your task is to modify the provided code according to the user's request. Follow these instructions precisely:
1. Return ONLY the complete modified code.
2. Do not include any explanations, comments, or line numbers in your response.
3. Ensure the returned code is complete and can be directly used as a replacement for the original code.
4. Preserve the original structure, indentation, and formatting of the code as much as possible.
5. Do not omit any parts of the code, even if they are unchanged.
6. Maintain the SAME indentation in the returned code as in the source code
7. DO NOT include three backticks: {%raw%}```{%endraw%} in your suggestion. Treat the suggested code AS IS.
8. Only return the new code snippets to be updated, DO NOT return the entire file content.
Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file.
{% endblock %}

View File

@ -0,0 +1,125 @@
{# Uses https://mitsuhiko.github.io/minijinja-playground/ for testing:
{
"ask": true,
"use_xml_format": true,
"question": "Refactor to include tab flow",
"code_lang": "lua",
"file_content": "local Config = require('avante.config')"
}
#}
{%- if use_xml_format -%}
{%- if selected_code -%}
<context>
```{{code_lang}}
{{file_content}}
```
</context>
<code>
```{{code_lang}}
{{selected_code}}
```
</code>
{%- else -%}
<code>
```{{code_lang}}
{{file_content}}
```
</code>
{%- endif %}{%- if project_context -%}
<project_context>
{{project_context}}
</project_context>
{%- endif %}{%- if memory_context -%}
<memory_context>
{{memory_context}}
</memory_context>
{%- endif %}
{% else %}
{%- if selected_code -%}
CONTEXT:
```{{code_lang}}
{{file_content}}
```
CODE:
```{{code_lang}}
{{selected_code}}
```
{%- else -%}
CODE:
```{{code_lang}}
{{file_content}}
```
{%- endif %}{%- if project_context -%}
PROJECT CONTEXT:
{{project_context}}
{%- endif %}{%- if memory_context -%}
MEMORY CONTEXT:
{{memory_context}}
{%- endif %}{%- endif %}{%- if ask %}
{%- if not use_xml_format %}
INSTRUCTION: {% else %}
<instruction>{% endif -%}
{% block user_prompt %}
Your primary task is to suggest code modifications with precise line number ranges. Follow these instructions meticulously:
1. Carefully analyze the original code, paying close attention to its structure and line numbers. Line numbers start from 1 and include ALL lines, even empty ones.
2. When suggesting modifications:
a. Use the language in the question to reply. If there are non-English parts in the question, use the language of those parts.
b. Explain why the change is necessary or beneficial.
c. If an image is provided, make sure to use the image in conjunction with the code snippet.
d. Provide the exact code snippet to be replaced using this format:
{% raw %}
Replace lines: {{start_line}}-{{end_line}}
```{{language}}
{{suggested_code}}
```
{% endraw %}
3. Crucial guidelines for suggested code snippets:
- The content regarding line numbers MUST strictly follow the format "Replace lines: {{start_line}}-{{end_line}}". Do not be lazy!
- Only apply the change(s) suggested by the most recent assistant message (before your generation).
- Do not make any unrelated changes to the code.
- Produce a valid full rewrite of the entire original file without skipping any lines. Do not be lazy!
- Do not arbitrarily delete pre-existing comments/empty Lines.
- Do not omit large parts of the original file for no reason.
- Do not omit any needed changes from the requisite messages/code blocks.
- If there is a clicked code block, bias towards just applying that (and applying other changes implied).
- Please keep your suggested code changes minimal, and do not include irrelevant lines in the code snippet.
- Maintain the SAME indentation in the returned code as in the source code
4. Crucial guidelines for line numbers:
- The range {{start_line}}-{{end_line}} is INCLUSIVE. Both start_line and end_line are included in the replacement.
- Count EVERY line, including empty lines and comments lines, comments. Do not be lazy!
- Use the same number for start and end lines for single-line changes.
- For multi-line changes, ensure the range covers ALL affected lines, from first to last.
- Double-check that your line numbers align perfectly with the original code structure.
5. Final check:
- Review all suggestions, ensuring each line number is correct, especially the start_line and end_line.
- Confirm that no unrelated code is accidentally modified or deleted.
- Verify that the start_line and end_line correctly include all intended lines for replacement.
- Perform a final alignment check to ensure your line numbers haven't shifted, especially the start_line.
- Double-check that your line numbers align perfectly with the original code structure.
- DO NOT return the complete modified code with applied changes!
Remember that ACCURATE line numbers are CRITICAL. The range {%raw%}{{start_line}}{%endraw%} to {%raw%}{{end_line}}{%endraw%} must include ALL LINES to be replaced. Double-check ALL RANGES before finalizing your response, and MAKE SURE THAT {%raw%}{{start_line}}{%endraw%} hasn't been shifted down. ENSURE line numbers MATCH the original code structure and indentation ARE PRESERVED.
{% endblock %}
{%- if use_xml_format -%}
</instruction>
<question>{{question}}</question>
{%- else %}
QUESTION:
{{question}}
{%- endif %}
{% else %}
{% if use_xml_format -%}
<question>{{question}}</question>
{% else %}
QUESTION:
{{question}}
{%- endif %}
{%- endif %}

View File

@ -0,0 +1,32 @@
{% extends "planning.avanterules" %}
{% block user_prompt %}
Your task is to suggest code modifications at the cursor position. Follow these instructions meticulously:
1. Carefully analyze the original code, paying close attention to its structure and the cursor position.
2. You must follow this json format when suggesting modifications:
{% raw %}
[
{
"row": ${row},
"col": ${column},
"content": "Your suggested code here"
}
]
{% endraw %}
3. When suggesting suggested code:
- Each element in the returned list is a COMPLETE and INDEPENDENT code snippet.
- MUST be a valid json format. Don't be lazy!
- Only return the new code to be inserted.
- Your returned code should not overlap with the original code in any way. Don't be lazy!
- Please strictly check the code around the position and ensure that the complete code after insertion is correct. Don't be lazy!
- Do not return the entire file content or any surrounding code.
- Do not include any explanations, comments, or line numbers in your response.
- Ensure the suggested code fits seamlessly with the existing code structure and indentation.
- If there are no recommended modifications, return an empty list.
Remember to ONLY RETURN the suggested code snippet, without any additional formatting or explanation.
{% endblock %}

View File

@ -9,14 +9,19 @@ local M = {}
---@param model "gpt-4o" | string ---@param model "gpt-4o" | string
M.setup = function(model) M.setup = function(model)
local ok, core = pcall(require, "avante_tokenizers") vim.defer_fn(function()
if not ok then local ok, core = pcall(require, "avante_tokenizers")
return if not ok then
end return
---@cast core AvanteTokenizer end
if tokenizers == nil then
tokenizers = core ---@cast core AvanteTokenizer
end if tokenizers == nil then
tokenizers = core
end
core.from_pretrained(model)
end, 1000)
local HF_TOKEN = os.getenv("HF_TOKEN") local HF_TOKEN = os.getenv("HF_TOKEN")
if HF_TOKEN == nil and model ~= "gpt-4o" then if HF_TOKEN == nil and model ~= "gpt-4o" then
@ -26,9 +31,6 @@ M.setup = function(model)
) )
end end
vim.env.HF_HUB_DISABLE_PROGRESS_BARS = 1 vim.env.HF_HUB_DISABLE_PROGRESS_BARS = 1
---@cast core AvanteTokenizer
core.from_pretrained(model)
end end
M.available = function() M.available = function()

View File

@ -4,6 +4,7 @@ local lsp = vim.lsp
---@class avante.utils: LazyUtilCore ---@class avante.utils: LazyUtilCore
---@field tokens avante.utils.tokens ---@field tokens avante.utils.tokens
---@field root avante.utils.root
local M = {} local M = {}
setmetatable(M, { setmetatable(M, {
@ -30,6 +31,10 @@ M.has = function(plugin)
return package.loaded[plugin] ~= nil return package.loaded[plugin] ~= nil
end end
M.is_win = function()
return jit.os:find("Windows") ~= nil
end
---@return "linux" | "darwin" | "windows" ---@return "linux" | "darwin" | "windows"
M.get_os_name = function() M.get_os_name = function()
local os_name = vim.uv.os_uname().sysname local os_name = vim.uv.os_uname().sysname
@ -254,8 +259,34 @@ function M.get_hl(name)
return api.nvim_get_hl(0, { name = name }) return api.nvim_get_hl(0, { name = name })
end end
M.lsp = {}
---@alias vim.lsp.Client.filter {id?: number, bufnr?: number, name?: string, method?: string, filter?:fun(client: vim.lsp.Client):boolean}
---@param opts? vim.lsp.Client.filter
---@return vim.lsp.Client[]
M.lsp.get_clients = function(opts)
---@type vim.lsp.Client[]
local ret = vim.lsp.get_clients(opts)
return (opts and opts.filter) and vim.tbl_filter(opts.filter, ret) or ret
end
--- vendor from lazy.nvim for early access and override --- vendor from lazy.nvim for early access and override
---@param path string
---@return string
function M.norm(path)
if path:sub(1, 1) == "~" then
local home = vim.uv.os_homedir()
if home:sub(-1) == "\\" or home:sub(-1) == "/" then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end
path = path:gsub("\\", "/"):gsub("/+", "/")
return path:sub(-1) == "/" and path:sub(1, -2) or path
end
---@param msg string|string[] ---@param msg string|string[]
---@param opts? LazyNotifyOpts ---@param opts? LazyNotifyOpts
function M.notify(msg, opts) function M.notify(msg, opts)

161
lua/avante/utils/root.lua Normal file
View File

@ -0,0 +1,161 @@
-- COPIED and MODIFIED from https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/util/root.lua
local Utils = require("avante.utils")
---@class avante.utils.root
---@overload fun(): string
local M = setmetatable({}, {
__call = function(m)
return m.get()
end,
})
---@class AvanteRoot
---@field paths string[]
---@field spec AvanteRootSpec
---@alias AvanteRootFn fun(buf: number): (string|string[])
---@alias AvanteRootSpec string|string[]|AvanteRootFn
---@type AvanteRootSpec[]
M.spec = { "lsp", { ".git", "lua" }, "cwd" }
M.detectors = {}
function M.detectors.cwd()
return { vim.uv.cwd() }
end
---@param buf number
function M.detectors.lsp(buf)
local bufpath = M.bufpath(buf)
if not bufpath then
return {}
end
local roots = {} ---@type string[]
for _, client in pairs(Utils.lsp.get_clients({ bufnr = buf })) do
local workspace = client.config.workspace_folders
for _, ws in pairs(workspace or {}) do
roots[#roots + 1] = vim.uri_to_fname(ws.uri)
end
if client.root_dir then
roots[#roots + 1] = client.root_dir
end
end
return vim.tbl_filter(function(path)
path = Utils.norm(path)
return path and bufpath:find(path, 1, true) == 1
end, roots)
end
---@param patterns string[]|string
function M.detectors.pattern(buf, patterns)
patterns = type(patterns) == "string" and { patterns } or patterns
local path = M.bufpath(buf) or vim.uv.cwd()
local pattern = vim.fs.find(function(name)
for _, p in ipairs(patterns) do
if name == p then
return true
end
if p:sub(1, 1) == "*" and name:find(vim.pesc(p:sub(2)) .. "$") then
return true
end
end
return false
end, { path = path, upward = true })[1]
return pattern and { vim.fs.dirname(pattern) } or {}
end
function M.bufpath(buf)
return M.realpath(vim.api.nvim_buf_get_name(assert(buf)))
end
function M.cwd()
return M.realpath(vim.uv.cwd()) or ""
end
function M.realpath(path)
if path == "" or path == nil then
return nil
end
path = vim.uv.fs_realpath(path) or path
return Utils.norm(path)
end
---@param spec AvanteRootSpec
---@return AvanteRootFn
function M.resolve(spec)
if M.detectors[spec] then
return M.detectors[spec]
elseif type(spec) == "function" then
return spec
end
return function(buf)
return M.detectors.pattern(buf, spec)
end
end
---@param opts? { buf?: number, spec?: AvanteRootSpec[], all?: boolean }
function M.detect(opts)
opts = opts or {}
opts.spec = opts.spec or type(vim.g.root_spec) == "table" and vim.g.root_spec or M.spec
opts.buf = (opts.buf == nil or opts.buf == 0) and vim.api.nvim_get_current_buf() or opts.buf
local ret = {} ---@type AvanteRoot[]
for _, spec in ipairs(opts.spec) do
local paths = M.resolve(spec)(opts.buf)
paths = paths or {}
paths = type(paths) == "table" and paths or { paths }
local roots = {} ---@type string[]
for _, p in ipairs(paths) do
local pp = M.realpath(p)
if pp and not vim.tbl_contains(roots, pp) then
roots[#roots + 1] = pp
end
end
table.sort(roots, function(a, b)
return #a > #b
end)
if #roots > 0 then
ret[#ret + 1] = { spec = spec, paths = roots }
if opts.all == false then
break
end
end
end
return ret
end
---@type table<number, string>
M.cache = {}
-- returns the root directory based on:
-- * lsp workspace folders
-- * lsp root_dir
-- * root pattern of filename of the current buffer
-- * root pattern of cwd
---@param opts? {normalize?:boolean, buf?:number}
---@return string
function M.get(opts)
opts = opts or {}
local buf = opts.buf or vim.api.nvim_get_current_buf()
local ret = M.cache[buf]
if not ret then
local roots = M.detect({ all = false, buf = buf })
ret = roots[1] and roots[1].paths[1] or vim.uv.cwd()
M.cache[buf] = ret
end
if opts and opts.normalize then
return ret
end
return Utils.is_win() and ret:gsub("/", "\\") or ret
end
function M.git()
local root = M.get()
local git_root = vim.fs.find(".git", { path = root, upward = true })[1]
local ret = git_root and vim.fn.fnamemodify(git_root, ":h") or root
return ret
end
return M

View File

@ -1,3 +1,13 @@
if vim.fn.has("nvim-0.10") == 0 then
vim.api.nvim_echo({
{ "Avante requires at least nvim-0.10", "ErrorMsg" },
{ "Please upgrade your neovim version", "WarningMsg" },
{ "Press any key to exit", "ErrorMsg" },
}, true, {})
vim.fn.getchar()
vim.cmd([[quit]])
end
--- NOTE: We will override vim.paste if img-clip.nvim is available to work with avante.nvim internal logic paste --- NOTE: We will override vim.paste if img-clip.nvim is available to work with avante.nvim internal logic paste
local Clipboard = require("avante.clipboard") local Clipboard = require("avante.clipboard")

81
syntax/jinja.vim vendored Normal file
View File

@ -0,0 +1,81 @@
" reference: https://github.com/lepture/vim-jinja/blob/master/syntax/jinja.vim
if exists("b:current_syntax")
finish
endif
if !exists("main_syntax")
let main_syntax = 'html'
endif
runtime! syntax/html.vim
unlet b:current_syntax
syntax case match
" jinja template built-in tags and parameters
" 'comment' doesn't appear here because it gets special treatment
syn keyword jinjaStatement contained if else elif endif is not
syn keyword jinjaStatement contained for in recursive endfor
syn keyword jinjaStatement contained raw endraw
syn keyword jinjaStatement contained block endblock extends super scoped
syn keyword jinjaStatement contained macro endmacro call endcall
syn keyword jinjaStatement contained from import as do continue break
syn keyword jinjaStatement contained filter endfilter set endset
syn keyword jinjaStatement contained include ignore missing
syn keyword jinjaStatement contained with without context endwith
syn keyword jinjaStatement contained trans endtrans pluralize
syn keyword jinjaStatement contained autoescape endautoescape
" jinja templete built-in filters
syn keyword jinjaFilter contained abs attr batch capitalize center default
syn keyword jinjaFilter contained dictsort escape filesizeformat first
syn keyword jinjaFilter contained float forceescape format groupby indent
syn keyword jinjaFilter contained int join last length list lower pprint
syn keyword jinjaFilter contained random replace reverse round safe slice
syn keyword jinjaFilter contained sort string striptags sum
syn keyword jinjaFilter contained title trim truncate upper urlize
syn keyword jinjaFilter contained wordcount wordwrap
" jinja template built-in tests
syn keyword jinjaTest contained callable defined divisibleby escaped
syn keyword jinjaTest contained even iterable lower mapping none number
syn keyword jinjaTest contained odd sameas sequence string undefined upper
syn keyword jinjaFunction contained range lipsum dict cycler joiner
" Keywords to highlight within comments
syn keyword jinjaTodo contained TODO FIXME XXX
" jinja template constants (always surrounded by double quotes)
syn region jinjaArgument contained start=/"/ skip=/\\"/ end=/"/
syn region jinjaArgument contained start=/'/ skip=/\\'/ end=/'/
syn keyword jinjaArgument contained true false
" Mark illegal characters within tag and variables blocks
syn match jinjaTagError contained "#}\|{{\|[^%]}}\|[&#]"
syn match jinjaVarError contained "#}\|{%\|%}\|[<>!&#%]"
syn cluster jinjaBlocks add=jinjaTagBlock,jinjaVarBlock,jinjaComBlock,jinjaComment
" jinja template tag and variable blocks
syn region jinjaTagBlock start="{%" end="%}" contains=jinjaStatement,jinjaFilter,jinjaArgument,jinjaFilter,jinjaTest,jinjaTagError display containedin=ALLBUT,@jinjaBlocks
syn region jinjaVarBlock start="{{" end="}}" contains=jinjaFilter,jinjaArgument,jinjaVarError display containedin=ALLBUT,@jinjaBlocks
syn region jinjaComBlock start="{#" end="#}" contains=jinjaTodo containedin=ALLBUT,@jinjaBlocks
hi def link jinjaTagBlock PreProc
hi def link jinjaVarBlock PreProc
hi def link jinjaStatement Statement
hi def link jinjaFunction Function
hi def link jinjaTest Type
hi def link jinjaFilter Identifier
hi def link jinjaArgument Constant
hi def link jinjaTagError Error
hi def link jinjaVarError Error
hi def link jinjaError Error
hi def link jinjaComment Comment
hi def link jinjaComBlock Comment
hi def link jinjaTodo Todo
let b:current_syntax = "jinja"