Files
absolute-vim/docs/superpowers/specs/2026-05-02-nvim-modernization-design.md
David Ibia 5d9852f4ee 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.
2026-05-02 06:04:31 +01:00

17 KiB
Raw Permalink Blame History

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:

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.

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:

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:

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:

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:

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:

-- 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:

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:

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.