diff --git a/docs/superpowers/plans/2026-05-02-nvim-modernization.md b/docs/superpowers/plans/2026-05-02-nvim-modernization.md new file mode 100644 index 0000000..4f99e35 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-nvim-modernization.md @@ -0,0 +1,1291 @@ +# 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 `j`/`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 `?` keymap +- `lua/absolute/core/colorscheme.lua` — swap `cs`, drop `th` +- `lua/absolute/after/todo-comments.lua` — swap `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 (`k` lnext), 73 (`j` lprev), 120 (`k` resize), 124 (`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", "k", "lnextzz", opts) + +opts.desc = "Jump to previous location and center screen" +vim.keymap.set("n", "j", "lprevzz", opts) +``` + +with: + +```lua +opts.desc = "Next location-list entry" +vim.keymap.set("n", "]l", "lnextzz", opts) + +opts.desc = "Previous location-list entry" +vim.keymap.set("n", "[l", "lprevzz", 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 `k` and `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 -- ` 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 j/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) +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. `rn` on a symbol in a code file — centered floating prompt appears (proves `vim.ui.input` override). +5. `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) +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", "", function() + Snacks.picker.files() +end, with_desc("Find files")) + +map("n", "gf", function() + Snacks.picker.git_files() +end, with_desc("Find git files")) + +map("n", "pr", function() + Snacks.picker.recent() +end, with_desc("Recent files")) + +map("n", "ps", function() + Snacks.picker.grep() +end, with_desc("Live grep")) + +map("n", "sb", function() + Snacks.picker.grep_word() +end, with_desc("Grep word under cursor")) + +map("n", "pf", function() + Snacks.picker.explorer() +end, with_desc("File explorer (cwd)")) + +map("n", "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 `?` 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", "?", "Telescope keymaps", 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", "?", "Telescope keymaps", opts) -- toggle file explorer +``` + +with: + +```lua +opts.desc = "Show keymaps" +vim.keymap.set("n", "?", 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", "cs", "Telescope colorscheme", opts) + +opts.desc = "Fuzzy find themes" +vim.api.nvim_set_keymap("n", "th", "Telescope themes", 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", "cs", function() + Snacks.picker.colorschemes() +end, opts) +``` + +(Drops `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 `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", "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", "pt", function() + vim.cmd([[TodoTelescope]]) +end, opts) +``` + +with: + +```lua +opts.desc = "Open todo comments in picker" +vim.keymap.set("n", "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", "Telescope lsp_definitions", opts) +26: vim.keymap.set("n", "gi", "Telescope lsp_implementations", opts) +38: vim.keymap.set("n", "gt", "Telescope lsp_type_definitions", opts) +47: vim.keymap.set("n", "cr", "Telescope lsp_references", 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", "Telescope lsp_definitions", opts) +``` + +with: + +```lua + vim.keymap.set("n", "gd", function() Snacks.picker.lsp_definitions() end, opts) +``` + +Replace line 26: + +```lua + vim.keymap.set("n", "gi", "Telescope lsp_implementations", opts) +``` + +with: + +```lua + vim.keymap.set("n", "gi", function() Snacks.picker.lsp_implementations() end, opts) +``` + +Replace line 38: + +```lua + vim.keymap.set("n", "gt", "Telescope lsp_type_definitions", opts) +``` + +with: + +```lua + vim.keymap.set("n", "gt", function() Snacks.picker.lsp_type_definitions() end, opts) +``` + +Replace line 47: + +```lua + vim.keymap.set("n", "cr", "Telescope lsp_references", opts) +``` + +with: + +```lua + vim.keymap.set("n", "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: ``/`` navigate; `` accepts. +4. In Lua, type `for` — confirm the `friendly-snippets` `for` snippet expands. +5. Type `:Lazy ` 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 `cr` on a symbol — `Snacks.picker.lsp_references` opens. +11. Press `` — file picker; preview pane works. +12. Press `gf` — git files; preview works. +13. Press `pr` — recent files. +14. Press `ps` — live grep; type `function`; results stream in. +15. Press `sb` on a word — grep word under cursor; results show. +16. Press `pf` — file explorer at cwd. +17. Press `cf` — file explorer at current file's directory. +18. Press `?` — keymaps picker. +19. Press `cs` — colorscheme picker; pick a theme; confirm wezterm/ghostty sync still fires. +20. Press `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) +EOF +)" +``` + +- [ ] **Step 5: Verify commit and overall branch state** + +Run: `git log --oneline -4` + +Expected (most recent first): +``` + feat(nvim): phase 3 — replace nvim-cmp with blink.cmp and telescope with snacks.picker + feat(nvim): phase 2 — replace nvim-notify and dressing with snacks + 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.