diff --git a/.gitattributes b/.gitattributes index 91084dcfc..59ee09c88 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ * text=auto eol=lf **/*.lock linguist-generated=true +*.avanterules linguist-language=jinja +syntax/jinja.vim linguist-vendored diff --git a/Build.ps1 b/Build.ps1 index 23798d569..8b05ea4ef 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -12,8 +12,10 @@ function Build-FromSource($feature) { cargo build --release --features=$feature - $targetFile = "avante_tokenizers.dll" - Copy-Item (Join-Path "target\release\libavante_tokenizers.dll") (Join-Path $BuildDir $targetFile) + $targetTokenizerFile = "avante_tokenizers.dll" + $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" } diff --git a/Cargo.lock b/Cargo.lock index 88bd522cc..78470b2a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "avante-templates" +version = "0.1.0" +dependencies = [ + "minijinja", + "mlua", + "serde", +] + [[package]] name = "avante-tokenizers" version = "0.1.0" @@ -546,6 +555,28 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "minimal-lexical" version = "0.2.1" @@ -1047,6 +1078,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + [[package]] name = "serde" version = "1.0.209" @@ -1244,6 +1281,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "unicode-bidi" version = "0.3.15" @@ -1328,12 +1374,24 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 7e4e8563d..de6aead55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,28 @@ version = "0.1.0" [workspace.dependencies] 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] unsafe_code = "warn" diff --git a/Makefile b/Makefile index 2239da347..1cd59b8db 100644 --- a/Makefile +++ b/Makefile @@ -11,32 +11,40 @@ else $(error Unsupported operating system: $(UNAME)) endif -LUA_VERSIONS := luajit lua51 +LUA_VERSIONS := luajit lua51 lua52 lua53 lua54 + BUILD_DIR := build +TARGET_LIBRARY ?= all all: luajit -luajit: $(BUILD_DIR)/libavante_tokenizers.$(EXT) -lua51: $(BUILD_DIR)/libavante_tokenizers-lua51.$(EXT) -lua52: $(BUILD_DIR)/libavante_tokenizers-lua52.$(EXT) -lua53: $(BUILD_DIR)/libavante_tokenizers-lua53.$(EXT) -lua54: $(BUILD_DIR)/libavante_tokenizers-lua54.$(EXT) +define make_definitions +ifeq ($(TARGET_LIBRARY), all) +$1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT) $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT) +else ifeq ($(TARGET_LIBRARY), tokenizers) +$1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT) +else ifeq ($(TARGET_LIBRARY), templates) +$1: $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT) +else + $$(error TARGET_LIBRARY must be one of all, tokenizers, templates) +endif +endef + +$(foreach lua_version,$(LUA_VERSIONS),$(eval $(call make_definitions,$(lua_version)))) define build_from_source - cargo build --release --features=$1 - cp target/release/libavante_tokenizers.$(EXT) $(BUILD_DIR)/avante_tokenizers.$(EXT) + cargo build --release --features=$1 -p avante-$2 + cp target/release/libavante_$2.$(EXT) $(BUILD_DIR)/avante_$2.$(EXT) +endef + +define build_targets +$(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT): $(BUILD_DIR) + $$(call build_from_source,$1,tokenizers) +$(BUILD_DIR)/libAvanteTemplates-$1.$(EXT): $(BUILD_DIR) + $$(call build_from_source,$1,templates) endef -$(BUILD_DIR)/libavante_tokenizers.$(EXT): $(BUILD_DIR) - $(call build_from_source,luajit) -$(BUILD_DIR)/libavante_tokenizers-lua51.$(EXT): $(BUILD_DIR) - $(call build_from_source,lua51) -$(BUILD_DIR)/libavante_tokenizers-lua52.$(EXT): $(BUILD_DIR) - $(call build_from_source,lua52) -$(BUILD_DIR)/libavante_tokenizers-lua53.$(EXT): $(BUILD_DIR) - $(call build_from_source,lua53) -$(BUILD_DIR)/libavante_tokenizers-lua54.$(EXT): $(BUILD_DIR) - $(call build_from_source,lua54) +$(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_targets,$(lua_version)))) $(BUILD_DIR): mkdir -p $(BUILD_DIR) diff --git a/README.md b/README.md index c88b95d41..67a89188f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ https://github.com/user-attachments/assets/86140bfd-08b4-483d-a887-1b701d9e37dd opts = { -- 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 = { "stevearc/dressing.nvim", "nvim-lua/plenary.nvim", @@ -93,7 +93,7 @@ Plug 'yetone/avante.nvim', { 'branch': 'main', 'do': { -> avante#build() }, 'on' > [!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) @@ -147,6 +147,7 @@ require('copilot').setup ({ require('render-markdown').setup ({ -- use recommended settings from above }) +require('avante_lib').load() require('avante').setup ({ -- 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 +## 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", "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 + +
+ + Example folder structure for custom prompt + +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. + +
+ +> [!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 - [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] Slash commands - [x] Edit the selected block -- [ ] Smart Tab (Cursor Flow) +- [x] Smart Tab (Cursor Flow) - [ ] Chat with project - [ ] 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: -| 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 | -| [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 | -| [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 | -| [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 | +| [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 | [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 | [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 | [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. diff --git a/crates/avante-templates/Cargo.toml b/crates/avante-templates/Cargo.toml new file mode 100644 index 000000000..b6b5ff4c7 --- /dev/null +++ b/crates/avante-templates/Cargo.toml @@ -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"] diff --git a/crates/avante-templates/src/lib.rs b/crates/avante-templates/src/lib.rs new file mode 100644 index 000000000..300aac9fd --- /dev/null +++ b/crates/avante-templates/src/lib.rs @@ -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>>, +} + +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, + project_context: Option, + memory_context: Option, +} + +// 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 { + 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 { + 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) +} diff --git a/crates/avante-tokenizers/Cargo.toml b/crates/avante-tokenizers/Cargo.toml index 26c188607..afe9a5519 100644 --- a/crates/avante-tokenizers/Cargo.toml +++ b/crates/avante-tokenizers/Cargo.toml @@ -12,17 +12,9 @@ license = { workspace = true } workspace = true [dependencies] -mlua = { version = "0.10.0-beta.1", features = [ - "module", - "serialize", -], 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 } +mlua = { workspace = true } +tiktoken-rs = { workspace = true } +tokenizers = { workspace = true } [features] lua51 = ["mlua/lua51"] diff --git a/crates/avante-tokenizers/src/lib.rs b/crates/avante-tokenizers/src/lib.rs index 0d5760044..39016fa4b 100644 --- a/crates/avante-tokenizers/src/lib.rs +++ b/crates/avante-tokenizers/src/lib.rs @@ -68,13 +68,12 @@ fn encode(state: &State, text: String) -> LuaResult<(Vec, 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(); *tokenizer_mutex = Some(match model.as_str() { "gpt-4o" => TokenizerType::Tiktoken(Tiktoken::new(model)), _ => TokenizerType::HuggingFace(HuggingFaceTokenizer::new(model)), }); - Ok(()) } #[mlua::lua_module] @@ -86,7 +85,10 @@ fn avante_tokenizers(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; exports.set( "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( "encode", diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 878dc062d..d634f97fe 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -18,6 +18,13 @@ M.defaults = { -- 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. 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 openai = { endpoint = "https://api.openai.com/v1", @@ -309,6 +316,7 @@ M.BASE_PROVIDER_KEYS = { "local", "_shellenv", "tokenizer_id", + "use_xml_format", } ---@return {width: integer, height: integer} diff --git a/lua/avante/init.lua b/lua/avante/init.lua index 97574460d..6d586888e 100644 --- a/lua/avante/init.lua +++ b/lua/avante/init.lua @@ -234,6 +234,15 @@ H.autocmds = function() -- automatically setup Avante filetype to markdown vim.treesitter.language.register("markdown", "Avante") + + vim.filetype.add({ + extension = { + ["avanterules"] = "jinja", + }, + pattern = { + ["%.avanterules%.[%w_.-]+"] = "jinja", + }, + }) end ---@param current boolean? false to disable setting current, otherwise use this to track across tabs. @@ -359,16 +368,6 @@ end) ---@param opts? avante.Config 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 ---but most of the other functionality will only be called once from lazy.nvim Config.setup(opts) diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index 2623df46a..c4ddef949 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -4,6 +4,7 @@ local curl = require("plenary.curl") local Utils = require("avante.utils") local Config = require("avante.config") +local Path = require("avante.path") local P = require("avante.providers") ---@class avante.LLM @@ -13,136 +14,32 @@ M.CANCEL_PATTERN = "AvanteLLMEscape" ------------------------------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 }) ----@class StreamOptions ----@field file_content string +---@alias LlmMode "planning" | "editing" | "suggesting" +--- +---@class TemplateOptions +---@field use_xml_format boolean +---@field ask boolean +---@field question string ---@field code_lang string +---@field file_content string ---@field selected_code string | nil ----@field instructions string ---@field project_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_complete AvanteCompleteParser ---@param opts StreamOptions M.stream = function(opts) local mode = opts.mode or "planning" - local provider = 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 + ---@type AvanteProviderFunctor + local Provider = P[Config.provider] -- Check if the instructions contains an image path local image_paths = {} @@ -159,52 +56,30 @@ M.stream = function(opts) original_instructions = table.concat(lines, "\n") end - local user_prompts = {} - - if opts.selected_code and opts.selected_code ~= "" then - table.insert( - user_prompts, - string.format("```%s\n%s\n```", opts.code_lang, opts.file_content) - ) - table.insert(user_prompts, string.format("```%s\n%s\n```", opts.code_lang, opts.selected_code)) - else - table.insert(user_prompts, string.format("```%s\n%s\n```", opts.code_lang, opts.file_content)) - end - - if opts.project_context then - table.insert(user_prompts, string.format("%s", opts.project_context)) - end - - if opts.memory_context then - table.insert(user_prompts, string.format("%s", opts.memory_context)) - end - - if opts.full_file_contents_context then - table.insert( - user_prompts, - string.format("%s", opts.full_file_contents_context) - ) - end - - table.insert(user_prompts, "" .. original_instructions .. "") - - local user_prompt = user_prompt_tpl:gsub("%${(.-)}", opts) + 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, + }) - table.insert(user_prompts, user_prompt) + Utils.debug(user_prompt) ---@type AvantePromptOptions local code_opts = { - system_prompt = system_prompt, - user_prompts = user_prompts, + system_prompt = Config.system_prompt, + user_prompt = user_prompt, image_paths = image_paths, } ---@type string local current_event_state = nil - ---@type AvanteProviderFunctor - local Provider = P[provider] - ---@type AvanteHandlerOptions local handler_opts = { on_chunk = opts.on_chunk, on_complete = opts.on_complete } ---@type AvanteCurlOutput @@ -244,7 +119,7 @@ M.stream = function(opts) return end 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 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.", diff --git a/lua/avante/path.lua b/lua/avante/path.lua index 82565a823..8758c298a 100644 --- a/lua/avante/path.lua +++ b/lua/avante/path.lua @@ -1,5 +1,7 @@ local fn, api = vim.fn, vim.api +local Utils = require("avante.utils") local Path = require("plenary.path") +local Scan = require("plenary.scandir") local Config = require("avante.config") ---@class avante.Path @@ -7,9 +9,10 @@ local Config = require("avante.config") ---@field cache_path Path local P = {} -local M = {} - +-- Helpers local H = {} + +-- Get a chat history file name given a buffer ---@param bufnr integer ---@return string H.filename = function(bufnr) @@ -20,12 +23,23 @@ H.filename = function(bufnr) return fn.substitute(path_with_separators, "[^A-Za-z0-9._]", "_", "g") .. ".json" 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 ---@return Path M.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) @@ -36,6 +50,7 @@ M.load = function(bufnr) return {} end +-- Saves the chat history for the given buffer. ---@param bufnr integer ---@param history table M.save = function(bufnr, history) @@ -45,6 +60,83 @@ end 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() local history_path = Path:new(Config.history.storage_path) if not history_path:exists() then @@ -57,6 +149,26 @@ P.setup = function() cache_path:mkdir({ parents = true }) end 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 return P diff --git a/lua/avante/providers/claude.lua b/lua/avante/providers/claude.lua index e453a3231..0abf37514 100644 --- a/lua/avante/providers/claude.lua +++ b/lua/avante/providers/claude.lua @@ -7,13 +7,13 @@ local M = {} M.api_key_name = "ANTHROPIC_API_KEY" M.tokenizer_id = "gpt-4o" +M.use_xml_format = true ----@param prompt_opts AvantePromptOptions -M.parse_message = function(prompt_opts) +M.parse_message = function(opts) local message_content = {} - if Clipboard.support_paste_image() and prompt_opts.image_paths then - for _, image_path in ipairs(prompt_opts.image_paths) do + if Clipboard.support_paste_image() and opts.image_paths then + for _, image_path in ipairs(opts.image_paths) do table.insert(message_content, { type = "image", source = { @@ -25,32 +25,15 @@ M.parse_message = function(prompt_opts) end end - local user_prompts_with_length = {} - for idx, user_prompt in ipairs(prompt_opts.user_prompts) do - table.insert(user_prompts_with_length, { idx = idx, length = Utils.tokens.calculate_tokens(user_prompt) }) - end - - table.sort(user_prompts_with_length, function(a, b) - 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 + local user_prompt_obj = { + type = "text", + text = opts.user_prompt, + } + if Utils.tokens.calculate_tokens(opts.user_prompt) then + user_prompt_obj.cache_control = { type = "ephemeral" } 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 + table.insert(message_content, user_prompt_obj) return { { diff --git a/lua/avante/providers/cohere.lua b/lua/avante/providers/cohere.lua index 4a024c0e0..dbbd7da42 100644 --- a/lua/avante/providers/cohere.lua +++ b/lua/avante/providers/cohere.lua @@ -32,11 +32,9 @@ M.api_key_name = "CO_API_KEY" M.tokenizer_id = "CohereForAI/c4ai-command-r-plus-08-2024" M.parse_message = function(opts) - local user_prompt = table.concat(opts.user_prompts, "\n\n") - return { preamble = opts.system_prompt, - message = user_prompt, + message = opts.user_prompt, } end diff --git a/lua/avante/providers/copilot.lua b/lua/avante/providers/copilot.lua index 30e4f4e48..a3de3e082 100644 --- a/lua/avante/providers/copilot.lua +++ b/lua/avante/providers/copilot.lua @@ -132,7 +132,7 @@ M.tokenizer_id = "gpt-4o" M.parse_message = function(opts) return { { role = "system", content = opts.system_prompt }, - { role = "user", content = table.concat(opts.user_prompts, "\n\n") }, + { role = "user", content = opts.user_prompt }, } end diff --git a/lua/avante/providers/gemini.lua b/lua/avante/providers/gemini.lua index 485e0c96f..2f52b3a6d 100644 --- a/lua/avante/providers/gemini.lua +++ b/lua/avante/providers/gemini.lua @@ -25,11 +25,7 @@ M.parse_message = function(opts) end -- insert a part into parts - for _, user_prompt in ipairs(opts.user_prompts) do - table.insert(message_content, { - text = user_prompt, - }) - end + table.insert(message_content, { text = opts.user_prompt }) return { systemInstruction = { diff --git a/lua/avante/providers/init.lua b/lua/avante/providers/init.lua index 2b5952ff7..949269015 100644 --- a/lua/avante/providers/init.lua +++ b/lua/avante/providers/init.lua @@ -10,7 +10,7 @@ local Dressing = require("avante.ui.dressing") --- ---@class AvantePromptOptions: table<[string], string> ---@field system_prompt string ----@field user_prompts string[] +---@field user_prompt string ---@field image_paths? string[] --- ---@class AvanteBaseMessage @@ -70,6 +70,7 @@ local Dressing = require("avante.ui.dressing") ---@field has fun(): boolean ---@field api_key_name string ---@field tokenizer_id string | "gpt-4o" +---@field use_xml_format boolean ---@field model? string ---@field parse_api_key fun(): string | nil ---@field parse_stream_data? AvanteStreamParser @@ -275,6 +276,10 @@ M = setmetatable(M, { t[k].tokenizer_id = "gpt-4o" end + if t[k].use_xml_format == nil then + t[k].use_xml_format = false + end + if t[k].has == nil then t[k].has = function() return E.parse_envvar(t[k]) ~= nil diff --git a/lua/avante/providers/openai.lua b/lua/avante/providers/openai.lua index fbc324ec7..e96df2b6b 100644 --- a/lua/avante/providers/openai.lua +++ b/lua/avante/providers/openai.lua @@ -30,12 +30,10 @@ M.tokenizer_id = "gpt-4o" ---@param opts AvantePromptOptions M.get_user_message = function(opts) - return table.concat(opts.user_prompts, "\n\n") + return opts.user_prompt end M.parse_message = function(opts) - local user_prompt = table.concat(opts.user_prompts, "\n\n") - ---@type string | OpenAIMessage[] local user_content 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 - table.insert(user_content, { type = "text", text = user_prompt }) + table.insert(user_content, { type = "text", text = opts.user_prompt }) else - user_content = user_prompt + user_content = opts.user_prompt end return { diff --git a/lua/avante/selection.lua b/lua/avante/selection.lua index 1d36f8a86..e61789b86 100644 --- a/lua/avante/selection.lua +++ b/lua/avante/selection.lua @@ -414,6 +414,7 @@ function Selection:create_editing_input() local filetype = api.nvim_get_option_value("filetype", { buf = code_bufnr }) Llm.stream({ + bufnr = code_bufnr, file_content = code_content, code_lang = filetype, selected_code = self.selection.content, diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index 74f6053dd..d15c35291 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -1346,6 +1346,7 @@ function Sidebar:create_input() end Llm.stream({ + bufnr = self.code.bufnr, file_content = content_with_line_numbers, code_lang = filetype, selected_code = selected_code_content_with_line_numbers, diff --git a/lua/avante/suggestion.lua b/lua/avante/suggestion.lua index 4d7f5df70..3e741234f 100644 --- a/lua/avante/suggestion.lua +++ b/lua/avante/suggestion.lua @@ -111,6 +111,7 @@ function Suggestion:suggest() local full_response = "" Llm.stream({ + bufnr = bufnr, file_content = code_content, code_lang = filetype, instructions = vim.json.encode(doc), diff --git a/lua/avante/templates/editing.avanterules b/lua/avante/templates/editing.avanterules new file mode 100644 index 000000000..29b0054a2 --- /dev/null +++ b/lua/avante/templates/editing.avanterules @@ -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 %} diff --git a/lua/avante/templates/planning.avanterules b/lua/avante/templates/planning.avanterules new file mode 100644 index 000000000..c63effbfc --- /dev/null +++ b/lua/avante/templates/planning.avanterules @@ -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 -%} + +```{{code_lang}} +{{file_content}} +``` + + + +```{{code_lang}} +{{selected_code}} +``` + +{%- else -%} + +```{{code_lang}} +{{file_content}} +``` + +{%- endif %}{%- if project_context -%} + +{{project_context}} + +{%- endif %}{%- if 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 %} +{% 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 -%} + + +{{question}} +{%- else %} +QUESTION: +{{question}} +{%- endif %} +{% else %} +{% if use_xml_format -%} +{{question}} +{% else %} +QUESTION: +{{question}} +{%- endif %} +{%- endif %} diff --git a/lua/avante/templates/suggesting.avanterules b/lua/avante/templates/suggesting.avanterules new file mode 100644 index 000000000..3107d706d --- /dev/null +++ b/lua/avante/templates/suggesting.avanterules @@ -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 %} + diff --git a/lua/avante/tokenizers.lua b/lua/avante/tokenizers.lua index d07f3ee68..7000c961b 100644 --- a/lua/avante/tokenizers.lua +++ b/lua/avante/tokenizers.lua @@ -9,14 +9,19 @@ local M = {} ---@param model "gpt-4o" | string M.setup = function(model) - local ok, core = pcall(require, "avante_tokenizers") - if not ok then - return - end - ---@cast core AvanteTokenizer - if tokenizers == nil then - tokenizers = core - end + vim.defer_fn(function() + local ok, core = pcall(require, "avante_tokenizers") + if not ok then + return + end + + ---@cast core AvanteTokenizer + if tokenizers == nil then + tokenizers = core + end + + core.from_pretrained(model) + end, 1000) local HF_TOKEN = os.getenv("HF_TOKEN") if HF_TOKEN == nil and model ~= "gpt-4o" then @@ -26,9 +31,6 @@ M.setup = function(model) ) end vim.env.HF_HUB_DISABLE_PROGRESS_BARS = 1 - - ---@cast core AvanteTokenizer - core.from_pretrained(model) end M.available = function() diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index cb6c6925c..8a977b31f 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -4,6 +4,7 @@ local lsp = vim.lsp ---@class avante.utils: LazyUtilCore ---@field tokens avante.utils.tokens +---@field root avante.utils.root local M = {} setmetatable(M, { @@ -30,6 +31,10 @@ M.has = function(plugin) return package.loaded[plugin] ~= nil end +M.is_win = function() + return jit.os:find("Windows") ~= nil +end + ---@return "linux" | "darwin" | "windows" M.get_os_name = function() 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 }) 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 +---@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 opts? LazyNotifyOpts function M.notify(msg, opts) diff --git a/lua/avante/utils/root.lua b/lua/avante/utils/root.lua new file mode 100644 index 000000000..048d5092e --- /dev/null +++ b/lua/avante/utils/root.lua @@ -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 +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 diff --git a/plugin/avante.lua b/plugin/avante.lua index 1a18c5ede..8b53290a4 100644 --- a/plugin/avante.lua +++ b/plugin/avante.lua @@ -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 local Clipboard = require("avante.clipboard") diff --git a/syntax/jinja.vim b/syntax/jinja.vim new file mode 100644 index 000000000..b9cd47bd0 --- /dev/null +++ b/syntax/jinja.vim @@ -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"