Ghost-text AI autocomplete for Vim 9+ and Neovim, with pluggable multi-model round-robin (Gemini, Claude, or any model you configure).
Requires curl on $PATH. Set at least one API key as an environment variable before starting Vim/Neovim (GEMINI_API_KEY and/or ANTHROPIC_API_KEY, or whatever api_key_env you configure per model — see Configuration).
git submodule add https://github.com/albertosca/vim-ai-autocomplete.git ~/.vim/bundle/vim-ai-autocomplete{
'albertosca/vim-ai-autocomplete',
config = function()
require('vim-ai-autocomplete').setup()
end,
}Plug 'albertosca/vim-ai-autocomplete'On Neovim, call require('vim-ai-autocomplete').setup() somewhere in your init.lua after plug is loaded. On Vim, no extra call is needed — plugin/vim-ai-autocomplete.vim loads itself.
local add = MiniDeps.add
add({
source = 'albertosca/vim-ai-autocomplete',
})
require('vim-ai-autocomplete').setup()# Vim
git clone https://github.com/albertosca/vim-ai-autocomplete ~/.vim/pack/plugins/start/vim-ai-autocomplete
# Neovim
git clone https://github.com/albertosca/vim-ai-autocomplete ~/.local/share/nvim/site/pack/plugins/start/vim-ai-autocompleteBoth sides read the same globals — Vimscript globals are visible from Lua via vim.g, so there's a single source of truth even on Neovim.
" Vim (~/.vimrc) or Neovim (init.vim, or before require(...).setup() in init.lua)
let g:vim_ai_autocomplete_models = [
\ {'name': 'gemini-flash', 'family': 'gemini', 'model_id': 'gemini-3.1-flash-lite', 'api_key_env': 'GEMINI_API_KEY'},
\ {'name': 'claude-sonnet', 'family': 'anthropic', 'model_id': 'claude-sonnet-5', 'api_key_env': 'ANTHROPIC_API_KEY'},
\ ]Or, on Neovim, the equivalent via the setup(opts) facilitator (sugar over the same globals above — pick either style, not both):
require('vim-ai-autocomplete').setup({
models = {
{ name = 'gemini-flash', family = 'gemini', model_id = 'gemini-3.1-flash-lite', api_key_env = 'GEMINI_API_KEY' },
{ name = 'claude-sonnet', family = 'anthropic', model_id = 'claude-sonnet-5', api_key_env = 'ANTHROPIC_API_KEY' },
},
auto_trigger = true, -- optional, defaults to true
})| Field | Meaning |
|---|---|
name |
Whatever you want to call this model in ,pr/,pm/:VimAiAutocompleteModel |
family |
'gemini' or 'anthropic' — determines the request/response shape |
model_id |
The real model ID sent to the provider's API |
api_key_env |
Name of the environment variable holding that provider's API key |
If you configure nothing, it defaults to one Gemini and one Claude model. A model only becomes "active" (eligible for ,pr cycling) if its api_key_env is actually set and non-empty in the environment.
| Key | Action |
|---|---|
Tab |
Accept the visible suggestion (falls through to your original Tab mapping otherwise) |
<C-]> |
Dismiss the visible suggestion without leaving insert mode |
,pt |
Toggle auto-trigger on/off |
,pr |
Cycle to the next active model (only registered with 2+ active models) |
,pm |
Pick a model via vim.ui.select (Neovim only, only registered with 2+ active models) |
:VimAiAutocompleteModel <name> |
Switch directly to a named model, with completion |
- FIM (fill-in-the-middle) prompting: the buffer around the cursor is split into a "before" and "after" section (never sending the current line whole to either side), so the model knows exactly where the cursor sits and what already exists after it.
- Redundancy detection: two mechanisms, summed into one count of "characters to discard" from the real buffer text after the cursor — a structural bracket/quote-stack comparison (catches the case where the suggestion closes something already open before the cursor) and a textual suffix/prefix overlap check (catches the model literally repeating what's already there). The discarded span is always shown in red/strikethrough before being dropped on accept, never silently trimmed.
- Ghost text rendering: Vim uses
prop_add/textprop (Vim 9+); Neovim uses extmarks (nvim_buf_set_extmarkwithvirt_text/virt_lines). - Context enrichment (Neovim only): the buffer cut is Treesitter-scope-aware (uses the enclosing function/class instead of a naive line count) when a parser is available, falling back to the naive cut otherwise; a short-timeout (150ms) LSP
textDocument/definitionlookup optionally appends real cross-file definitions for symbols in scope.
bash test/run.shRuns the full suite (vader for the Vim side, plenary for the Neovim side) — see CI for how it runs in GitHub Actions. No API key or network access is required; every test is either pure logic or mocks the API call.
- minuet-ai.nvim inspired one specific design decision — the 75/25
context_ratioweighting of the FIM prompt (more weight to the text before the cursor). No code was copied. - copilot.vim inspired the ghost-text technique (Vim 9's
prop_add/textprop APIs, debounced viatimer_start) — not its code.copilot.vimis "All Rights Reserved", not open-source, so only the publicly-documented Vim APIs were reused, independently implemented.
MIT — see LICENSE.
