Personal Neovim configuration for Windows, organized into core settings, plugin configuration, and custom terminal support. It uses Neovim's built-in vim.pack package manager and sets both the global and local leader to Space.
See CHEATSHEET.md for the command and key reference
- Neovim 0.12 or newer
- Git
- ripgrep for Telescope live grep
- lazygit for the floating Git interface
styluaandprettieronPATHfor configured formatting- Git for Windows, for Git Bash and custom terminals —
bash.exeis auto-detected fromgitonPATHor the standard Git for Windows install locations (seelua/config/shell.lua) - Optional:
makefor Telescope's native FZF extension and supported plugin build hooks - Node.js, for
vtsls,eslint,oxlint, and:TSC - Optional: the Odin compiler on
PATH, forols's type checking and itscore/vendorcollections —olsandodinfmtthemselves come from Mason
Run :checkhealth kickstart to verify all of the above at once — each optional tool it reports names the single feature that stops working without it.
nvim-pack-lock.json is tracked in this repository so plugin commits are pinned across machines (:help vim.pack-lockfile).
| Path | Responsibility |
|---|---|
init.lua |
Loads every module in dependency order |
lua/config/options.lua |
Leaders, globals, and editor options |
lua/config/shell.lua |
Shared Bash, Command Prompt, and PowerShell profiles |
lua/config/keymaps.lua |
Global mappings and diagnostics |
lua/config/autocmds.lua |
General autocmds: yank highlight, terminal highlight, trim-trailing-whitespace on save |
lua/config/pack.lua |
vim.pack build hooks and GitHub URL helper |
lua/plugins/ui.lua |
Theme, Bufferline, statusline, Gitsigns, Which-key, and Mini |
lua/plugins/telescope.lua |
Search, picker, buffer, and LSP picker mappings |
lua/plugins/lsp.lua |
LSP attachment, servers, Fidget, and Mason |
lua/plugins/tsc.lua |
tsc.nvim: on-demand type-checking with the project's real tsc (:TSC) |
lua/plugins/formatting.lua |
Conform setup and manual formatting (<leader>f) |
lua/plugins/completion.lua |
Blink completion and LuaSnip |
lua/plugins/treesitter.lua |
Parser installation, attachment, and folding |
lua/plugins/neo-tree.lua |
File explorer setup |
lua/custom/terminal.lua |
Persistent split terminals and floating commands |
lua/custom/plugins/init.lua |
Auto-loads any extra plugin files dropped into lua/custom/plugins/ |
lua/kickstart/health.lua |
:checkhealth kickstart — Neovim version, required/optional external tools, and shell-profile resolution |
| Notation | Meaning |
|---|---|
<leader> |
Space |
<C-x> |
Hold Ctrl and press x |
<A-x> |
Hold Alt and press x |
<S-x> |
Hold Shift and press x |
| Normal | Normal mode |
| Visual | Visual or selection mode |
| Insert | Insert mode |
| Terminal | Terminal-input mode |
The shell used by :!, filters such as :.!command, and custom terminal windows is selected in lua/config/shell.lua:
M.selected = 'bash'Change that value to one of the configured profiles:
| Value | Shell | Example command |
|---|---|---|
'bash' |
Git Bash | :!ls |
'cmd' |
Command Prompt | :!dir |
'powershell' |
Windows PowerShell | :!Get-ChildItem |
Each profile sets its executable, command flag, quoting, redirection, and terminal arguments together. Restart Neovim after changing the selected profile.
Useful forms:
:!commandruns a command and displays its output.:.!commandreplaces the current line with the command output.:read !commandinserts the command output below the current line.:%!commandfilters the entire buffer through the command.:set shell? shellcmdflag?displays the active executable and command flag.
| Mode | Key | Action |
|---|---|---|
| Normal | <S-h> |
Open the previous buffer |
| Normal | <S-l> |
Open the next buffer |
| Normal | <leader>v |
Create a vertical split |
| Normal | <leader>h/j/k/l |
Focus the window to the left/down/up/right |
| Normal | <C-h/j/k/l> |
Focus the window to the left/down/up/right |
| Normal | <A-j> |
Move the current line down and reindent it |
| Normal | <A-k> |
Move the current line up and reindent it |
| Visual | <A-j> |
Move the selected lines down and preserve the selection |
| Visual | <A-k> |
Move the selected lines up and preserve the selection |
| Normal | <Esc> |
Clear search highlighting |
| Visual | s |
Replace the selection, using Vim's c operation |
| Key | Action |
|---|---|
<leader>bn |
Open the next buffer |
<leader>bp |
Open the previous buffer |
<leader>bd |
Delete the current buffer, leaving its window in place |
<leader>bb |
Create a new empty buffer |
<leader>sb |
Search open buffers with Telescope |
Deleting a buffer never takes its window with it. :bdelete on its own closes every window showing the buffer, so with the terminal panel open the editor window above it would disappear and the 12-line panel would stretch over the whole screen. <leader>bd points those windows at something else first -- the buffer they were on before, the next one in the buffer list, or an empty buffer once the last file is gone -- so the split layout survives the delete. lua/custom/buffer.lua has it.
| Key | Action |
|---|---|
\ |
Reveal the current file in Neo-tree |
\ inside Neo-tree |
Close the Neo-tree window |
<leader>e |
Toggle Neo-tree and reveal the current file |
Useful commands:
:Neotree revealreveals the current file.:Neotree toggle revealtoggles the explorer while revealing the current file.
| Mode | Key | Action |
|---|---|---|
| Normal | <leader><leader> |
Find files |
| Normal | <leader>sh |
Search Neovim help tags |
| Normal | <leader>sk |
Search active keymaps |
| Normal | <leader>sf |
Find files |
| Normal | <leader>ss |
Select a Telescope picker |
| Normal/Visual | <leader>sw |
Search for the word under the cursor or selection |
| Normal | <leader>sg |
Search project text with live grep, with ripgrep flag support (live-grep-args) |
| Normal | <leader>sd |
Search diagnostics |
| Normal | <leader>sr |
Resume the previous Telescope picker |
| Normal | <leader>s. |
Search recently opened files |
| Normal | <leader>sc |
Search available commands |
| Normal | <leader>sn |
Search files in the Neovim configuration |
| Normal | <leader>/ |
Fuzzy-search the current buffer in a dropdown |
| Normal | <leader>s/ |
Live-grep only the currently open files |
Use :Telescope keymaps to inspect all active mappings or :Telescope builtin to choose any available picker.
<leader>sg uses telescope-live-grep-args.nvim, which lets ripgrep flags be typed directly into the prompt.
The flags must come first, or the search word must be quoted first. The plugin decides how to parse the whole prompt based on its first character: if the prompt doesn't start with -, ', or ", the entire line — search word and all — is treated as one literal search string and flags are not applied. So useState -tjs silently searches for the literal text useState -tjs instead of filtering. Put the flag(s) before the search word instead, or quote the word first (which is what <C-k> does for you).
Example prompts:
| Prompt | Effect |
|---|---|
-tjs useState |
Search useState, only in JavaScript files |
-Tjs useState |
Search useState, in every file except JavaScript |
-g '*.md' foo |
Search foo, only in Markdown files |
-g '!vendor/*' foo |
Search foo, skip anything under vendor/ |
-tlua -g '!vendor/*' foo |
Search foo, only in Lua files, and skip vendor/ |
"TODO: fix" -tjs |
Search the exact phrase TODO: fix, only in JavaScript files (quoting first also works) |
-t (lowercase) means "only these file types"; -T (uppercase) means "every file type except these." -g matches by filename/path glob instead of file type, and a leading ! in the glob excludes rather than includes. Flags can be combined in one prompt.
The LSP mappings below are buffer-local and appear only after a language server attaches.
| Mode | Key | Action |
|---|---|---|
| Normal | <leader>q |
Put diagnostics into the location list |
| Normal | <leader>d |
Open a focused diagnostic float for the current line |
| Normal | grn |
Rename the symbol under the cursor |
| Normal/Visual | gra |
Request a code action |
| Normal | grr |
Find references with Telescope |
| Normal | gri |
Find implementations with Telescope |
| Normal | grd |
Find definitions with Telescope |
| Normal | grD |
Go to the declaration with LSP |
| Normal | grt |
Find type definitions with Telescope |
| Normal | go |
Search document symbols |
| Normal | gw |
Search workspace symbols |
| Normal | grx |
Run the code lens under the cursor |
| Normal | <leader>th |
Toggle inlay hints when the server-support check succeeds |
| Normal | <leader>tl |
Toggle code lens for the current buffer |
grn, gra, grr, gri, grt, and grx are Neovim defaults (0.11+), not defined by this config. grr, gri, grt, and grd are re-mapped buffer-locally in lua/plugins/telescope.lua so results land in a Telescope picker instead of the quickfix list; grD is added in lua/plugins/lsp.lua because Neovim has no default for it.
Code lens is enabled on attach for servers that support it (lua/plugins/lsp.lua) — Neovim keeps it off by default, so the referencesCodeLens and implementationsCodeLens settings requested from vtsls would otherwise never render. <leader>tl turns it off per buffer.
Configured language servers:
- TypeScript:
vtsls, for every project regardless of its own TypeScript version.tsc(TypeScript 7+'s native--lspserver) was tried and removed after it crashed independently in testing and in real use — see commitf5e9eb3to revive the per-project version gate if that ever proves stable. - Linting:
eslintandoxlint, run alongside each other; fixes are applied on demand via:LspEslintFixAlland:LspOxlintFixAll - HTML (
html) - CSS (
cssls) - Lua (
lua_ls) - Odin (
ols)
Use :Mason to inspect installed language tooling. Diagnostic jumps use Neovim's normal diagnostic mappings; the configuration opens a rounded diagnostic float after a jump.
A long diagnostic message (a TypeScript union type, especially) can run off the edge of the window: inline diagnostics (virtual_lines, scoped to the current line only — other lines just get an underline) don't wrap, by design — 'wrap' does not apply to virtual lines (:h nvim_buf_set_extmark()). <leader>d is the reliable way to read one in full: it opens the same kind of floating window as hover, which does wrap. Press it once to see the float, press it again to move the cursor inside — from there it's a normal (read-only) buffer, so y/visual-select copies out of it normally.
Neovim 0.12 ships a native :lsp command, which makes nvim-lspconfig skip its plugin/ script — so :LspInfo and :LspStart/:LspStop/:LspRestart do not exist (:lsp and :checkhealth vim.lsp replace them). :LspLog is redefined in lua/plugins/lsp.lua to open the client log at its newest entries. :LspEslintFixAll and :LspOxlintFixAll are unaffected — those are buffer-local commands created from each server's on_attach, not from the skipped plugin/ script.
eslint, oxlint, html, and cssls get their cmd from node_bin_cmd in lua/plugins/lsp.lua instead of taking nvim-lspconfig's default. Upstream prefers a project-local server by joining <root>/node_modules/.bin/<name>, which on Windows is npm's POSIX shell-script shim — executable() reports it as runnable, then the spawn fails with "The language server is either not installed, missing from PATH, or not executable." node_bin_cmd looks for the .cmd (then .exe) shim npm writes alongside it and falls back to Mason/PATH if neither exists. Off Windows it resolves exactly as upstream does.
vtsls always type-checks against its own bundled TypeScript, never the project's own pinned version — TypeScript 7's package no longer ships tsserver.js at all, so there's no setting that changes this. lua/plugins/tsc.lua (tsc.nvim) closes that gap on demand: :TSC runs the project's real, pinned tsc --noEmit across every tsconfig.json in the tree and reports results as both quickfix entries and normal diagnostics. It never runs automatically — only on :TSC; :TSCStop cancels a run in progress.
| Mode | Key | Action |
|---|---|---|
| Normal/Visual | <leader>f |
Format the buffer or selection synchronously with Conform |
Formatting is manual: there is no format-on-save, so <leader>f is the only way to format. It's configured for Lua, Odin, JavaScript, JSX, TypeScript, TSX, HTML, CSS, JSON, and JSONC. Lua uses stylua, Odin uses odinfmt; the other configured filetypes use prettier. LSP formatting is used as a fallback.
odinfmt is defined by hand in lua/plugins/formatting.lua — Conform has no built-in for it, and it comes from Mason's ols package rather than from PATH. It reads its settings from an odinfmt.json found by walking up from the process working directory, and under -stdin it is never told which file it is formatting; Conform therefore runs it from the buffer's own directory, so a project's odinfmt.json applies no matter where Neovim's :pwd happens to be. Without one, odinfmt's own defaults (tabs, 100 columns) apply.
Use :ConformInfo to see the formatter selected for the current buffer and whether its executable is available.
Trailing whitespace is stripped automatically on every save (lua/config/autocmds.lua), independent of Conform. Markdown and diff buffers are excluded, since Markdown uses trailing double-spaces as an intentional hard line break.
| Mode | Key | Action |
|---|---|---|
| Insert | <C-l> |
Explicitly show the Blink completion menu |
| Insert | <C-y> |
Accept the selected completion using Blink's default preset |
| Insert | <C-Space> |
Open completion, or documentation when completion is already open |
| Insert | <C-n> / <C-p> |
Select the next or previous completion item |
| Insert | <Down> / <Up> |
Select the next or previous completion item |
| Insert | <C-e> |
Hide completion |
| Insert | <C-k> |
Toggle signature help |
| Insert/Snippet | <Tab> / <S-Tab> |
Move forward or backward through snippet positions |
Completion sources are LSP, filesystem paths, and LuaSnip snippets. Documentation does not open automatically, while signature help is enabled.
| Mode | Key | Action |
|---|---|---|
| Operator/Visual | aa |
Use the next “around” text object |
| Operator/Visual | ii |
Use the next “inside” text object |
| Normal/Visual | gsa |
Add surrounding characters |
| Normal | gsd |
Delete surrounding characters |
| Normal | gsf |
Find a surrounding character |
| Normal | gsh |
Highlight surrounding characters |
| Normal | gsr |
Replace surrounding characters |
| Normal | gsn |
Change the number of lines searched for surroundings |
vim-visual-multi is installed with its upstream default mappings. Use :help vm-mappings for its complete multi-cursor key reference.
Folds are calculated from Treesitter syntax and start open, so files remain fully visible until a fold command is used.
| Mode | Key | Action |
|---|---|---|
| Normal | zc |
Close the fold under the cursor |
| Normal | zo |
Open the fold under the cursor |
| Normal | za |
Toggle the fold under the cursor |
| Normal | zM |
Close all folds |
| Normal | zR |
Open all folds |
| Normal | zj |
Move to the next fold |
| Normal | zk |
Move to the previous fold |
| Mode | Key | Action |
|---|---|---|
| Normal | <leader>` |
Focus the terminal split, or hide it when it already has focus; creates one when necessary |
| Normal | <leader>tf |
Show the terminal full screen on its own tab page, or close that tab when already on it |
| Normal | <leader>tn |
Create a new terminal |
| Normal | <leader>tv |
Create a new terminal in a vertical split beside the current pane |
| Normal | <leader>t] |
Show the next managed terminal |
| Normal | <leader>t[ |
Show the previous managed terminal |
| Normal | <leader>tc |
Close the current managed terminal; the last terminal is protected |
| Normal | <leader>gg |
Open lazygit in a centered floating terminal |
| Terminal-Normal | i |
Enter Terminal mode and type commands |
| Terminal | <Esc><Esc> |
Leave terminal-input mode and return to Normal mode |
Managed terminals open in Terminal-Normal mode in a 12-line bottom split, remain alive when hidden, and use Git Bash as an interactive login shell. <leader>tf is the same terminal at a second size: it moves onto a tab page of its own, where it fills the editor area, and the layout it came from waits untouched on the tab underneath. This lets terminal switching and window-navigation mappings work immediately; press i when you want to type. Floating commands close their window automatically when the command exits.
Inside lazygit, o and e open the file under the cursor in this Neovim, in the window <leader>gg was pressed from, and the float closes with it. lua/custom/lazygit.lua arranges that: it writes a lazygit config with this instance's RPC address baked in and points lazygit at it with LG_CONFIG_FILE, next to the personal config in %LOCALAPPDATA%\lazygit\config.yml. Without it o hands the file to whatever application the extension is associated with, and e opens a second Neovim nested inside the float.
| Command | Purpose |
|---|---|
:lua vim.pack.update(nil, { offline = true }) |
Inspect plugin state and pending updates without fetching |
:lua vim.pack.update() |
Fetch and update managed plugins |
:Mason |
Inspect and manage external LSP tools |
:TSUpdate |
Update installed Treesitter parsers |
:Telescope keymaps |
Search all active keymaps |
:ConformInfo |
Inspect formatting for the current buffer |
:checkhealth |
Run Neovim and plugin health checks |
:checkhealth kickstart |
Check this config's own required and optional external tools |
:messages |
Review recent notifications and errors |
Which-key opens immediately after a recognized prefix and groups search under <leader>s, terminal actions under <leader>t, and LSP actions under gr.