English | 中文
Every editor — VS Code, Emacs, Sublime, JetBrains, Neovim — is a composition of the same eight things:
- A plugin manager that disappears into the background
- The editor's own knobs —
vim.opt, autocmds, the built-ins - A modal keymap layer you fully own
- A UI with taste — dashboard, statusline, indent guides, colorscheme
- An editing flow — pairs, file tree, picker, which-key, marks, terminal
- First-class language support — for every language, all eight of: Tree-sitter, LSP, completion, diagnostics, linter, formatter, DAP, snippets
- Version control that doesn't make you drop to a shell
- An AI stance — because it's 2026 and blindly accepting agent tooling is its own statement
This config covers the first seven directly. The eighth is now a deliberate omission.
You're already maintaining your own config. You're trying to organize 20+ language setups without the whole thing becoming a mess, you've outgrown copy-pasting from someone else's init.lua, or you want to see what a from-scratch architecture looks like at some scale.
Or: you're using LazyVim, NvChad, AstroVim, LunarVim and you're starting to run into its limits. You can't quite articulate what you want changed, but you know your editor is doing things you don't fully understand. This repo is a from-scratch alternative.
If you're brand new to Neovim, start with kickstart.nvim. Come back here in six months.
Requirements:
- Neovim 0.12+
- Git, the
tree-sitterCLI, a Nerd Font - a C compiler (for tree-sitter parsers)
ripgrep(required bygrug-farand other pickers).
git clone https://github.com/SOV710/nvim-config ~/.config/nvim
nvimlazy.nvim bootstraps itself on first launch, installs all plugins, and mason-tool-installer pulls every mason-managed package.
Then run:
:checkhealth langs
:TSInstall:checkhealth langs reports which non-mason external tools still need installing, with the install command for each. :TSInstall clones the tree-sitter parser sources declared by the language modules, generates parsers when needed, compiles them, and installs parsers plus queries into this config's managed Neovim runtime.
lazy.nvim. Spec files are organized by purpose under lua/plugins/{ui,editor,langs,git}/, each directory imported as a unit. Snacks modules live in separate spec files under lua/plugins/ui/; lazy.nvim merges them automatically.
lua/core/options.lua — a few dozen lines of vim.opt. If something can stay a default, it stays a default.
All keymaps live under lua/keymaps/, organized by feature. Plugin specs never call vim.keymap.set directly — they reference keymap files that return plain tables:
-- lua/plugins/editor/flash.lua
return {
'folke/flash.nvim',
event = 'VeryLazy',
opts = {},
keys = require('keymaps.editor.flash'),
}-- lua/keymaps/editor/flash.lua
return {
{ 's', mode = { 'n', 'x', 'o' }, function() require('flash').jump() end, desc = 'Flash jump' },
{ 'S', mode = { 'n', 'x', 'o' }, function() require('flash').treesitter() end, desc = 'Flash treesitter' },
}All keymaps are greppable from one directory.
A few notable bindings:
s/S→ flash (overrides native substitute, which I never use)m→ grapple toggle (overrides native mark)'→ grapple menu (overrides native mark jump)+/-→ dial (replaces<C-a>/<C-x>)<leader>g*→ git,<leader>h*→ hunks
Tokyo Night colorscheme throughout. The statusline and tabline are a custom heirline build — the tabline integrates grapple.nvim so marked files appear directly in the top bar.
| Component | Plugin |
|---|---|
| Colorscheme | folke/tokyonight.nvim |
| Statusline + tabline | rebelot/heirline.nvim (custom) |
| Icons | nvim-mini/mini.icons |
| Messages | folke/noice.nvim (messages only; cmdline stays inline at the bottom) |
| Dashboard, indent, scroll, statuscolumn, image | folke/snacks.nvim |
| Mode-aware cursorline | mvllow/modes.nvim |
| Color literals (hex, CSS, Tailwind) | brenoprata10/nvim-highlight-colors |
🚧 Showcase pending: full UI tour — dashboard → editing buffer → grapple-aware tabline (15s GIF)
| Component | Plugin |
|---|---|
| File explorer | stevearc/oil.nvim + snacks.explorer |
| Picker | snacks.picker (replaces telescope) |
| Motion | folke/flash.nvim |
| Surround | echasnovski/mini.surround |
| Pairs | windwp/nvim-autopairs |
| Match brackets / tags | andymass/vim-matchup |
| Text objects | nvim-treesitter-textobjects |
| Yank ring | gbprod/yanky.nvim |
| Comments | folke/ts-comments.nvim (treesitter-aware) |
| Marks (harpoon-style) | cbochs/grapple.nvim |
| LSP reference jumps | snacks.words |
| Scratch buffer | snacks.scratch |
| Split / join | Wansmer/treesj |
| Inc / dec | monaqa/dial.nvim |
| Undo tree | Neovim builtin nvim.undotree |
| Substitute | gbprod/substitute.nvim |
| Project search & replace | MagicDuck/grug-far.nvim |
| Terminal | akinsho/toggleterm.nvim |
| Which-key | folke/which-key.nvim |
🚧 Showcase pending: 10s flash.nvim jump GIF
🚧 Showcase pending: grapple marks appearing in heirline tabline (10s GIF)
Each supported language — Rust, Go, Python, TypeScript, Haskell, Lua, C/C++, LaTeX, Fish, SQL, and 20+ others — has a single file under lua/langs/. That file declares everything: LSP server config, tree-sitter parser source/build/query manifests, formatters, linters, DAP adapters, snippets, mason packages, file-type detection, and any language-specific plugins.
A typical lua/langs/<name>.lua:
return {
filetypes = { 'rust' },
treesitter = {
languages = {
rust = {
parser = {
source = {
type = 'git',
url = 'https://github.com/tree-sitter/tree-sitter-rust',
},
build = {
files = { 'src/parser.c', 'src/scanner.c' },
},
},
queries = {
sources = {
{ type = 'parser_source', lang = 'rust' },
},
},
},
},
},
lsp = {
rust_analyzer = {
settings = {
['rust-analyzer'] = {
checkOnSave = { command = 'clippy' },
},
},
},
},
formatter = 'rustfmt',
-- linter omitted: rust_analyzer covers it
dap = {
adapter = { codelldb = { type = 'server', port = '${port}', ... } },
configurations = { rust = { { name = 'Launch', type = 'codelldb', ... } } },
},
snippets = function()
local ls = require('luasnip')
return {
ls.snippet('pln', { ls.text_node('println!("'), ls.insert_node(1), ls.text_node('");') }),
}
end,
mason = { 'rust-analyzer', 'codelldb' },
external_deps = {
{ cmd = 'cargo', install = 'rustup default stable', required = true },
{ cmd = 'rustfmt', install = 'rustup component add rustfmt', required = true },
},
plugins = {
{ 'mrcjkb/rustaceanvim', version = '^5', lazy = false },
},
}lua/core/language.lua scans lua/langs/*.lua on require and aggregates the declarations into:
| Aggregated as | Consumed by |
|---|---|
language.formatters |
conform.nvim |
language.linters |
nvim-lint |
language.treesitter |
core.treesitter's native installer/runtime |
language.mason |
mason-tool-installer |
language.dap_adapters, language.dap_configurations |
nvim-dap |
language.snippets |
LuaSnip |
language.plugins |
injected directly into the lazy.nvim spec |
| LSP servers | registered via the native vim.lsp.config / vim.lsp.enable API — no nvim-lspconfig |
lua/core/treesitter.lua owns the tree-sitter pipeline. It provides :TSInstall, :TSUpdate, :TSStatus, and :TSClean; checks out each parser source from its declared git URL; runs tree-sitter generate when the manifest asks for it; compiles parser shared objects; installs query sources into a managed runtime; registers filetypes through vim.treesitter.language.register; and starts highlighting through Neovim's native vim.treesitter.start.
There is no nvim-treesitter parser registry, ensure_installed, or installer dependency in this config. Parser provenance lives in lua/langs/*.lua, not in an external plugin registry.
So conform's full spec is:
opts = {
formatters_by_ft = require('core.language').formatters,
}🚧 Showcase pending: architecture diagram —
lua/langs/*.lua→core/language.lua→ fan-out to consumers
Toggling languages. Each lang file accepts an enabled = false field. When set, the file is dropped during the scan and all downstream consumers (Tree-sitter, LSP, conform, lint, DAP, snippets, mason) lose it consistently. Useful for isolating which language broke after a plugin update.
External dependencies. Many language toolchains can't be managed by mason — tsgo builds from source, ty ships via PyPI, HLS via GHCup, fish-lsp via npm, sleek via cargo, and things like latexindent and chktex come bundled with TeX Live. Each lang file with non-mason deps documents them in two places:
- A top-of-file block comment with the install command and a one-liner to verify it.
- A structured
external_depsfield consumed by:checkhealth langs, which reports what's present and what's missing.
:checkhealth langs
==============================================================================
langs: require("langs.health").check()
haskell ~
- OK ghc
- OK cabal
- ERROR haskell-language-server: not found
- install: ghcup install hls recommended
- WARN haskell-debug-adapter: not found (optional)
- install: cabal install haskell-debug-adapter haskell-dap ghci-dap
python ~
- OK ty
rust ~
- OK cargo
- OK rustfmt
The plumbing per language:
| Layer | Plugin |
|---|---|
| Package manager | williamboman/mason.nvim + WhoIsSethDaniel/mason-tool-installer.nvim |
| Tree-sitter | core.treesitter + native vim.treesitter |
| LSP | vim.lsp.config + vim.lsp.enable (native, no nvim-lspconfig) |
| Completion | saghen/blink.cmp (Rust fuzzy matcher) |
| Snippets | L3MON4D3/LuaSnip + friendly-snippets |
| Formatter | stevearc/conform.nvim |
| Linter | mfussenegger/nvim-lint |
| Diagnostics | vim.diagnostic (native) + rachartier/tiny-inline-diagnostic.nvim |
| Diagnostics list | folke/trouble.nvim |
| DAP | mfussenegger/nvim-dap + theHamsta/nvim-dap-virtual-text |
Language-specific plugins. Injected into the lazy.nvim spec via each lang file's plugins field:
| Language | Plugin |
|---|---|
| Rust | mrcjkb/rustaceanvim, Saecki/crates.nvim |
| Haskell | mrcjkb/haskell-tools.nvim |
| C / C++ | p00f/clangd_extensions.nvim |
| LaTeX | lervag/vimtex |
| Clojure / Lisp | Olical/conjure |
| Markdown | MeanderingProgrammer/render-markdown.nvim |
| CSV / TSV | cameron-wags/rainbow_csv.nvim |
| JSON / YAML | b0o/SchemaStore.nvim |
| Ghostty | bezhermoso/tree-sitter-ghostty |
| Component | Plugin | Purpose |
|---|---|---|
| Hunks in the gutter | lewis6991/gitsigns.nvim |
]h / [h jump, <leader>hs stage hunk, blame line |
| Git commands | tpope/vim-fugitive |
:Git status, commit, push, pull, blame |
| Diff viewer | sindrets/diffview.nvim |
Visual diffs and file history |
| Open in browser | Snacks.gitbrowse |
<leader>gB to GitHub / Codeberg |
The four tools don't overlap: fugitive handles git commands, diffview handles viewing diffs, gitsigns handles in-buffer hunk operations, gitbrowse opens the web view.
In practice the plugin I reach for most in this section is toggleterm. Most git operations — stage, commit, rebase, push — happen in a shell buffer. The four plugins above exist to cover the cases a terminal is awkward for: inline blame, hunk navigation inside the current buffer, side-by-side diffs, and jumping to a commit on the web.
There is no AI plugin layer now. lua/plugins/ai/ was removed deliberately.
Claude Code has, for my purposes, become a piece of bullshit. Anthropic is no longer a company I trust or respect: the product direction feels hostile to the people using it, and Claude Code now carries far too much opaque telemetry and detection surface for something that wants to act as an agent in my development environment. Whether you call that spyware or simply hostile tooling, it is not acceptable as a layer inside this config.
The broader agent workflow has also moved on. Agents are leaving the old "Cursor mode" — editor sidebars, inline chat overlays, editor-owned workflows — and moving toward "chatbox mode": standalone tools such as Codex and opencode that own their own TUI/chat surface and treat the editor as context, not as the whole interface. In that world, claudecode.nvim, avante.nvim, and similar Cursor-style Neovim plugins add more coupling than value.
So AI remains part of the workflow, but outside Neovim. This config will not carry a dedicated AI plugin category for now.
- Drop a new file:
lua/langs/<name>.lua - Fill in the fields you need (everything is optional except
filetypes) - Restart Neovim
The aggregator picks it up on the next require; all downstream plugins see the new entries automatically.
If the new language declares a tree-sitter manifest, run :TSInstall <lang> after restart to fetch, build, and register its parser/query runtime.
To temporarily disable a language without deleting the file, add enabled = false at the top and restart. It disappears from all consumers — Tree-sitter, LSP, formatters, linters, DAP, snippets.
- Delete
lua/langs/<name>.lua— or just trim the unwanted entries out of itsmasonfield. - Restart Neovim.
- Run
:checkhealth langs. Thelangs.masonsection flags any mason package that's installed but no longer declared. - Run
:TSCleanif the removed language had a tree-sitter parser, so stale parser/query checkouts and runtime entries are pruned. - Remove orphans with
:MasonUninstall <pkg>(targeted) or:MasonToolsClean(all at once).:MasonToolsCleanalso removes packages belonging to a lang currently markedenabled = false, so prefer per-package uninstall if you have disabled langs.
init.lua -- lazy.nvim bootstrap, top-level setup
lua/
├── core/
│ ├── options.lua
│ ├── language.lua -- the aggregator
│ ├── treesitter.lua -- native parser/query installer and runtime
│ └── sysinfo.lua
├── keymaps/ -- every keymap, organized by feature
│ ├── init.lua
│ ├── editing.lua
│ ├── winbuf.lua
│ ├── snacks.lua
│ ├── which-key.lua
│ ├── editor/
│ ├── git/
│ ├── ai/
│ └── langs/
├── plugins/
│ ├── snacks.lua -- central Snacks registration
│ ├── ui/
│ ├── editor/
│ ├── langs/ -- completion, format, lint, treesitter, dap, mason, snippets
│ ├── git/
│ └── ai/
└── langs/ -- one file per language; the source of truth
├── rust.lua
├── python.lua
├── haskell.lua
└── ...
Thanks to @Lingshinx for the great ASCII art.
GPL-3.0-or-later. See LICENSE.
Every source file carries an SPDX-License-Identifier header.

