diff --git a/README.md b/README.md index c5e2d7e..5d2c3b4 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ **avante.nvim** is a Neovim plugin designed to emulate the behavior of the [Cursor](https://www.cursor.com) AI IDE, providing users with AI-driven code suggestions and the ability to apply these recommendations directly to their source files with minimal effort. > [!NOTE] -> +> > ⚠️ This plugin is still in a very early stage of development, so please be aware that the current code is very messy and unstable, and problems are likely to occur. -> +> > 🥰 This project is undergoing rapid iterations, and many exciting features will be added successively. Stay tuned! https://github.com/user-attachments/assets/510e6270-b6cf-459d-9a2f-15b397d1fe53 @@ -69,8 +69,11 @@ Install `avante.nvim` using [lazy.nvim](https://github.com/folke/lazy.nvim): Default setup configuration: +_See [config.lua#L9](./lua/avante/config.lua) for the full config_ + ```lua { + ---@alias Provider "openai" | "claude" | "azure" provider = "claude", -- "claude" or "openai" or "azure" openai = { endpoint = "https://api.openai.com", @@ -79,7 +82,7 @@ Default setup configuration: max_tokens = 4096, }, azure = { - endpoint = "", -- Example: "https://.openai.azure.com" + endpoint = "", -- example: "https://.openai.azure.com" deployment = "", -- Azure deployment name (e.g., "gpt-4o", "my-gpt-4o-deployment") api_version = "2024-06-01", temperature = 0, @@ -92,13 +95,17 @@ Default setup configuration: max_tokens = 4096, }, highlights = { + ---@type AvanteConflictHighlights diff = { - current = "DiffText", -- need have background color - incoming = "DiffAdd", -- need have background color + current = "DiffText", + incoming = "DiffAdd", }, }, mappings = { ask = "aa", + edit = "ae", + refresh = "ar", + --- @class AvanteConflictMappings diff = { ours = "co", theirs = "ct", @@ -107,6 +114,20 @@ Default setup configuration: next = "]x", prev = "[x", }, + jump = { + next = "]]", + prev = "[[", + }, + }, + windows = { + width = 30, -- default % based on available width + }, + --- @class AvanteConflictUserConfig + diff = { + debug = false, + autojump = true, + ---@type string | fun(): any + list_opener = "copen", }, } ``` @@ -115,30 +136,33 @@ Default setup configuration: Given its early stage, `avante.nvim` currently supports the following basic functionalities: -1. Set the appropriate API key as an environment variable: +> [!IMPORTANT] +> +> For most consistency between neovim session, it is recommended to set the environment variables in your shell file. +> By default, `Avante` will prompt you at startup to input the API key for the provider you have selected. +> +> For Claude: +> +> ```sh +> export ANTHROPIC_API_KEY=your-api-key +> ``` +> +> For OpenAI: +> +> ```sh +> export OPENAI_API_KEY=your-api-key +> ``` +> +> For Azure OpenAI: +> +> ```sh +> export AZURE_OPENAI_API_KEY=your-api-key +> ``` - For Claude: - - ```sh - export ANTHROPIC_API_KEY=your-api-key - ``` - - For OpenAI: - - ```sh - export OPENAI_API_KEY=your-api-key - ``` - - For Azure OpenAI: - - ```sh - export AZURE_OPENAI_API_KEY=your-api-key - ``` - -2. Open a code file in Neovim. -3. Use the `:AvanteAsk` command to query the AI about the code. -4. Review the AI's suggestions. -5. Apply the recommended changes directly to your code with a simple command or key binding. +1. Open a code file in Neovim. +2. Use the `:AvanteAsk` command to query the AI about the code. +3. Review the AI's suggestions. +4. Apply the recommended changes directly to your code with a simple command or key binding. **Note**: The plugin is still under active development, and both its functionality and interface are subject to significant changes. Expect some rough edges and instability as the project evolves. @@ -147,6 +171,7 @@ Given its early stage, `avante.nvim` currently supports the following basic func The following key bindings are available for use with `avante.nvim`: - Leaderaa — show sidebar +- Leaderar — show sidebar - co — choose ours - ct — choose theirs - cb — choose both diff --git a/lua/avante/ai_bot.lua b/lua/avante/ai_bot.lua index 424569a..a426b0f 100644 --- a/lua/avante/ai_bot.lua +++ b/lua/avante/ai_bot.lua @@ -1,6 +1,9 @@ local fn = vim.fn +local api = vim.api local curl = require("plenary.curl") +local Input = require("nui.input") +local Event = require("nui.utils.autocmd").event local Utils = require("avante.utils") local Config = require("avante.config") @@ -9,6 +12,153 @@ local Tiktoken = require("avante.tiktoken") ---@class avante.AiBot local M = {} +---@class Environment: table<[string], any> +---@field [string] string the environment variable name +---@field fallback? string Optional fallback API key environment variable name + +---@class EnvironmentHandler: table<[Provider], string> +local E = { + ---@type table + env = { + openai = "OPENAI_API_KEY", + claude = "ANTHROPIC_API_KEY", + azure = { "AZURE_OPENAI_API_KEY", fallback = "OPENAI_API_KEY" }, + }, + _once = false, +} + +E = setmetatable(E, { + ---@param k Provider + __index = function(_, k) + local envvar = E.env[k] + if type(envvar) == "string" then + local value = os.getenv(envvar) + return value and true or false + elseif type(envvar) == "table" then + local main_key = envvar[1] + local value = os.getenv(main_key) + if value then + return true + elseif envvar.fallback then + return os.getenv(envvar.fallback) and true or false + end + end + return false + end, +}) + +-- courtesy of https://github.com/MunifTanjim/nui.nvim/wiki/nui.input +local SecretInput = Input:extend("SecretInput") + +function SecretInput:init(popup_options, options) + assert( + not options.conceal_char or vim.api.nvim_strwidth(options.conceal_char) == 1, + "conceal_char must be a single char" + ) + + popup_options.win_options = vim.tbl_deep_extend("force", popup_options.win_options or {}, { + conceallevel = 2, + concealcursor = "nvi", + }) + + SecretInput.super.init(self, popup_options, options) + + self._.conceal_char = type(options.conceal_char) == "nil" and "*" or options.conceal_char +end + +function SecretInput:mount() + SecretInput.super.mount(self) + + local conceal_char = self._.conceal_char + local prompt_length = vim.api.nvim_strwidth(vim.fn.prompt_getprompt(self.bufnr)) + + vim.api.nvim_buf_call(self.bufnr, function() + vim.cmd(string.format( + [[ + syn region SecretValue start=/^/ms=s+%s end=/$/ contains=SecretChar + syn match SecretChar /./ contained conceal %s + ]], + prompt_length, + conceal_char and "cchar=" .. (conceal_char or "*") or "" + )) + end) +end + +--- return the environment variable name for the given provider +---@param provider? Provider +---@return string the envvar key +E.key = function(provider) + provider = provider or Config.provider + local var = E.env[provider] + return type(var) == "table" and var[1] ---@cast var string + or var +end + +E.setup = function(var) + if E._once then + return + end + + local input = SecretInput({ + position = "50%", + size = { + width = 40, + }, + border = { + style = "single", + text = { + top = "Enter " .. var, + top_align = "center", + }, + }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:Normal", + }, + }, { + prompt = "> ", + default_value = "", + on_submit = function(value) + vim.fn.setenv(var, value) + end, + on_close = function() + if not E[Config.provider] then + vim.notify_once("Failed to set " .. var .. ". Avante won't work as expected", vim.log.levels.WARN) + end + end, + }) + + api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, { + pattern = "*", + callback = function() + if E._once then + return + end + + vim.defer_fn(function() + -- only mount if given buffer is not of buftype ministarter, dashboard, alpha, qf + local exclude_buftypes = { "dashboard", "alpha", "qf", "nofile" } + local exclude_filetypes = + { "NvimTree", "Outline", "help", "dashboard", "alpha", "qf", "ministarter", "TelescopePrompt", "gitcommit" } + if + not vim.tbl_contains(exclude_buftypes, vim.bo.buftype) + and not vim.tbl_contains(exclude_filetypes, vim.bo.filetype) + then + E._once = true + input:mount() + end + end, 200) + end, + }) + + input:map("n", "", function() + input:unmount() + end, { noremap = true }) + + input:on(Event.BufLeave, function() + input:unmount() + end) +end + local system_prompt = [[ You are an excellent programming expert. ]] @@ -57,10 +207,7 @@ Remember: Accurate line numbers are CRITICAL. The range start_line to end_line m ]] local function call_claude_api_stream(question, code_lang, code_content, selected_code_content, on_chunk, on_complete) - local api_key = os.getenv("ANTHROPIC_API_KEY") - if not api_key then - error("ANTHROPIC_API_KEY environment variable is not set") - end + local api_key = os.getenv(E.key("azure")) local tokens = Config.claude.max_tokens local headers = { @@ -174,11 +321,7 @@ local function call_claude_api_stream(question, code_lang, code_content, selecte end local function call_openai_api_stream(question, code_lang, code_content, selected_code_content, on_chunk, on_complete) - local api_key = os.getenv("OPENAI_API_KEY") - if not api_key and Config.provider == "openai" then - error("OPENAI_API_KEY environment variable is not set") - end - + local api_key = os.getenv(E.key("openai")) local user_prompt = base_user_prompt .. "\n\nCODE:\n" .. "```" @@ -209,10 +352,7 @@ local function call_openai_api_stream(question, code_lang, code_content, selecte local url, headers, body if Config.provider == "azure" then - api_key = os.getenv("AZURE_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY") - if not api_key then - error("Azure OpenAI API key is not set. Please set AZURE_OPENAI_API_KEY or OPENAI_API_KEY environment variable.") - end + api_key = os.getenv(E.key("azure")) url = Config.azure.endpoint .. "/openai/deployments/" .. Config.azure.deployment @@ -306,4 +446,11 @@ function M.call_ai_api_stream(question, code_lang, code_content, selected_conten end end +function M.setup() + local has = E[Config.provider] + if not has then + E.setup(E.key()) + end +end + return M diff --git a/lua/avante/config.lua b/lua/avante/config.lua index e7b7e5b..004d2e6 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -6,6 +6,7 @@ local M = {} ---@class avante.Config M.defaults = { + ---@alias Provider "openai" | "claude" | "azure" provider = "claude", -- "claude" or "openai" or "azure" openai = { endpoint = "https://api.openai.com", diff --git a/lua/avante/diff.lua b/lua/avante/diff.lua index 826be71..827acb4 100644 --- a/lua/avante/diff.lua +++ b/lua/avante/diff.lua @@ -331,9 +331,10 @@ local function register_cursor_move_events(bufnr) end local hint = string.format( - " [Press <%s> for OURS, <%s> for THEIRS, <%s> for PREV, <%s> for NEXT] ", + " [Press <%s> for OURS, <%s> for THEIRS, <%s> for BOTH, <%s> for PREV, <%s> for NEXT] ", Config.diff.mappings.ours, Config.diff.mappings.theirs, + Config.diff.mappings.both, Config.diff.mappings.prev, Config.diff.mappings.next ) diff --git a/lua/avante/init.lua b/lua/avante/init.lua index 86e08e9..a1b6dbe 100644 --- a/lua/avante/init.lua +++ b/lua/avante/init.lua @@ -4,6 +4,7 @@ local Tiktoken = require("avante.tiktoken") local Sidebar = require("avante.sidebar") local Config = require("avante.config") local Diff = require("avante.diff") +local AiBot = require("avante.ai_bot") local Selection = require("avante.selection") ---@class Avante @@ -164,6 +165,7 @@ function M.setup(opts) end Diff.setup() + AiBot.setup() M.selection = Selection:new():setup() -- setup helpers diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index e93b40b..3755b0c 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -132,6 +132,16 @@ function Sidebar:intialize() relative = { type = "win", winid = fn.bufwinid(self.view.buf) }, }) + self.renderer:add_mappings({ + { + mode = { "n" }, + key = "q", + handler = function() + self.renderer:close() + end, + }, + }) + self.renderer:on_mount(function() self.winid.result = self.renderer:get_component_by_id("result").winid self.winid.input = self.renderer:get_component_by_id("input").winid