feat(ui): add envvar popup with warnings (#54)

* feat: add support for input popup of envvar

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update README and ignore certain filetypes for popup

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix: readme doesn't support nested callout

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
This commit is contained in:
Aaron Pham 2024-08-17 16:04:40 -04:00 committed by GitHub
parent f4ab995515
commit af1f51455e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 228 additions and 42 deletions

View File

@ -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://<your-resource-name>.openai.azure.com"
endpoint = "", -- example: "https://<your-resource-name>.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 = "<leader>aa",
edit = "<leader>ae",
refresh = "<leader>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`:
- <kbd>Leader</kbd><kbd>a</kbd><kbd>a</kbd> — show sidebar
- <kbd>Leader</kbd><kbd>a</kbd><kbd>r</kbd> — show sidebar
- <kbd>c</kbd><kbd>o</kbd> — choose ours
- <kbd>c</kbd><kbd>t</kbd> — choose theirs
- <kbd>c</kbd><kbd>b</kbd> — choose both

View File

@ -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<Provider, Environment | string>
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", "<Esc>", 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

View File

@ -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",

View File

@ -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
)

View File

@ -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

View File

@ -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