Files
absolute-vim/docs/superpowers/plans/2026-05-02-nvim-modernization.md
T
David Ibia b4bb915a3f docs(plan): add nvim modernization implementation plan
Three-phase, single-branch plan derived from the modernization
spec. Each phase is one commit with restart-and-validate gates.
Phase 1 fixes critical bugs and removes abandoned plugins;
phase 2 swaps notify/dressing for snacks UI; phase 3 swaps
nvim-cmp for blink.cmp and telescope for snacks.picker.
2026-05-02 06:04:43 +01:00

1292 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Neovim Modernization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Modernize a Neovim 0.12 config by fixing four silent breakages, removing abandoned plugins, and swapping nvim-cmp + telescope + nvim-notify + dressing for blink.cmp + snacks.nvim.
**Architecture:** Three sequential phases on a single branch. Each phase ends in one commit. Tasks within a phase stage changes only — they do *not* commit individually. Restart Neovim and run the validation checklist at the end of each phase before moving on.
**Tech Stack:** Neovim 0.12, Lua, lazy.nvim, blink.cmp v1.x, snacks.nvim, gitsigns.nvim.
**Spec:** `docs/superpowers/specs/2026-05-02-nvim-modernization-design.md`.
**Working directory for all commands:** `/Users/davidibia/dotfiles/.config/nvim`.
---
## File map
### Phase 1 — Triage and deletions
**Modify:**
- `lua/absolute/remap.lua` — drop dead `<leader>j`/`<leader>k` lnext/lprev binding; rebind to `[l`/`]l`
- `lua/absolute/after/lsp-config.lua` — replace `vim.fn.sign_define` block with `vim.diagnostic.config`
- `lua/absolute/lazy.lua` — fix `require("../current-theme")` → `require("current-theme")`
- `lua/absolute/after/gitsigns.lua` — enable current-line blame
**Create:**
- `lua/absolute/plugins/snacks.lua` — top-level snacks.nvim spec (foundation only)
**Delete:**
- `lua/absolute/after/nvim-tree.lua` (orphan)
- `lua/absolute/plugins/nvim-treesitter-playground.lua`
- `lua/absolute/plugins/vim-maximizer.lua`
- `lua/absolute/plugins/poet-v.lua`
- `lua/absolute/after/poet-v.lua`
- `lua/absolute/plugins/render-markdown.lua`
- `lua/absolute/plugins/git-blame.lua`
- `lua/absolute/plugins/codecompanion.lua`
- `lua/absolute/after/codecompanion.lua`
- `lua/absolute/plugins/copilot.lua`
- `lua/absolute/plugins/gen.lua`
- `lua/absolute/after/gen.lua`
- `lua/absolute/plugins/llm.lua`
- `lua/absolute/after/llm.lua`
- `lua/absolute/plugins/opencode.lua`
- `lua/absolute/after/opencode.lua`
### Phase 2 — UI swap
**Modify:**
- `lua/absolute/plugins/snacks.lua` — enable `input` and `notifier` modules
**Delete:**
- `lua/absolute/plugins/notify.lua`
- `lua/absolute/after/notify.lua`
- `lua/absolute/plugins/dressing.lua`
### Phase 3 — Engine swaps
**Modify:**
- `lua/absolute/plugins/snacks.lua` — enable `picker` module
- `lua/absolute/after/lsp-config.lua` — swap capabilities provider; swap LSP picker keymaps
- `lua/absolute/remap.lua` — swap `<leader>?` keymap
- `lua/absolute/core/colorscheme.lua` — swap `<leader>cs`, drop `<leader>th`
- `lua/absolute/after/todo-comments.lua` — swap `<leader>pt` from `:TodoTelescope` to `Snacks.picker.todo_comments`
**Create:**
- `lua/absolute/plugins/blink-cmp.lua`
- `lua/absolute/after/blink-cmp.lua`
- `lua/absolute/after/snacks.lua` (replaces `after/telescope.lua`)
**Delete:**
- `lua/absolute/plugins/nvim-cmp.lua`
- `lua/absolute/after/nvim-cmp.lua`
- `lua/absolute/plugins/telescope.lua`
- `lua/absolute/after/telescope.lua`
- `lua/absolute/utils/telescope_image_preview.lua`
---
## Phase 1 — Triage and deletions
### Task 1.1: Fix keymap collision (C1)
**Files:**
- Modify: `lua/absolute/remap.lua`
- [ ] **Step 1: Open the file and confirm current state**
Run: `grep -n "leader>k\|leader>j\|lnext\|lprev" lua/absolute/remap.lua`
Expected output includes lines 70 (`<leader>k` lnext), 73 (`<leader>j` lprev), 120 (`<leader>k` resize), 124 (`<leader>j` resize).
- [ ] **Step 2: Replace the dead lnext/lprev lines with idiomatic location-list bracket pair**
Edit `lua/absolute/remap.lua`. Replace lines 69–73:
```lua
opts.desc = "Jump to next location and center screen"
vim.keymap.set("n", "<leader>k", "<cmd>lnext<CR>zz", opts)
opts.desc = "Jump to previous location and center screen"
vim.keymap.set("n", "<leader>j", "<cmd>lprev<CR>zz", opts)
```
with:
```lua
opts.desc = "Next location-list entry"
vim.keymap.set("n", "]l", "<cmd>lnext<CR>zz", opts)
opts.desc = "Previous location-list entry"
vim.keymap.set("n", "[l", "<cmd>lprev<CR>zz", opts)
```
- [ ] **Step 3: Verify the file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/remap.lua" -c "qa" 2>&1 | head -5`
Expected: no output (file loads clean) or only a "did not load mapleader" type warning. Any `E5113`/`E:` line is a failure — re-edit.
---
### Task 1.2: Migrate diagnostic-signs API (C2)
**Files:**
- Modify: `lua/absolute/after/lsp-config.lua:73-83`
- [ ] **Step 1: Open the file and confirm current sign_define block**
Run: `sed -n '72,84p' lua/absolute/after/lsp-config.lua`
Expected: lines 73–83 contain the `signs` table and the `for type, icon in pairs(signs)` loop calling `vim.fn.sign_define`.
- [ ] **Step 2: Replace the block**
In `lua/absolute/after/lsp-config.lua`, replace lines 73–83:
```lua
-- Change the Diagnostic symbols in the sign column (gutter)
local signs = {
Error = " ",
Warn = " ",
Hint = "󰠠 ",
Info = " ",
}
for type, icon in pairs(signs) do
local hl = "DiagnosticSign" .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = "" })
end
```
with:
```lua
-- Configure diagnostic signs (Neovim 0.10+ API; required in 0.12)
vim.diagnostic.config({
signs = {
text = {
[vim.diagnostic.severity.ERROR] = " ",
[vim.diagnostic.severity.WARN] = " ",
[vim.diagnostic.severity.HINT] = "󰠠 ",
[vim.diagnostic.severity.INFO] = " ",
},
},
})
```
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/after/lsp-config.lua" -c "qa" 2>&1 | head -5`
Note: This will fail because the file requires `conform` and `cmp_nvim_lsp`, which aren't on `runtimepath` under `-u NONE`. That's expected. What you're checking for is *parse* errors (`E5113: Lua: ... unexpected symbol near ...`). If you see only `module not found` errors, the file parses.
---
### Task 1.3: Delete orphan after-file (C3)
**Files:**
- Delete: `lua/absolute/after/nvim-tree.lua`
- [ ] **Step 1: Confirm there is no plugin spec**
Run: `ls lua/absolute/plugins/ | grep -i nvim-tree`
Expected: empty output (no nvim-tree plugin spec exists).
- [ ] **Step 2: Confirm nothing else `require`s the orphan**
Run: `grep -rn "absolute.after.nvim-tree\|after.nvim-tree" lua/ init.lua 2>/dev/null`
Expected: empty output.
- [ ] **Step 3: Delete the file**
Run: `rm lua/absolute/after/nvim-tree.lua`
- [ ] **Step 4: Verify deletion**
Run: `ls lua/absolute/after/nvim-tree.lua 2>&1`
Expected: `ls: ...: No such file or directory`.
---
### Task 1.4: Fix require path in lazy.lua (C4)
**Files:**
- Modify: `lua/absolute/lazy.lua:36`
- [ ] **Step 1: Confirm current state**
Run: `sed -n '36p' lua/absolute/lazy.lua`
Expected: `require("../current-theme");`
- [ ] **Step 2: Edit the line**
In `lua/absolute/lazy.lua`, replace line 36:
```lua
require("../current-theme");
```
with:
```lua
require("current-theme")
```
(Drops the bogus `../` prefix and the unnecessary semicolon.)
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/lazy.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean (module-not-found errors are fine; syntax errors are not).
---
### Task 1.5: Add foundation snacks.nvim spec
**Files:**
- Create: `lua/absolute/plugins/snacks.lua`
- [ ] **Step 1: Create the file**
Create `lua/absolute/plugins/snacks.lua`:
```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
},
}
```
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/plugins/snacks.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 1.6: Enable gitsigns current-line blame
**Files:**
- Modify: `lua/absolute/after/gitsigns.lua`
- [ ] **Step 1: Confirm current contents**
Run: `cat lua/absolute/after/gitsigns.lua`
Expected:
```lua
local gitsigns = require("gitsigns")
gitsigns.setup()
```
- [ ] **Step 2: Replace the file contents**
Replace the entire contents of `lua/absolute/after/gitsigns.lua` with:
```lua
local gitsigns = require("gitsigns")
gitsigns.setup({
current_line_blame = true,
current_line_blame_opts = {
virt_text_pos = "eol",
delay = 500,
ignore_whitespace = false,
},
})
```
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/after/gitsigns.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean (a "module gitsigns not found" error is fine).
---
### Task 1.7: Delete abandoned plugin specs
**Files:**
- Delete: `lua/absolute/plugins/nvim-treesitter-playground.lua`
- Delete: `lua/absolute/plugins/vim-maximizer.lua`
- Delete: `lua/absolute/plugins/poet-v.lua`
- Delete: `lua/absolute/after/poet-v.lua`
- Delete: `lua/absolute/plugins/render-markdown.lua`
- Delete: `lua/absolute/plugins/git-blame.lua`
- [ ] **Step 1: Verify no references in remaining code**
Run:
```bash
grep -rn "playground\|vim-maximizer\|poet-v\|render-markdown\|git-blame\|gitblame" lua/ init.lua 2>/dev/null | grep -v "lua/absolute/plugins/nvim-treesitter-playground.lua\|lua/absolute/plugins/vim-maximizer.lua\|lua/absolute/plugins/poet-v.lua\|lua/absolute/after/poet-v.lua\|lua/absolute/plugins/render-markdown.lua\|lua/absolute/plugins/git-blame.lua"
```
Expected output may include:
- `lua/absolute/plugins/codecompanion.lua` mentioning `render-markdown` in its `ft = { "markdown", "codecompanion" }` (the `render-markdown.lua` spec) — that's fine, codecompanion is being deleted in Task 1.8.
- Comments mentioning old gitsigns/git-blame — fine.
If anything else lights up (e.g. an active `require("git-blame")`), **stop and address it** before deleting.
- [ ] **Step 2: Delete the six files**
Run:
```bash
rm lua/absolute/plugins/nvim-treesitter-playground.lua \
lua/absolute/plugins/vim-maximizer.lua \
lua/absolute/plugins/poet-v.lua \
lua/absolute/after/poet-v.lua \
lua/absolute/plugins/render-markdown.lua \
lua/absolute/plugins/git-blame.lua
```
- [ ] **Step 3: Verify all six are gone**
Run:
```bash
ls lua/absolute/plugins/nvim-treesitter-playground.lua \
lua/absolute/plugins/vim-maximizer.lua \
lua/absolute/plugins/poet-v.lua \
lua/absolute/after/poet-v.lua \
lua/absolute/plugins/render-markdown.lua \
lua/absolute/plugins/git-blame.lua 2>&1
```
Expected: each line reports `No such file or directory`.
---
### Task 1.8: Delete AI plugin specs
**Files:**
- Delete: `lua/absolute/plugins/codecompanion.lua`
- Delete: `lua/absolute/after/codecompanion.lua`
- Delete: `lua/absolute/plugins/copilot.lua`
- Delete: `lua/absolute/plugins/gen.lua`
- Delete: `lua/absolute/after/gen.lua`
- Delete: `lua/absolute/plugins/llm.lua`
- Delete: `lua/absolute/after/llm.lua`
- Delete: `lua/absolute/plugins/opencode.lua`
- Delete: `lua/absolute/after/opencode.lua`
- [ ] **Step 1: Verify no references elsewhere**
Run:
```bash
grep -rn "codecompanion\|copilot\|gen.nvim\|huggingface/llm\|opencode" lua/ init.lua 2>/dev/null \
| grep -v "lua/absolute/plugins/codecompanion.lua\|lua/absolute/after/codecompanion.lua\|lua/absolute/plugins/copilot.lua\|lua/absolute/plugins/gen.lua\|lua/absolute/after/gen.lua\|lua/absolute/plugins/llm.lua\|lua/absolute/after/llm.lua\|lua/absolute/plugins/opencode.lua\|lua/absolute/after/opencode.lua"
```
Expected: empty output (or only comments).
If you see live references in the toggleterm config (`_OLLAMA_TOGGLE` is fine — that's an Ollama terminal, not gen.nvim), in markview, or anywhere else, **stop and remove those references first.**
- [ ] **Step 2: Delete the nine files**
Run:
```bash
rm lua/absolute/plugins/codecompanion.lua \
lua/absolute/after/codecompanion.lua \
lua/absolute/plugins/copilot.lua \
lua/absolute/plugins/gen.lua \
lua/absolute/after/gen.lua \
lua/absolute/plugins/llm.lua \
lua/absolute/after/llm.lua \
lua/absolute/plugins/opencode.lua \
lua/absolute/after/opencode.lua
```
- [ ] **Step 3: Verify deletion**
Run:
```bash
ls lua/absolute/plugins/codecompanion.lua lua/absolute/after/codecompanion.lua \
lua/absolute/plugins/copilot.lua \
lua/absolute/plugins/gen.lua lua/absolute/after/gen.lua \
lua/absolute/plugins/llm.lua lua/absolute/after/llm.lua \
lua/absolute/plugins/opencode.lua lua/absolute/after/opencode.lua 2>&1
```
Expected: every line reports `No such file or directory`.
---
### Task 1.9: Validate and commit Phase 1
- [ ] **Step 1: Show pending changes**
Run: `git status`
Expected: modifications to `remap.lua`, `after/lsp-config.lua`, `lazy.lua`, `after/gitsigns.lua`, plus the previously-modified `core/colorscheme.lua`, `core/init.lua`, `lazy-lock.json`, `plugins/codecompanion.lua` (now deleted), `plugins/copilot.lua` (now deleted), `plugins/venv-selector.lua`, `lua/current-theme.lua`. New files: `plugins/snacks.lua`, `core/theme-registry.lua`, `plugins/flash.lua`. Many `D` (deleted) entries.
- [ ] **Step 2: Run Neovim once to bootstrap snacks and clean orphans**
Run: `nvim --headless "+Lazy! sync" "+Lazy! clean" "+qa" 2>&1 | tail -20`
Expected: lazy installs `snacks.nvim`, removes `playground`, `vim-maximizer`, `poet-v`, `git-blame.nvim`, `render-markdown.nvim`, `codecompanion.nvim`, `copilot.vim`, `gen.nvim`, `llm.nvim`, `opencode.nvim`. No errors.
- [ ] **Step 3: Open Neovim interactively and run validation checklist**
Run: `nvim`
In Neovim:
1. `:messages` — should be empty of `E:` lines.
2. `:Lazy` — should show `snacks.nvim` installed; should NOT list `nvim-notify`-replacement plugins yet (those come in Phase 2).
3. `:checkhealth lspconfig` — diagnostic signs section should not warn about `sign_define`.
4. Open a Lua file with a syntax error (e.g. `:e /tmp/bad.lua`, type `local x =`); confirm error gutter sign renders.
5. `:Inspect`, `:InspectTree`, `:EditQuery` — all three open without errors (proves treesitter playground replacement).
6. Open a git-tracked file, position cursor on a line with prior commit history; after ~500 ms confirm virtual blame text appears at end of line.
7. Press `<leader>k` and `<leader>j` in normal mode — current window should resize vertically (no `E:` errors).
8. Press `]l` and `[l` — should move through the location list (or report "location list empty"; either is fine).
9. `:q`
If anything fails, roll back individually-suspect files with `git checkout -- <file>` and address before continuing.
- [ ] **Step 4: Stage and commit Phase 1**
Run:
```bash
git add lua/absolute/remap.lua \
lua/absolute/after/lsp-config.lua \
lua/absolute/lazy.lua \
lua/absolute/after/gitsigns.lua \
lua/absolute/plugins/snacks.lua \
lua/absolute/core/theme-registry.lua \
lua/absolute/plugins/flash.lua \
lua/absolute/core/colorscheme.lua \
lua/absolute/core/init.lua \
lua/absolute/plugins/venv-selector.lua \
lua/current-theme.lua \
lazy-lock.json
git add -u # picks up the deletions
```
- [ ] **Step 5: Verify the staged set**
Run: `git status`
Expected: green entries match the file map for Phase 1; nothing in red.
- [ ] **Step 6: Commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
feat(nvim): phase 1 — triage and abandoned-plugin cleanup
Fix keymap collision on <leader>j/<leader>k (now resize-only;
location-list moved to ]l/[l). Migrate diagnostic signs from
deprecated vim.fn.sign_define to vim.diagnostic.config. Drop
orphan after/nvim-tree.lua and the bogus require("../current-theme").
Add foundation snacks.nvim spec. Remove playground, vim-maximizer,
poet-v, git-blame, render-markdown, and all five AI plugin specs.
Enable gitsigns current_line_blame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 7: Verify commit**
Run: `git log -1 --stat`
Expected: shows the Phase 1 commit with file list matching the file map above.
---
## Phase 2 — UI swap
### Task 2.1: Enable snacks input and notifier
**Files:**
- Modify: `lua/absolute/plugins/snacks.lua`
- [ ] **Step 1: Replace the file contents**
Replace the entire contents of `lua/absolute/plugins/snacks.lua` with:
```lua
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
---@type snacks.Config
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 = "",
},
},
-- picker added in Phase 3
},
}
```
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/plugins/snacks.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 2.2: Delete nvim-notify and dressing
**Files:**
- Delete: `lua/absolute/plugins/notify.lua`
- Delete: `lua/absolute/after/notify.lua`
- Delete: `lua/absolute/plugins/dressing.lua`
- [ ] **Step 1: Verify no live references**
Run:
```bash
grep -rn "rcarriga/nvim-notify\|require(\"notify\")\|stevearc/dressing\|require(\"dressing\")" lua/ init.lua 2>/dev/null \
| grep -v "lua/absolute/plugins/notify.lua\|lua/absolute/after/notify.lua\|lua/absolute/plugins/dressing.lua"
```
Expected: empty.
- [ ] **Step 2: Delete the three files**
Run:
```bash
rm lua/absolute/plugins/notify.lua \
lua/absolute/after/notify.lua \
lua/absolute/plugins/dressing.lua
```
- [ ] **Step 3: Verify deletion**
Run:
```bash
ls lua/absolute/plugins/notify.lua \
lua/absolute/after/notify.lua \
lua/absolute/plugins/dressing.lua 2>&1
```
Expected: each line reports `No such file or directory`.
---
### Task 2.3: Validate and commit Phase 2
- [ ] **Step 1: Sync lazy and prune**
Run: `nvim --headless "+Lazy! sync" "+Lazy! clean" "+qa" 2>&1 | tail -20`
Expected: lazy removes `nvim-notify` and `dressing.nvim`. No errors.
- [ ] **Step 2: Validate interactively**
Run: `nvim`
In Neovim:
1. `:messages` — clean.
2. `:lua vim.notify("Phase 2 OK", vim.log.levels.INFO)` — compact toast appears top-right.
3. `:lua vim.notify("Test warn", vim.log.levels.WARN)` — warning-styled toast.
4. `<leader>rn` on a symbol in a code file — centered floating prompt appears (proves `vim.ui.input` override).
5. `<leader>ca` on a code-action target — selection menu uses snacks picker UI (proves `vim.ui.select` override; picker not yet enabled, so this routes through snacks.input).
6. `:q`
- [ ] **Step 3: Stage and commit Phase 2**
Run:
```bash
git add lua/absolute/plugins/snacks.lua lazy-lock.json
git add -u
git commit -m "$(cat <<'EOF'
feat(nvim): phase 2 — replace nvim-notify and dressing with snacks
Enable snacks.input and snacks.notifier modules; remove the
now-redundant nvim-notify and archived dressing.nvim specs.
Notifier preserves the prior icons and 3000 ms timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 4: Verify commit**
Run: `git log -1 --stat`
Expected: shows the Phase 2 commit.
---
## Phase 3 — Engine swaps
### Task 3.1: Add blink.cmp plugin spec
**Files:**
- Create: `lua/absolute/plugins/blink-cmp.lua`
- [ ] **Step 1: Create the file**
Create `lua/absolute/plugins/blink-cmp.lua`:
```lua
return {
"saghen/blink.cmp",
version = "1.*",
event = "InsertEnter",
dependencies = { "rafamadriz/friendly-snippets" },
config = function()
require("absolute.after.blink-cmp")
end,
}
```
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/plugins/blink-cmp.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 3.2: Add blink.cmp configuration
**Files:**
- Create: `lua/absolute/after/blink-cmp.lua`
- [ ] **Step 1: Create the file**
Create `lua/absolute/after/blink-cmp.lua`:
```lua
require("blink.cmp").setup({
keymap = { preset = "default" },
appearance = {
nerd_font_variant = "mono",
},
sources = {
default = { "lsp", "path", "snippets", "buffer" },
},
cmdline = {
keymap = { preset = "cmdline" },
completion = { menu = { auto_show = true } },
},
signature = { enabled = true },
})
```
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/after/blink-cmp.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean (`module blink.cmp not found` is fine; syntax errors are not).
---
### Task 3.3: Migrate LSP capabilities provider to blink
**Files:**
- Modify: `lua/absolute/after/lsp-config.lua:3,70`
- [ ] **Step 1: Confirm current state**
Run: `sed -n '1,5p;68,72p' lua/absolute/after/lsp-config.lua`
Expected: line 3 is `local cmp_nvim_lsp = require("cmp_nvim_lsp")`; line 70 is `local capabilities = cmp_nvim_lsp.default_capabilities()`.
- [ ] **Step 2: Edit line 3**
In `lua/absolute/after/lsp-config.lua`, replace line 3:
```lua
local cmp_nvim_lsp = require("cmp_nvim_lsp")
```
with: *(delete the line entirely)*. Make sure no blank line is left between `local conform = require("conform")` and the next non-blank line.
- [ ] **Step 3: Edit the capabilities line**
In the same file, replace the line:
```lua
local capabilities = cmp_nvim_lsp.default_capabilities()
```
with:
```lua
local capabilities = require("blink.cmp").get_lsp_capabilities()
```
- [ ] **Step 4: Verify the edit**
Run: `grep -n "capabilities\|cmp_nvim_lsp\|blink" lua/absolute/after/lsp-config.lua | head -10`
Expected: no `cmp_nvim_lsp` references; one `blink.cmp` reference; many `capabilities = capabilities` reuses.
---
### Task 3.4: Delete nvim-cmp specs
**Files:**
- Delete: `lua/absolute/plugins/nvim-cmp.lua`
- Delete: `lua/absolute/after/nvim-cmp.lua`
- [ ] **Step 1: Verify no live references outside the doomed files**
Run:
```bash
grep -rn "hrsh7th/nvim-cmp\|require(\"cmp\")\|require(\"cmp_nvim_lsp\")\|require(\"luasnip\")\|require(\"lspkind\")" lua/ init.lua 2>/dev/null \
| grep -v "lua/absolute/plugins/nvim-cmp.lua\|lua/absolute/after/nvim-cmp.lua"
```
Expected: empty (the LSP capabilities reference was removed in Task 3.3).
If anything else lights up — e.g. another plugin that called `cmp.register_source` — pause and migrate that reference before continuing.
- [ ] **Step 2: Delete the two files**
Run:
```bash
rm lua/absolute/plugins/nvim-cmp.lua \
lua/absolute/after/nvim-cmp.lua
```
- [ ] **Step 3: Verify deletion**
Run: `ls lua/absolute/plugins/nvim-cmp.lua lua/absolute/after/nvim-cmp.lua 2>&1`
Expected: each line reports `No such file or directory`.
---
### Task 3.5: Enable snacks picker module
**Files:**
- Modify: `lua/absolute/plugins/snacks.lua`
- [ ] **Step 1: Replace the file contents**
Replace the entire contents of `lua/absolute/plugins/snacks.lua` with:
```lua
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
---@type snacks.Config
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 = "",
},
},
picker = {
enabled = true,
ui_select = true,
},
},
}
```
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/plugins/snacks.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 3.6: Add new after/snacks.lua with picker keymaps
**Files:**
- Create: `lua/absolute/after/snacks.lua`
- [ ] **Step 1: Create the file**
Create `lua/absolute/after/snacks.lua`:
```lua
local map = vim.keymap.set
local function with_desc(desc)
return { noremap = true, silent = true, desc = desc }
end
map("n", "<C-p>", function()
Snacks.picker.files()
end, with_desc("Find files"))
map("n", "<leader>gf", function()
Snacks.picker.git_files()
end, with_desc("Find git files"))
map("n", "<leader>pr", function()
Snacks.picker.recent()
end, with_desc("Recent files"))
map("n", "<leader>ps", function()
Snacks.picker.grep()
end, with_desc("Live grep"))
map("n", "<leader>sb", function()
Snacks.picker.grep_word()
end, with_desc("Grep word under cursor"))
map("n", "<leader>pf", function()
Snacks.picker.explorer()
end, with_desc("File explorer (cwd)"))
map("n", "<leader>cf", function()
Snacks.picker.explorer({ cwd = vim.fn.expand("%:p:h") })
end, with_desc("File explorer (current file dir)"))
```
Note: snacks auto-loads its `after/plugin/snacks.vim` shim that creates the `Snacks` global, so the `function()` wrappers (rather than direct references) are required to defer global lookup until the keymap fires.
- [ ] **Step 2: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/after/snacks.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean. (The `Snacks` global won't exist under `-u NONE`, but that's a runtime concern, not a parse error.)
- [ ] **Step 3: Wire the after-file into the plugin spec**
Edit `lua/absolute/plugins/snacks.lua` to call the new after-file. Replace the entire contents with:
```lua
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
---@type snacks.Config
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 = "",
},
},
picker = {
enabled = true,
ui_select = true,
},
},
config = function(_, opts)
require("snacks").setup(opts)
require("absolute.after.snacks")
end,
}
```
- [ ] **Step 4: Verify the spec parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/plugins/snacks.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 3.7: Migrate `<leader>?` keymap in remap.lua
**Files:**
- Modify: `lua/absolute/remap.lua:7`
- [ ] **Step 1: Confirm current state**
Run: `sed -n '6,8p' lua/absolute/remap.lua`
Expected:
```lua
opts.desc = "Show Keymap"
vim.keymap.set("n", "<leader>?", "<cmd>Telescope keymaps<CR>", opts) -- toggle file explorer
```
- [ ] **Step 2: Edit the binding**
Replace those two lines in `lua/absolute/remap.lua`:
```lua
opts.desc = "Show Keymap"
vim.keymap.set("n", "<leader>?", "<cmd>Telescope keymaps<CR>", opts) -- toggle file explorer
```
with:
```lua
opts.desc = "Show keymaps"
vim.keymap.set("n", "<leader>?", function()
Snacks.picker.keymaps()
end, opts)
```
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/remap.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 3.8: Migrate keymaps in colorscheme.lua
**Files:**
- Modify: `lua/absolute/core/colorscheme.lua:147-153`
- [ ] **Step 1: Confirm current state**
Run: `sed -n '147,154p' lua/absolute/core/colorscheme.lua`
Expected:
```lua
local opts = { noremap = true, silent = true }
opts.desc = "Fuzzy find colorschemes"
vim.api.nvim_set_keymap("n", "<leader>cs", "<cmd>Telescope colorscheme<CR>", opts)
opts.desc = "Fuzzy find themes"
vim.api.nvim_set_keymap("n", "<leader>th", "<cmd>Telescope themes<CR>", opts)
```
- [ ] **Step 2: Replace the block**
Replace those eight lines (inclusive of the trailing blank if present) with:
```lua
local opts = { noremap = true, silent = true }
opts.desc = "Fuzzy find colorschemes"
vim.keymap.set("n", "<leader>cs", function()
Snacks.picker.colorschemes()
end, opts)
```
(Drops `<leader>th` per the spec — the registry-aware behavior fires on `ColorScheme` autocmds regardless of how the colorscheme is selected.)
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/core/colorscheme.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean (a "module absolute.core.theme-registry not found" error is fine).
---
### Task 3.9: Migrate `<leader>pt` in todo-comments.lua
**Files:**
- Modify: `lua/absolute/after/todo-comments.lua:17-20`
- [ ] **Step 1: Confirm current state**
Run: `sed -n '17,21p' lua/absolute/after/todo-comments.lua`
Expected:
```lua
opts.desc = "Open Comments in Telescope"
vim.keymap.set("n", "<leader>pt", function()
vim.cmd([[TodoTelescope]])
end, opts)
```
- [ ] **Step 2: Replace the block**
Replace those four lines in `lua/absolute/after/todo-comments.lua`:
```lua
opts.desc = "Open Comments in Telescope"
vim.keymap.set("n", "<leader>pt", function()
vim.cmd([[TodoTelescope]])
end, opts)
```
with:
```lua
opts.desc = "Open todo comments in picker"
vim.keymap.set("n", "<leader>pt", function()
Snacks.picker.todo_comments()
end, opts)
```
- [ ] **Step 3: Verify file parses**
Run: `nvim --headless -u NONE -c "luafile lua/absolute/after/todo-comments.lua" -c "qa" 2>&1 | head -5`
Expected: parse-clean.
---
### Task 3.10: Migrate LSP picker keymaps in lsp-config.lua
**Files:**
- Modify: `lua/absolute/after/lsp-config.lua:20,26,38,47`
- [ ] **Step 1: Confirm current state**
Run: `grep -n "Telescope" lua/absolute/after/lsp-config.lua`
Expected:
```
20: vim.keymap.set("n", "gd", "<cmd>Telescope lsp_definitions<CR>", opts)
26: vim.keymap.set("n", "gi", "<cmd>Telescope lsp_implementations<CR>", opts)
38: vim.keymap.set("n", "gt", "<cmd>Telescope lsp_type_definitions<CR>", opts)
47: vim.keymap.set("n", "<leader>cr", "<cmd>Telescope lsp_references<CR>", opts)
```
- [ ] **Step 2: Replace each binding**
Make four individual edits in `lua/absolute/after/lsp-config.lua`:
Replace line 20:
```lua
vim.keymap.set("n", "gd", "<cmd>Telescope lsp_definitions<CR>", opts)
```
with:
```lua
vim.keymap.set("n", "gd", function() Snacks.picker.lsp_definitions() end, opts)
```
Replace line 26:
```lua
vim.keymap.set("n", "gi", "<cmd>Telescope lsp_implementations<CR>", opts)
```
with:
```lua
vim.keymap.set("n", "gi", function() Snacks.picker.lsp_implementations() end, opts)
```
Replace line 38:
```lua
vim.keymap.set("n", "gt", "<cmd>Telescope lsp_type_definitions<CR>", opts)
```
with:
```lua
vim.keymap.set("n", "gt", function() Snacks.picker.lsp_type_definitions() end, opts)
```
Replace line 47:
```lua
vim.keymap.set("n", "<leader>cr", "<cmd>Telescope lsp_references<CR>", opts)
```
with:
```lua
vim.keymap.set("n", "<leader>cr", function() Snacks.picker.lsp_references() end, opts)
```
- [ ] **Step 3: Verify**
Run: `grep -n "Telescope\|Snacks.picker" lua/absolute/after/lsp-config.lua`
Expected: zero `Telescope` matches; four `Snacks.picker.lsp_*` matches.
---
### Task 3.11: Delete telescope ecosystem
**Files:**
- Delete: `lua/absolute/plugins/telescope.lua`
- Delete: `lua/absolute/after/telescope.lua`
- Delete: `lua/absolute/utils/telescope_image_preview.lua`
- [ ] **Step 1: Verify no live references remain**
Run:
```bash
grep -rn "Telescope\|require(\"telescope\|telescope.builtin\|telescope_image_preview\|nvim-telescope" lua/ init.lua 2>/dev/null \
| grep -v "lua/absolute/plugins/telescope.lua\|lua/absolute/after/telescope.lua\|lua/absolute/utils/telescope_image_preview.lua"
```
Expected: empty.
If anything else lights up — e.g. a custom command, a comment that's actually executable — pause and migrate before deleting.
- [ ] **Step 2: Delete the three files**
Run:
```bash
rm lua/absolute/plugins/telescope.lua \
lua/absolute/after/telescope.lua \
lua/absolute/utils/telescope_image_preview.lua
```
- [ ] **Step 3: Verify deletion**
Run:
```bash
ls lua/absolute/plugins/telescope.lua \
lua/absolute/after/telescope.lua \
lua/absolute/utils/telescope_image_preview.lua 2>&1
```
Expected: each line reports `No such file or directory`.
---
### Task 3.12: Validate and commit Phase 3
- [ ] **Step 1: Sync lazy and prune**
Run: `nvim --headless "+Lazy! sync" "+Lazy! clean" "+qa" 2>&1 | tail -30`
Expected:
- lazy installs `blink.cmp` (and downloads its Rust matcher binary; first run takes longer).
- lazy removes `telescope.nvim`, `telescope-file-browser.nvim`, `telescope-live-grep-args.nvim`, `telescope-media-files.nvim`, `telescope-themes`, `telescope-dap.nvim`, `popup.nvim`, `nvim-cmp`, `cmp-nvim-lsp`, `cmp-buffer`, `cmp-path`, `cmp-cmdline`, `cmp_luasnip`, `LuaSnip`, `lspkind.nvim`.
- No errors.
- [ ] **Step 2: Run `:checkhealth blink.cmp`**
Run: `nvim --headless "+checkhealth blink.cmp" "+qa" 2>&1 | tail -40`
Expected: green checks for `vim.snippet`, fuzzy matcher binary, and at least one source.
- [ ] **Step 3: Validate interactively**
Run: `nvim`
In Neovim:
1. `:messages` — clean.
2. Open a Lua file. Type `vim.l` and confirm a blink completion menu appears with LSP entries.
3. With the menu open: `<C-n>`/`<C-p>` navigate; `<C-y>` accepts.
4. In Lua, type `for<Tab>` — confirm the `friendly-snippets` `for` snippet expands.
5. Type `:Lazy <Tab>` in cmdline — confirm cmdline completion shows.
6. Press `K` on a symbol — LSP hover floats appear.
7. Press `gd` on a symbol — `Snacks.picker.lsp_definitions` opens.
8. Press `gi` on a symbol — `Snacks.picker.lsp_implementations` opens.
9. Press `gt` on a symbol — `Snacks.picker.lsp_type_definitions` opens.
10. Press `<leader>cr` on a symbol — `Snacks.picker.lsp_references` opens.
11. Press `<C-p>` — file picker; preview pane works.
12. Press `<leader>gf` — git files; preview works.
13. Press `<leader>pr` — recent files.
14. Press `<leader>ps` — live grep; type `function`; results stream in.
15. Press `<leader>sb` on a word — grep word under cursor; results show.
16. Press `<leader>pf` — file explorer at cwd.
17. Press `<leader>cf` — file explorer at current file's directory.
18. Press `<leader>?` — keymaps picker.
19. Press `<leader>cs` — colorscheme picker; pick a theme; confirm wezterm/ghostty sync still fires.
20. Press `<leader>pt` — todo-comments picker.
21. `:q`
If any binding fails, run `:lua print(vim.inspect(vim.api.nvim_get_keymap("n")))` to inspect the actual binding state and re-edit the source file.
- [ ] **Step 4: Stage and commit Phase 3**
Run:
```bash
git add lua/absolute/plugins/blink-cmp.lua \
lua/absolute/after/blink-cmp.lua \
lua/absolute/after/snacks.lua \
lua/absolute/plugins/snacks.lua \
lua/absolute/after/lsp-config.lua \
lua/absolute/remap.lua \
lua/absolute/core/colorscheme.lua \
lua/absolute/after/todo-comments.lua \
lazy-lock.json
git add -u
git commit -m "$(cat <<'EOF'
feat(nvim): phase 3 — replace nvim-cmp with blink.cmp and telescope with snacks.picker
Swap the completion engine to blink.cmp v1.x, dropping nvim-cmp,
its eight cmp-* sources, lspkind, and LuaSnip. Swap the picker
to snacks.picker, removing telescope.nvim and six telescope
extensions. LSP capabilities now sourced from blink.cmp; LSP
navigation keymaps and the colorscheme/keymaps/todo pickers move
to Snacks.picker. friendly-snippets is preserved as blink's
snippet source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 5: Verify commit and overall branch state**
Run: `git log --oneline -4`
Expected (most recent first):
```
<sha> feat(nvim): phase 3 — replace nvim-cmp with blink.cmp and telescope with snacks.picker
<sha> feat(nvim): phase 2 — replace nvim-notify and dressing with snacks
<sha> feat(nvim): phase 1 — triage and abandoned-plugin cleanup
fd3878b docs(spec): add nvim modernization design
```
Run: `git diff --stat HEAD~3 HEAD | tail -3`
Expected: large net negative line count (~hundreds of lines deleted, fewer added).
---
## Done. Acceptance verification
- [ ] **Final acceptance check**
Run:
```bash
grep -E '"(nvim-cmp|cmp-buffer|cmp-cmdline|cmp-path|cmp_luasnip|cmp-nvim-lsp|lspkind\.nvim|LuaSnip|telescope\.nvim|telescope-file-browser\.nvim|telescope-live-grep-args\.nvim|telescope-media-files\.nvim|telescope-themes|telescope-dap\.nvim|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)"' lazy-lock.json
```
Expected: empty output (every removed plugin is gone from the lockfile).
Run:
```bash
grep -E '"(blink\.cmp|snacks\.nvim)"' lazy-lock.json
```
Expected: both lines present.
Run: `grep -rn "vim.fn.sign_define\|require(\"cmp_nvim_lsp\")\|require(\"telescope\)" lua/ init.lua 2>/dev/null`
Expected: empty.
If all three checks pass, the implementation matches the spec.