docs(spec): add nvim modernization design

Plan a three-phase modernization: triage critical bugs and remove
abandoned plugins, swap notify+dressing for snacks UI, and replace
nvim-cmp with blink.cmp + telescope with snacks.picker.
This commit is contained in:
David Ibia
2026-05-02 06:04:31 +01:00
parent 13ddbe2f56
commit 5d9852f4ee
@@ -0,0 +1,369 @@
# Neovim Config Modernization — Design Spec
**Date:** 2026-05-02
**Target Neovim version:** 0.12.x
**Branch strategy:** Single branch, three sequential commits with restart-and-validate between each.
## Goals
1. Fix four silent breakages currently in the running config.
2. Remove abandoned, archived, or superseded plugins.
3. Swap two plugin engines that have been overtaken by snacks.nvim and blink.cmp in the 2026 ecosystem.
4. End up with a smaller, less-redundant `lua/absolute/` tree that is consistent with Neovim 0.12 idioms.
## Non-goals
- Migrating `nvim-treesitter` from frozen `master` to the 0.12-only `main` rewrite. Deferred to a separate, future spec.
- Restructuring the `absolute/` module layout. The current `core / plugins / after / utils / helpers` split is preserved.
- Adding new AI plugins. All AI integrations (`opencode`, `codecompanion`, `copilot`, `gen`, `llm`) are removed in this spec.
- Changing colorscheme behavior or the `theme-registry` system, which is recent and intentional.
## Scope summary
| Phase | Theme | Risk | Files touched |
|---|---|---|---|
| 1 | Triage and deletions | Low | ~20 files (mostly deletes) |
| 2 | UI swap (notify + dressing → snacks) | Low | 4 files |
| 3 | Engine swaps (nvim-cmp → blink, telescope → snacks.picker) | Medium | ~15 files |
Each phase is one commit on the same branch. Restart Neovim and validate after each commit before starting the next.
---
## Phase 1 — Triage and deletions
### Critical fixes
**C1 — `lua/absolute/remap.lua`: keymap collision on `<leader>j` / `<leader>k`.**
Lines 70–73 bind `<leader>k`/`<leader>j` to `lnext`/`lprev`. Lines 120–124 rebind the same keys to window resize, silently overriding them. The window-resize 4-way pad (`<leader>h/j/k/l`) is the intentional binding.
**Action:** Delete lines 70–73. If location-list nav is wanted, rebind to the idiomatic `]l` / `[l` pair (next / previous location-list entry). Decision in this spec: rebind to `]l` / `[l`.
**C2 — `lua/absolute/after/lsp-config.lua`: deprecated diagnostic-sign API.**
Lines 73–83 use `vim.fn.sign_define` to set diagnostic gutter icons. This API is deprecated in 0.10 and is removed in 0.12.
**Action:** Replace with the modern `vim.diagnostic.config` table:
```lua
vim.diagnostic.config({
signs = {
text = {
[vim.diagnostic.severity.ERROR] = " ",
[vim.diagnostic.severity.WARN] = " ",
[vim.diagnostic.severity.HINT] = "󰠠 ",
[vim.diagnostic.severity.INFO] = " ",
},
},
})
```
**C3 — Orphan after-file `lua/absolute/after/nvim-tree.lua`.**
There is no matching plugin spec in `lua/absolute/plugins/`. The file is dead but will crash on `require` if anything ever loads it.
**Action:** Delete the file.
**C4 — Non-idiomatic require in `lua/absolute/lazy.lua:36`.**
`require("../current-theme")` uses a literal-slash module name. The file is at `lua/current-theme.lua`, which is on the runtime path; the canonical form is `require("current-theme")`.
**Action:** Change to `require("current-theme")`.
### Plugin removals
The following plugin specs and their after-files are deleted. lazy.nvim removes them from `lazy-lock.json` automatically on the next `:Lazy sync`.
| Plugin | Files deleted | Reason |
|---|---|---|
| `playground` | `plugins/nvim-treesitter-playground.lua` | Repo archived 2024; replaced by built-in `:Inspect` / `:InspectTree` / `:EditQuery` |
| `vim-maximizer` | `plugins/vim-maximizer.lua` | Last commit 2013; native `<C-w>_` and `<C-w>\|` cover the use case |
| `poet-v` | `plugins/poet-v.lua`, `after/poet-v.lua` | Abandoned; `venv-selector.nvim` covers Python envs |
| `git-blame.nvim` | `plugins/git-blame.lua` | Replaced by gitsigns `current_line_blame` |
| `render-markdown.nvim` | `plugins/render-markdown.lua` | Picked `markview.nvim` as single markdown renderer |
| `codecompanion.nvim` | `plugins/codecompanion.lua`, `after/codecompanion.lua` | AI cleanup |
| `copilot.vim` | `plugins/copilot.lua` | AI cleanup |
| `gen.nvim` | `plugins/gen.lua`, `after/gen.lua` | AI cleanup |
| `llm.nvim` | `plugins/llm.lua`, `after/llm.lua` | AI cleanup |
| `opencode.nvim` | `plugins/opencode.lua`, `after/opencode.lua` | AI cleanup |
`dressing.nvim` and `nvim-notify` removal is deferred to Phase 2 because both are *swapped*, not deleted.
### New file: `lua/absolute/plugins/snacks.lua`
Removing `opencode.nvim` removes the dependency that pulled `snacks.nvim` into the tree. snacks is required for Phase 2 (input, notifier) and Phase 3 (picker), so a top-level spec is added now.
```lua
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
---@type snacks.Config
opts = {
bigfile = { enabled = true },
quickfile = { enabled = true },
-- input, notifier added in Phase 2
-- picker added in Phase 3
},
}
```
### Config tweak: gitsigns line-blame
`lua/absolute/after/gitsigns.lua` is updated to enable virtual-text blame on the current line, replacing the functionality lost when `git-blame.nvim` is removed:
```lua
current_line_blame = true,
current_line_blame_opts = {
virt_text_pos = "eol",
delay = 500,
ignore_whitespace = false,
},
```
### Untracked work
- `lua/absolute/core/theme-registry.lua` is committed as-is (already complete).
- `lua/absolute/plugins/flash.lua` is committed as-is, retaining `enabled = false`.
- `lua/absolute/after/opencode.lua` is deleted in this phase along with the rest of opencode.
### Disabled-spec policy
Seven plugin specs currently have `enabled = false` and look intentional:
`apple-pkl`, `flash`, `flutter-tools`, `mini`, `nvim-surround`, `smear-cursor`, `tailwind-sorter`.
These are left untouched — the user toggles them on for specific projects. They are not part of this modernization.
### Phase 1 net change
- 16 plugin spec or after-files deleted
- 1 file added (`plugins/snacks.lua`)
- 4 files edited (`remap.lua`, `after/lsp-config.lua`, `after/gitsigns.lua`, `lazy.lua`)
- ~10 plugins removed from lockfile
### Phase 1 validation
After committing Phase 1, restart Neovim and verify:
1. No errors on startup (`:messages` empty of `E:` lines).
2. Diagnostic signs render (open a Lua file with an error).
3. `:Lazy clean` removes the orphaned plugins from disk.
4. `<leader>j` and `<leader>k` resize the current window.
5. `]l` / `[l` jump in location list.
6. Gitsigns shows current-line blame after a 500 ms idle.
7. `:Inspect`, `:InspectTree`, `:EditQuery` all work (proves treesitter playground replacement).
---
## Phase 2 — UI swap (notify + dressing → snacks)
Both replaced plugins are tiny. Migration is mostly deletes plus a snacks config bump.
### Deletions
| File | Notes |
|---|---|
| `lua/absolute/plugins/notify.lua` | Lazy spec |
| `lua/absolute/after/notify.lua` | Configures `vim.notify = notify` |
| `lua/absolute/plugins/dressing.lua` | Trivial `VeryLazy` spec, no after-file |
`nvim-notify` and `dressing.nvim` drop from the lockfile.
### Update `lua/absolute/plugins/snacks.lua`
Add `input` and `notifier` modules. Notifier config preserves the icons and timeout from the existing nvim-notify setup:
```lua
opts = {
bigfile = { enabled = true },
quickfile = { enabled = true },
input = { enabled = true },
notifier = {
enabled = true,
timeout = 3000,
style = "compact",
top_down = true,
icons = {
debug = "",
error = "",
info = "",
trace = "✎",
warn = "",
},
},
}
```
snacks installs `vim.notify = Snacks.notifier.notify` and overrides `vim.ui.input` automatically when the modules are enabled. No manual hookup is needed.
### Settings dropped (no equivalent needed)
`background_colour`, `fps`, `render`, `stages`, `minimum_width`, `time_formats` from the old nvim-notify config — all subsumed by snacks defaults or replaced by the `style = "compact"` choice.
### Phase 2 net change
- 3 files deleted
- 1 file edited (`plugins/snacks.lua`)
- 0 keymaps changed
- 2 plugins removed from lockfile
### Phase 2 validation
1. Restart Neovim. No errors on startup.
2. Trigger a notification (e.g. `:lua vim.notify("test", vim.log.levels.INFO)`) — toast appears in the top-right.
3. Trigger a `vim.ui.input` (e.g. `<leader>rn` to LSP-rename, or `<leader>gr` to enter a git-root command) — centered floating prompt appears.
4. Trigger a `vim.ui.select` (e.g. `<leader>ca` for code actions) — the snacks select UI appears.
---
## Phase 3 — Engine swaps (blink.cmp + snacks.picker)
The largest phase. Two engine swaps in one commit because they share the snacks ecosystem and both touch the LSP attach path.
### 3a — `nvim-cmp` → `blink.cmp`
**Plugins removed from lockfile** when `nvim-cmp` is deleted (they were nvim-cmp dependencies):
`nvim-cmp`, `cmp-nvim-lsp`, `cmp-buffer`, `cmp-path`, `cmp-cmdline`, `cmp_luasnip`, `LuaSnip`, `lspkind.nvim`. `friendly-snippets` is kept and consumed directly by blink.
Files deleted: `lua/absolute/plugins/nvim-cmp.lua`, `lua/absolute/after/nvim-cmp.lua`.
**New file `lua/absolute/plugins/blink-cmp.lua`:**
```lua
return {
"saghen/blink.cmp",
version = "1.*", -- pin to v1 stable
event = "InsertEnter",
dependencies = { "rafamadriz/friendly-snippets" },
config = function()
require("absolute.after.blink-cmp")
end,
}
```
**New file `lua/absolute/after/blink-cmp.lua`:**
```lua
require("blink.cmp").setup({
keymap = { preset = "default" }, -- C-y accept, C-n/p navigate, C-Space trigger
appearance = { nerd_font_variant = "mono" },
sources = {
default = { "lsp", "path", "snippets", "buffer" },
},
cmdline = {
keymap = { preset = "cmdline" },
completion = { menu = { auto_show = true } },
},
signature = { enabled = true },
})
```
**Edit `lua/absolute/after/lsp-config.lua`:**
Replace the cmp-nvim-lsp capabilities provider:
```lua
-- old:
local cmp_nvim_lsp = require("cmp_nvim_lsp")
local capabilities = cmp_nvim_lsp.default_capabilities()
-- new:
local capabilities = require("blink.cmp").get_lsp_capabilities()
```
### 3b — `telescope.nvim` → `snacks.picker`
**Plugins removed from lockfile** when `telescope.nvim` is deleted:
`telescope.nvim`, `telescope-file-browser.nvim`, `telescope-live-grep-args.nvim`, `telescope-media-files.nvim`, `telescope-themes`, `telescope-dap.nvim`, `popup.nvim`.
`junegunn/fzf` **stays** — it is also a dependency of `nvim-bqf` (`lua/absolute/plugins/nvim-bqf.lua:6`), which is unrelated to this swap. The `lua/absolute/plugins/fzf.lua` spec is kept as-is.
Files deleted:
- `lua/absolute/plugins/telescope.lua`
- `lua/absolute/after/telescope.lua`
- `lua/absolute/utils/telescope_image_preview.lua` (replaced by snacks built-in image preview via image.nvim)
**Update `lua/absolute/plugins/snacks.lua`** — add the picker module:
```lua
opts = {
bigfile = { enabled = true },
quickfile = { enabled = true },
input = { enabled = true },
notifier = { enabled = true, ... }, -- from Phase 2
picker = {
enabled = true,
ui_select = true, -- override vim.ui.select with picker
},
}
```
**New file `lua/absolute/after/snacks.lua`** — replaces the old `after/telescope.lua` and houses the picker keymaps:
```lua
local map = vim.keymap.set
local opts = { noremap = true, silent = true }
map("n", "<C-p>", function() Snacks.picker.files() end, vim.tbl_extend("force", opts, { desc = "Find files" }))
map("n", "<leader>gf", function() Snacks.picker.git_files() end, vim.tbl_extend("force", opts, { desc = "Find git files" }))
map("n", "<leader>pr", function() Snacks.picker.recent() end, vim.tbl_extend("force", opts, { desc = "Recent files" }))
map("n", "<leader>ps", function() Snacks.picker.grep() end, vim.tbl_extend("force", opts, { desc = "Live grep" }))
map("n", "<leader>sb", function() Snacks.picker.grep_word() end, vim.tbl_extend("force", opts, { desc = "Grep word under cursor" }))
map("n", "<leader>pf", function() Snacks.picker.explorer() end, vim.tbl_extend("force", opts, { desc = "File explorer" }))
map("n", "<leader>cf", function() Snacks.picker.explorer({ cwd = vim.fn.expand("%:p:h") }) end, vim.tbl_extend("force", opts, { desc = "File explorer at file dir" }))
```
**Edits to existing files**, swapping `<cmd>Telescope ...<CR>` for `Snacks.picker.*` calls:
| File | Old binding | New binding |
|---|---|---|
| `remap.lua:7` | `<cmd>Telescope keymaps<CR>` | `function() Snacks.picker.keymaps() end` |
| `core/colorscheme.lua:150` | `<cmd>Telescope colorscheme<CR>` | `function() Snacks.picker.colorschemes() end` |
| `core/colorscheme.lua:153` | `<cmd>Telescope themes<CR>` | **delete** the `<leader>th` binding entirely (merged into `<leader>cs`) |
| `after/lsp-config.lua:20` | `<cmd>Telescope lsp_definitions<CR>` | `function() Snacks.picker.lsp_definitions() end` |
| `after/lsp-config.lua:26` | `<cmd>Telescope lsp_implementations<CR>` | `function() Snacks.picker.lsp_implementations() end` |
| `after/lsp-config.lua:38` | `<cmd>Telescope lsp_type_definitions<CR>` | `function() Snacks.picker.lsp_type_definitions() end` |
| `after/lsp-config.lua:47` | `<cmd>Telescope lsp_references<CR>` | `function() Snacks.picker.lsp_references() end` |
| `after/todo-comments.lua:18-20` | `vim.cmd([[TodoTelescope]])` | `Snacks.picker.todo_comments()` |
### Things lost in Phase 3
1. **DAP picker** (`telescope-dap`) — DAP itself works; the picker UI for breakpoints/variables is gone. `nvim-dap-ui` already covers most of this.
2. **`live-grep-args` argument syntax** — snacks accepts `--type lua` style args via a slightly different prompt prefix; muscle-memory shifts.
3. **Custom `telescope_image_preview.lua`** — dropped. snacks displays images natively via `image.nvim` integration in its file picker.
4. **`telescope-themes` registry helpers** — `<leader>th` is dropped. `<leader>cs` (colorschemes picker) replaces both. The `theme-registry.lua` system continues to work because it hooks `ColorScheme` autocmds, which fire regardless of how the colorscheme is selected.
### Phase 3 net change
- ~14 plugins removed from lockfile (8 from `nvim-cmp` chain, 7 from `telescope` chain — `friendly-snippets` stays)
- 5 files deleted (`plugins/nvim-cmp.lua`, `after/nvim-cmp.lua`, `plugins/telescope.lua`, `after/telescope.lua`, `utils/telescope_image_preview.lua`)
- 3 files added (`plugins/blink-cmp.lua`, `after/blink-cmp.lua`, `after/snacks.lua`)
- 5 files edited (`plugins/snacks.lua`, `after/lsp-config.lua`, `core/colorscheme.lua`, `remap.lua`, `after/todo-comments.lua`)
### Phase 3 validation
1. Restart Neovim. No errors on startup.
2. Open an insert-mode completion (`<C-Space>` in a code file) — blink menu appears with LSP, snippets, path, buffer sources.
3. Trigger snippet expansion (e.g. `for<Tab>` in Lua) — friendly-snippets fire via blink's snippet engine.
4. `<C-p>` opens snacks file picker and previews work.
5. `<leader>ps` opens live grep; `<leader>sb` greps the word under cursor.
6. LSP `gd` / `gi` / `gt` / `<leader>cr` open snacks LSP pickers.
7. `<leader>cs` opens colorscheme picker; selecting a theme triggers the wezterm/ghostty sync.
8. `<leader>?` opens keymaps picker.
9. `:checkhealth blink.cmp` is clean.
---
## Risk and rollback
Each phase is one commit. To roll a phase back, `git revert <sha>` and `:Lazy sync` to restore the prior plugin set. Because each phase ends in a working state, partial rollback (e.g. revert Phase 3 but keep Phases 1 and 2) is safe.
The highest-risk single change is the LSP-capabilities provider swap in `after/lsp-config.lua` (Phase 3a). If LSP completion misbehaves after Phase 3, isolate by temporarily reverting just that one line to the cmp-nvim-lsp provider and bisecting the blink.cmp config.
## Open items deferred
- Treesitter `master` → `main` rewrite (Neovim 0.12-only). Worth its own spec when ready.
- Replacing `nvim-tmux-navigation` configuration file naming (the spec file is `vim-tmux-navigator.lua` but the plugin is `alexghergh/nvim-tmux-navigation`). Cosmetic; not part of this work.
- Auditing the seven intentionally-disabled plugin specs. Out of scope.
## Acceptance criteria
After all three phases land:
1. `:checkhealth` reports no deprecated API usage related to this work.
2. `lazy-lock.json` no longer contains: `nvim-cmp`, `cmp-*`, `lspkind.nvim`, `LuaSnip`, `telescope.nvim`, `telescope-*`, `popup.nvim`, `nvim-notify`, `dressing.nvim`, `playground`, `vim-maximizer`, `poet-v`, `git-blame.nvim`, `render-markdown.nvim`, `codecompanion.nvim`, `copilot.vim`, `gen.nvim`, `llm.nvim`, `opencode.nvim`.
3. `lazy-lock.json` newly contains: `blink.cmp`, `snacks.nvim`.
4. Every keymap in the migration tables resolves to a working snacks.picker or blink.cmp action.
5. Diagnostic gutter signs render through `vim.diagnostic.config`.
6. `<leader>j` and `<leader>k` resize the current window with no collision.
7. Startup time as reported by `:Lazy profile` is no worse than before.