My Neovim configuration: a few pinned plugins, one leader namespace, and a
written account of what it changes over a stock nvim configuration.
It runs wherever Neovim runs and does not care where that is. The config
works in the terminal, it works in GUIs like Neovide, and it stays out of
my way, unless I'm about to do something smart, like :restart in a
Neovide instance that happens to be wrapping a Neovim session on another
host.
This is a plain Neovim configuration: clone it where Neovim looks and run
nvim.
git clone https://github.com/azazeal/mivn.git ~/.config/nvim
The first start clones the plugins at their pinned revisions, which takes a
few seconds; then run :MivnInstallGrammars once for the grammars. There is
nothing else to fill in: this config has no personal-settings file, because
it is the settings.
To try it without touching an existing configuration, clone it anywhere and run it isolated under a different name:
git clone https://github.com/azazeal/mivn.git ~/.config/mivn
NVIM_APPNAME=mivn nvim
The configuration wants Neovim v0.12.5 or newer. Other than that, it relies on
the following (the first three on PATH):
git, whichvim.packuses to fetch the pinned plugins on first start.- A C compiler like
cc,gcc, orclang, which:MivnInstallGrammarsneeds to compile the tree-sitter grammars stock Neovim does not come with. Skipping it breaks nothing: until that command runs, the grammars Neovim already ships with keep highlighting and every other language falls back to classic syntax highlighting. ripgrep(orfd), optionally:<Space>fand<Space>/use whichever is installed, and fall back to slower built-ins otherwise.- A Nerd Font, for the icons in the tree, the tab bar, and the pickers. See Fonts for where the font is actually chosen.
Everything mivn adds is under one prefix, so :Mivn and <Tab> is the whole
list from inside the editor.
| Command | What it does |
|---|---|
:checkhealth mivn |
Runs every configured server and formatter once and reports which ones actually answer |
:MivnTrust [action] [dir] |
Decides whether this workspace may have code run for it. See Trust |
:MivnInstallGrammars |
Compiles the tree-sitter grammars mivn knows about, and repairs any that lost their queries. Run once after cloning |
:MivnUpdateGrammars |
Updates every installed grammar |
:MivnUpdate |
Pulls the newest mivn, if this checkout has no changes of its own |
:MivnDashboard |
Opens the landing buffer again |
:MivnBdAll |
Closes every file buffer and leaves the panels standing |
:MivnBdOthers |
Closes every file buffer except this one |
:MivnRestart |
What :restart becomes, so the panels step aside while the session is written |
:MivnRestartRemote |
What :restart becomes when the window is on another machine |
:MivnTreeBd |
What :bd typed inside the tree becomes. It closes nothing and says how to leave the tree instead |
Three of those are reached by their short spellings instead, because that is
what the fingers already type: :bda is :MivnBdAll, :bdo is
:MivnBdOthers, and a bare :%bd is rewritten to :MivnBdAll so the tree
and the terminal survive it. Stock :bd is untouched.
:MivnUpdate implements the manual half of a check that runs on its own: once a
day, at most, mivn asks the remote whether there is a release tag it does not
have, using one git ls-remote that fetches nothing and writes nothing into the
repository. It only considers releases and not commits.
Every language server comes from PATH, and this config installs none of
them: a server that is not on it is a language with tree-sitter colors and
nothing else, said once in :checkhealth mivn. Which toolchain a session
gets is the launcher's business, not this config's.
The set in use is Go (gopls, golangci-lint-langserver), Python (ty,
ruff), TypeScript (tsgo), Rust, Lua, Elixir, Gleam, Shell, Markdown,
TOML, YAML (yaml-language-server, actions-languageserver, zizmor), JSON,
HTML (vscode-html-language-server, superhtml), CSS, Terraform, Tilt,
Protocol Buffers, templ and the Docker files; XML has a formatter and no
server. The external formatters (stylua, shfmt, jq, taplo, yamlfmt,
dockerfmt, xmllint, rumdl) and gci for Go imports are looked up the
same way.
What this config owns is everything around that: what each server is told once
it starts, what runs after it (gci re-groups Go imports after the language
server has formatted, unless $GOIMPORTNOGCI says otherwise), and which JSON Schema a
file gets. One file per language under lua/mivn/languages/ holds all of it for
that language. :checkhealth mivn reports the lot.
What a server says about the line the cursor is on is drawn under that line
and wrapped to the width of the window, so a long message is read where it is
rather than scrolled sideways to. One too long for that ends with a … and
Ctrl+W d opens all of it in a float. DEFAULTS.md has the rest.
Servers run with the permissions you do, and several of them run the
repository's own code to answer questions about it: rust-analyzer builds
build.rs and expands proc macros, expert compiles mix.exs, gopls shells
out to the toolchain. So opening a repository is running it, and the workspace
has to be trusted before any of that starts. Until it is, no server starts and
nothing formats on save; files open, tree-sitter colors them, and the editor
is an editor.
The workspace is the directory the editor is working in, not the root each server picks for itself, so one answer covers everything under it, including a dependency's source or the standard library reached from inside it.
:MivnTrust ... |
What it does |
|---|---|
allow, or no argument at all |
Trusts this workspace and starts its servers there and then |
deny |
Records that it must not run, which covers everything under it |
forget |
Drops the decision, so the question is open again |
status |
Says where this directory stands, and which directory decided it |
A directory can be named as a second argument; without one the command means
the workspace. Decisions live in Neovim's own trust list, the file :trust
writes, so :trust still reads them. :checkhealth mivn lists every
directory decided about, either way.
It is a gate and not a sandbox: a server that does start runs as you.
Every key mivn takes is in one file, lua/mivn/keymaps.lua, and <Space>?
lists them from inside a running editor along with everything Vim and the
plugins bind. Holding a prefix for a moment shows what can follow it, so the
tables below are the same list the editor will show you.
Six keys sit directly on the leader and the rest are grouped, so what you remember is three words rather than two dozen keys: code, goto, toggle.
All six open the same floating window, so its own keys are learned once.
| Key | Opens |
|---|---|
<Space>f |
Find file |
<Space>/ |
Search the project |
<Space>b |
Open buffers |
<Space>: |
Command palette |
<Space>h |
Help |
<Space>? |
Every key, searchable |
| Key | What it does |
|---|---|
<Space>aa |
Code action, on the line or on the selection |
<Space>ar |
Rename symbol |
<Space>af |
Format this buffer |
<Space>aF |
Organize imports |
<Space>ai |
Hover documentation |
<Space>ax |
Run the code lens on this line |
<Space>ad |
Diagnostics in this buffer |
<Space>aD |
Diagnostics in the workspace |
| Key | Where |
|---|---|
<Space>gd |
Definition |
<Space>gD |
Declaration |
<Space>gi |
Implementation |
<Space>gt |
Type definition |
<Space>gr |
References |
<Space>gs |
Symbols in this document |
<Space>gS |
Symbols in the workspace |
One answer jumps straight there. Several open in the same floating window as the finders above, rather than in a quickfix split that rearranges the layout.
Neovim's own LSP keys are taken off, grn, gra, grr, gri, grt, grx,
gO and K included, gra over a selection too. Two keys for one request is
two things to keep in step, and these chains are the ones with a panel behind
them. Ctrl+S while typing, signature help, is the one Neovim key kept: it
has no twin here.
| Key | What it flips |
|---|---|
<Space>tt |
The file tree |
<Space>t` |
The terminal |
<Space>tw |
Wrapping long lines, in this window |
<Space>th |
Dotfiles, in the tree and the finders at once |
<Space>ti |
Files the SCM ignores, in the tree and the finders at once |
<Space>tr |
The old text, inline, for every line I have changed |
<Space>tb |
Who wrote the line I am on |
<Space>tn |
The inlay hints, in this buffer |
<Space>t? |
Nothing; it says which of the six flags are on |
The dotfile and ignored keys answer for both views of a directory, so a file drawn in one and missing from the other cannot happen.
Every key here says which way it went, except the tree and the terminal: those
two put a panel on screen, and being told what I am looking at is noise. The
rest are flags with nowhere to show themselves, and review is the clearest
case, since on a file I have not changed it looks the same on as off. ? asks
the six of them at once, keyed by the letter that flips each, and answers
(b: on, h: shown, i: hidden, n: off, r: off, w: off).
Keys that already meant something in stock Vim and mean something else here:
| Key | What it does here |
|---|---|
| Shift and an arrow | Selects, by a character or a line |
Ctrl+→ / Ctrl+← |
Past the end of the word / the start of the previous one, never the WORD |
Alt+→ / Alt+← |
The same, by subword |
| Ctrl or Alt and Shift | The same two distances, selecting |
e E ge gE |
Past the end of the word or WORD, rather than on its last character |
Home |
Alternates between the indent and column zero |
End |
Past the end of the line, the boundary after the last character |
Ctrl+Home / Ctrl+End |
Column one of the first line / past the last character of the last |
Insert |
Whichever of Insert and Normal you are not in |
| PageUp / PageDown | A page, or the first and last line when there is no page left |
| Shift+PageUp / PageDown | Select a page, stopping at the first and last line |
Tab / Shift+Tab over a selection |
Indent / dedent it, and keep it selected |
u over a selection |
Undo, rather than lowercase |
` / Ctrl+` / Alt+` over a selection |
Toggle its case / uppercase / lowercase |
Enter, Tab, Ctrl+Space while typing |
Take a completion, take the top one, open the menu |
Ctrl+Tab / Ctrl+Shift+Tab |
Along the tab bar of buffers |
Ctrl+↑ / Ctrl+↓ |
Move the line, or the selected lines |
Ctrl+Del |
Delete the word ahead, typing or not |
Alt+D / Alt+C |
Delete or change onto the clipboard |
( [ { " ' ` over a selection made while typing |
Wrap it |
{count} and | |
That column, counted in characters rather than screen cells |
Up / Down on the command line |
Walk the completion menu while it is open |
Esc in Normal mode |
Also clears leftover search highlighting |
ZR |
:restart, which comes back to the same layout, unless the window is on another machine |
y and p are the system clipboard; d and c are not. A selection opened
while typing lands in Select mode, where what you type next replaces it; one
opened from Normal mode lands in Visual, where the whole grammar applies and a
stray letter is a command.
One rule holds every key in that table to the same meaning in every mode: a
key means what it means whether I am typing or not, and pressed with a
selection up it drops the selection and then does the same. The caret is a
boundary between characters, so every key that goes to the end of something
lands to the right of its last character, in Normal, Insert, Visual and
Select alike. .github/scripts/keys prints the table of what is bound where,
and .github/scripts/motions presses the keys on a real editor and checks
where the caret lands.
Everything else is stock Vim, or a stock option doing its documented job. DEFAULTS.md is the full account, marks every deviation, and says what each one costs.
Nothing here sets a font, on purpose. In the terminal the font is the
terminal's. In Neovide it comes from Neovide's own
~/.config/neovide/config.toml. A font and its size are a per-machine,
per-screen choice, and an editor config has no way to make that choice well;
Neovim cannot even ask the compositor for the monitor's physical size.
Under Neovide, Ctrl+=, Ctrl+- and Ctrl+0 zoom in, out and back to 100%;
the numpad's +, - and 0 do the same. They scale what that file asked
for rather than writing a size anywhere, so 100% keeps meaning whatever the
file says. The step is foot's, near enough, and it stops at half and at
triple. In a terminal the keys are not mivn's to take: foot has them and
resizes its own font.
Neovim can run on one machine with its window on another: Neovide attaching
over ssh, with --neovim-bin pointing at a wrapper that runs the editor on
the far host. One command must not work there. :restart starts the new
editor on the machine the old one ran on and hands the window an address on
that machine's filesystem, so the window dies trying to connect and a
headless editor is left running behind it; Neovim's own docs call out the
same-system limit. mivn refuses :restart and ZR in that setup and says
why.
Export MIVN_REMOTE_UI=1 in whatever launches the remote session to declare
it. Without the variable, a remote Neovide is still recognized by the
clipboard bridge it registers, but the variable is the supported switch: it
survives Neovide configuration changes and covers UIs this config has never
heard of.
| Path | What it holds |
|---|---|
init.lua |
Options, and the order every module loads in |
lua/mivn/ |
One module per concern |
lua/mivn/keymaps.lua |
Every key that is on for the whole session |
lua/mivn/languages/ |
One file per language: its servers, settings and formatting |
colors/ |
The basalt theme |
queries/ |
Tree-sitter extras: SQL in Go strings |
.github/scripts/ |
The checks CI runs, and the release and repin tools |
.github/checks/ |
The cases of the checks that drive a real editor |
DEFAULTS.md |
The tour of stock Vim, and what mivn changes, marked so |
TODO.md |
State and queue; friction lands here first |
