diff --git a/docs/superpowers/specs/2026-05-02-nvim-modernization-design.md b/docs/superpowers/specs/2026-05-02-nvim-modernization-design.md new file mode 100644 index 0000000..4bd79d2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-02-nvim-modernization-design.md @@ -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 `j` / `k`.** +Lines 70–73 bind `k`/`j` to `lnext`/`lprev`. Lines 120–124 rebind the same keys to window resize, silently overriding them. The window-resize 4-way pad (`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 `_` and `\|` 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. `j` and `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. `rn` to LSP-rename, or `gr` to enter a git-root command) — centered floating prompt appears. +4. Trigger a `vim.ui.select` (e.g. `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", "", function() Snacks.picker.files() end, vim.tbl_extend("force", opts, { desc = "Find files" })) +map("n", "gf", function() Snacks.picker.git_files() end, vim.tbl_extend("force", opts, { desc = "Find git files" })) +map("n", "pr", function() Snacks.picker.recent() end, vim.tbl_extend("force", opts, { desc = "Recent files" })) +map("n", "ps", function() Snacks.picker.grep() end, vim.tbl_extend("force", opts, { desc = "Live grep" })) +map("n", "sb", function() Snacks.picker.grep_word() end, vim.tbl_extend("force", opts, { desc = "Grep word under cursor" })) +map("n", "pf", function() Snacks.picker.explorer() end, vim.tbl_extend("force", opts, { desc = "File explorer" })) +map("n", "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 `Telescope ...` for `Snacks.picker.*` calls: + +| File | Old binding | New binding | +|---|---|---| +| `remap.lua:7` | `Telescope keymaps` | `function() Snacks.picker.keymaps() end` | +| `core/colorscheme.lua:150` | `Telescope colorscheme` | `function() Snacks.picker.colorschemes() end` | +| `core/colorscheme.lua:153` | `Telescope themes` | **delete** the `th` binding entirely (merged into `cs`) | +| `after/lsp-config.lua:20` | `Telescope lsp_definitions` | `function() Snacks.picker.lsp_definitions() end` | +| `after/lsp-config.lua:26` | `Telescope lsp_implementations` | `function() Snacks.picker.lsp_implementations() end` | +| `after/lsp-config.lua:38` | `Telescope lsp_type_definitions` | `function() Snacks.picker.lsp_type_definitions() end` | +| `after/lsp-config.lua:47` | `Telescope lsp_references` | `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** — `th` is dropped. `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 (`` in a code file) — blink menu appears with LSP, snippets, path, buffer sources. +3. Trigger snippet expansion (e.g. `for` in Lua) — friendly-snippets fire via blink's snippet engine. +4. `` opens snacks file picker and previews work. +5. `ps` opens live grep; `sb` greps the word under cursor. +6. LSP `gd` / `gi` / `gt` / `cr` open snacks LSP pickers. +7. `cs` opens colorscheme picker; selecting a theme triggers the wezterm/ghostty sync. +8. `?` opens keymaps picker. +9. `:checkhealth blink.cmp` is clean. + +--- + +## Risk and rollback + +Each phase is one commit. To roll a phase back, `git revert ` 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. `j` and `k` resize the current window with no collision. +7. Startup time as reported by `:Lazy profile` is no worse than before.