From 89e4b756779c18be03835e924cf1d53092bbb122 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 20:06:34 +0700 Subject: [PATCH 01/26] ci: add headless config smoke test --- .github/workflows/smoke.yml | 26 +++++++++++++++++++++++++ scripts/smoke.sh | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .github/workflows/smoke.yml create mode 100755 scripts/smoke.sh diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..04f6c67 --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,26 @@ +name: smoke + +on: + push: + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: stable + + - name: Install plugins (lazy.nvim) + run: | + ISOLATED="$(mktemp -d)" + ln -s "$PWD" "$ISOLATED/nvim" + XDG_CONFIG_HOME="$ISOLATED" nvim --headless "+Lazy! sync" +qa + + - name: Smoke test + run: scripts/smoke.sh diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..1ca144a --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Headless load smoke test for BASEDDvim. +# +# Loads the full config in a clean, isolated Neovim instance and fails if +# Neovim emits any error patterns while doing so. +# +# Why inspect output (not exit code): `nvim --headless +qa` returns 0 even +# when the config errors at load time, so the exit code is not a reliable +# signal. We grep for Neovim's own error markers instead. +# +# Limitations: this is a LOAD test. It will not catch bugs that only trigger +# on a keypress, on opening a specific filetype, or inside an LSP callback. +# See plans/002..004 for those. +set -uo pipefail + +# Isolate the config so the host machine's personal ~/.config/nvim does not +# leak in and create false failures. Point XDG_CONFIG_HOME at this repo's +# parent layout: /nvim -> . +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ISOLATED="$(mktemp -d)" +trap 'rm -rf "$ISOLATED"' EXIT +ln -s "$REPO_ROOT" "$ISOLATED/nvim" + +LOG="$(mktemp)" + +XDG_CONFIG_HOME="$ISOLATED" nvim --headless "+qa" >"$LOG" 2>&1 + +# Neovim error markers. Tuned to Neovim's exact output formats observed in +# recon: "Error in :", "E5xxx:" codes, "stack traceback:", and the +# common nil-access messages. +if grep -nE 'Error in |E5[0-9]{3}:|stack traceback:|attempt to (call|index) a nil value' "$LOG"; then + echo "FAIL: errors detected while loading BASEDDvim config" >&2 + echo "--- captured output ---" >&2 + cat "$LOG" >&2 + exit 1 +fi + +echo "OK: config loaded cleanly" From 5d6c9473d8dc65a6532e155b6842fb19c20c2705 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 20:31:14 +0700 Subject: [PATCH 02/26] fix: correct flash treesitter_search keymap typo --- lua/plugins/flash.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/plugins/flash.lua b/lua/plugins/flash.lua index da4a2a4..5a603ea 100644 --- a/lua/plugins/flash.lua +++ b/lua/plugins/flash.lua @@ -7,7 +7,7 @@ return { { 's', mode = {'n', 'x', 'o'}, function() require('flash').jump() end, desc = 'Flash' }, { 'S', mode = {'n', 'x', 'o'}, function() require('flash').treesitter() end, desc = 'Flash Treesitter' }, { 'r', mode = {'o'}, function() require('flash').remote() end, desc = 'Remote Flash' }, - { 'R', mode = {'o', 'x'}, function() require('flash').tresitter_search() end, desc = 'Treesitter Search' }, + { 'R', mode = {'o', 'x'}, function() require('flash').treesitter_search() end, desc = 'Treesitter Search' }, { '', mode = {'c'}, function() require('flash').toggle() end, desc = 'Toggle Flash Search' }, }, } From b021096b96c2863674d48dcf2fb1ed6585ae4d8f Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 20:32:27 +0700 Subject: [PATCH 03/26] fix: use correct nvim-lint 'biome' linter name and honor configured events --- lua/plugins/web-linter.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/plugins/web-linter.lua b/lua/plugins/web-linter.lua index bf702ea..926efee 100644 --- a/lua/plugins/web-linter.lua +++ b/lua/plugins/web-linter.lua @@ -11,10 +11,10 @@ return { -- Events to trigger linter events = { "BufWritePost", "BufReadPost", "InsertLeave" }, linters_by_ft = { - javascript = { 'biomejs' }, - javascriptreact = { 'biomejs' }, - typescript = { 'biomejs' }, - typescriptreact = { 'biomejs' }, + javascript = { 'biome' }, + javascriptreact = { 'biome' }, + typescript = { 'biome' }, + typescriptreact = { 'biome' }, }, }, config = function(_, opts) @@ -29,7 +29,7 @@ return { lint.linters_by_ft = opts.linters_by_ft vim.api.nvim_create_autocmd( - { 'BufWritePost', 'BufEnter' }, + opts.events, { callback = function() lint.try_lint() From 66923ac06db685bbe03bda76275f9f6e12b4b0df Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 20:37:52 +0700 Subject: [PATCH 04/26] fix: scope jdtls LSP keymaps to args.buf in LspAttach --- lua/plugins/java.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/plugins/java.lua b/lua/plugins/java.lua index 98fa5f6..ebc510f 100644 --- a/lua/plugins/java.lua +++ b/lua/plugins/java.lua @@ -136,8 +136,8 @@ return { local client = vim.lsp.get_client_by_id(args.data.client_id) if client and client.name == "jdtls" then local keymaps = require("keymaps") - keymaps.lsp({ buffer = bufnr }) - keymaps.lsp_format({ buffer = bufnr }) + keymaps.lsp({ buffer = args.buf }) + keymaps.lsp_format({ buffer = args.buf }) -- User can set additional keymaps in opts.on_attach if opts.on_attach then From f5c7d0009d9a9193e4036633047e3492f90d4b1b Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 20:38:31 +0700 Subject: [PATCH 05/26] docs: fix stale colorscheme and sidekick fork in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 268f561..3a5e84e 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ git clone https://github.com/baseddxyz/BASEDDvim.git ~/.config/nvim ### AI & Code Completion - **[supermaven-nvim](https://github.com/supermaven-inc/supermaven-nvim)** - AI-powered inline code completion -- **[sidekick.nvim](https://github.com/qapquiz/sidekick.nvim)** - CLI integration for AI tools (Claude, etc.) +- **[sidekick.nvim](https://github.com/folke/sidekick.nvim)** - CLI integration for AI tools (Claude, etc.) - **[amp.nvim](https://github.com/sourcegraph/amp.nvim)** - Sourcegraph's AI code assistant - **[99](https://github.com/ThePrimeagen/99)** - AI-powered code refactoring with SKILL.md support ### Editor UI & Theme - **[bufferline.nvim](https://github.com/akinsho/bufferline.nvim)** - Buffer tabline -- **[monokai-pro.nvim](https://github.com/gthelding/monokai-pro.nvim)** - Monokai Pro colorscheme (machine filter) +- **[gruvbox.nvim](https://github.com/ellisonleao/gruvbox.nvim)** - Gruvbox colorscheme - **[smear-cursor.nvim](https://github.com/sphamba/smear-cursor.nvim)** - Smooth cursor animation - **[trouble.nvim](https://github.com/folke/trouble.nvim)** - Pretty diagnostics, references, and more From bd197327b8fc9caabc753b34a388646559b6ca26 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 21:12:41 +0700 Subject: [PATCH 06/26] fix: apply rustaceanvim ca after generic LSP keymaps --- lua/plugins/lspconfig.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index 2686de1..49b6aba 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -174,6 +174,9 @@ return { opts = { server = { on_attach = function(_, bufnr) + -- lsp keymap (generic first, so the Rust-specific ca below wins) + keymaps.lsp({ buffer = bufnr }) + vim.keymap.set("n", "ca", function() vim.cmd.RustLsp("codeAction") end, { desc = "Code Action", buffer = bufnr }) @@ -185,9 +188,6 @@ return { -- end, -- { desc = "Rust debuggables", buffer = bufnr } -- ) - - -- lsp keymap - keymaps.lsp({ buffer = bufnr }) end, default_settings = { -- rust-analyzer language server configuration From a1c7abfe682f901cad42705736b8739581b9a2c9 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 21:13:11 +0700 Subject: [PATCH 07/26] fix: move LSP signature help to insert mode to free for tmux --- README.md | 2 +- lua/keymaps.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a5e84e..2b74841 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ git clone https://github.com/baseddxyz/BASEDDvim.git ~/.config/nvim | `gi` | n | Go to implementation | | `gr` | n | Find references | | `K` | n | Hover documentation | -| `` | n | Signature help | +| `` | i | Signature help | | `D` | n | Go to type definition | | `ca` | n, v | Code action | | `rn` | n | Rename | diff --git a/lua/keymaps.lua b/lua/keymaps.lua index 5b6ac23..c8d8ae0 100644 --- a/lua/keymaps.lua +++ b/lua/keymaps.lua @@ -9,7 +9,7 @@ function M.lsp(opts) { "n", "gd", vim.lsp.buf.definition, opts }, { "n", "K", vim.lsp.buf.hover, opts }, { "n", "gi", vim.lsp.buf.implementation, opts }, - { "n", "", vim.lsp.buf.signature_help, opts }, + { "i", "", vim.lsp.buf.signature_help, opts }, { "n", "D", vim.lsp.buf.type_definition, opts }, { "n", "rn", vim.lsp.buf.rename, opts }, { { "n", "v" }, "ca", vim.lsp.buf.code_action, opts }, From e26247104b2c6c68ad4359dc571bf2543f842996 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 21:13:51 +0700 Subject: [PATCH 08/26] fix: include AGENTS.md in 99 md_files for repo context --- lua/plugins/ai.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/plugins/ai.lua b/lua/plugins/ai.lua index ff0ea9a..21151db 100644 --- a/lua/plugins/ai.lua +++ b/lua/plugins/ai.lua @@ -368,6 +368,7 @@ return configs.ai --- /foo/AGENT.md --- assuming that /foo is project root (based on cwd) md_files = { + "AGENTS.md", "AGENT.md", }, }) From d9a6fcc096eb1ee98fb3da9698bc6b170100629f Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 21:15:07 +0700 Subject: [PATCH 09/26] style: add desc to 99 and toggleterm keymaps --- lua/plugins/ai.lua | 8 ++++---- lua/plugins/terminal.lua | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/plugins/ai.lua b/lua/plugins/ai.lua index 21151db..698f72e 100644 --- a/lua/plugins/ai.lua +++ b/lua/plugins/ai.lua @@ -376,7 +376,7 @@ return configs.ai -- Create your own short cuts for the different types of actions vim.keymap.set("n", "9f", function() _99.fill_in_function() - end) + end, { desc = "99: Fill in function" }) -- take extra note that i have visual selection only in v mode -- technically whatever your last visual selection is, will be used -- so i have this set to visual mode so i dont screw up and use an @@ -386,12 +386,12 @@ return configs.ai -- so just prepare for it now vim.keymap.set("v", "9v", function() _99.visual() - end) + end, { desc = "99: Visual selection" }) --- if you have a request you dont want to make any changes, just cancel it vim.keymap.set("v", "9s", function() _99.stop_all_requests() - end) + end, { desc = "99: Stop all requests" }) --- Example: Using rules + actions for custom behaviors --- Create a rule file like ~/.rules/debug.md that defines custom behavior. @@ -399,7 +399,7 @@ return configs.ai --- throughout a function to help debug its execution flow. vim.keymap.set("n", "9fd", function() _99.fill_in_function() - end) + end, { desc = "99: Fill in function (debug rule)" }) end, }, } diff --git a/lua/plugins/terminal.lua b/lua/plugins/terminal.lua index 05beb98..2709905 100644 --- a/lua/plugins/terminal.lua +++ b/lua/plugins/terminal.lua @@ -4,7 +4,7 @@ return { version = '*', config = function() require('toggleterm').setup{} - vim.keymap.set('n', 'tf', 'ToggleTerm direction=float') + vim.keymap.set('n', 'tf', 'ToggleTerm direction=float', { desc = 'Toggle terminal (float)' }) end, } } From be48dccd890c4b3f68d92e48e6393eb34e083bf9 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 22:06:54 +0700 Subject: [PATCH 10/26] docs: add improvement plans (001-010) and index Self-contained implementation plans generated by the improve skill: - 001-009: verification baseline, the runtime bug fixes shipped on this branch, and second-pass correctness/consistency fixes - 010: a TODO feature plan for :LspToggle with persisted config plans/README.md indexes execution order, dependencies, status, and the deferred findings considered but not yet planned. --- plans/001-headless-smoke-and-ci.md | 251 +++++++++++ plans/002-flash-tresitter-typo.md | 156 +++++++ plans/003-web-linter-biomejs.md | 253 +++++++++++ plans/004-java-bufnr-args-buf.md | 210 +++++++++ plans/005-readme-stale-theme-sidekick.md | 189 ++++++++ plans/006-rust-leader-ca-clobber.md | 227 ++++++++++ plans/007-ck-collision-tmux-sighelp.md | 217 ++++++++++ plans/008-99-md-files-agents.md | 188 ++++++++ plans/009-keymap-desc-99-toggleterm.md | 245 +++++++++++ plans/010-lsp-toggle-ui.md | 525 +++++++++++++++++++++++ plans/README.md | 102 +++++ 11 files changed, 2563 insertions(+) create mode 100644 plans/001-headless-smoke-and-ci.md create mode 100644 plans/002-flash-tresitter-typo.md create mode 100644 plans/003-web-linter-biomejs.md create mode 100644 plans/004-java-bufnr-args-buf.md create mode 100644 plans/005-readme-stale-theme-sidekick.md create mode 100644 plans/006-rust-leader-ca-clobber.md create mode 100644 plans/007-ck-collision-tmux-sighelp.md create mode 100644 plans/008-99-md-files-agents.md create mode 100644 plans/009-keymap-desc-99-toggleterm.md create mode 100644 plans/010-lsp-toggle-ui.md create mode 100644 plans/README.md diff --git a/plans/001-headless-smoke-and-ci.md b/plans/001-headless-smoke-and-ci.md new file mode 100644 index 0000000..2d9e8e0 --- /dev/null +++ b/plans/001-headless-smoke-and-ci.md @@ -0,0 +1,251 @@ +# Plan 001: Add a headless load smoke test and CI workflow + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: this plan creates **new** files +> (`scripts/smoke.sh`, `.github/workflows/smoke.yml`); there is no existing +> content to drift against. Confirm the repo has no existing `scripts/` or +> `.github/` content that collides: +> `ls scripts .github 2>/dev/null` → either "No such file" or only unrelated +> files. + +## Status + +- **Priority**: P1 +- **Effort**: M +- **Risk**: LOW +- **Depends on**: none +- **Category**: dx / tests +- **Planned at**: commit `baf9a2c`, 2026-06-18 +- **Finding**: DX-9 (no verification baseline) + +## Why this matters + +BASEDDvim currently has **no automated way to know the config even loads** — +no tests, no CI, no smoke script. To check anything you must open nvim by +hand. That gap is exactly how three runtime bugs shipped (see plans 002–004) +and why nothing guards future changes. This plan establishes the baseline: a +one-command headless load check plus a GitHub Action that runs it on every +push. + +**Honest scope note (important):** a *load-only* smoke test does **not** catch +the three runtime bugs in plans 002–004 — those live in a keymap function +body, a table string, and a callback closure, none of which execute at load +time. What this baseline *does* catch: broken `require()`s, syntax that only +errors at load, bad top-level calls, and future regressions of that class. It +is the prerequisite for a healthy repo and the model for the per-plan +verifications. Do not oversell it as catching the bugs. + +## Current state + +- The repo is a lazy.nvim config installed at `~/.config/nvim` (per `README.md` + install instructions). Entry point `init.lua` requires `vim-options` then + sets up lazy with `{ import = "plugins" }`. +- There is **no** `scripts/` directory and **no** `.github/` directory today. +- Verified recon facts (determined on a nvim 0.12.3 box, 2026-06-18): + 1. `nvim --headless -u NONE +qa` → exit code `0`, empty output. (clean baseline) + 2. **`nvim --headless -u +qa` exits `0` even when the config + errors at load.** A bad `require()` printed `Error in :`, `E5113:`, + and `stack traceback:` but the process still returned 0. + → **Conclusion: exit code is unreliable; the smoke test MUST inspect + output for error patterns.** + 3. Loading with `-u ` also pulls in the host machine's real + `~/.config/nvim` (noise was observed from `/home/moshi/.config/nvim/plugin/*.lua`). + → The smoke test must run against an **isolated** config dir so the host's + personal config does not leak in and create false failures. + +### Repo conventions to match + +- Shell scripts: simple, `set -uo pipefail`, an informational final `echo`. +- No existing CI / formatter / linter config to mirror. Use a standard, + widely-used Vim setup action for CI (`rhysd/action-setup-vim@v1`). +- New top-level dirs are fine (`scripts/`, `.github/workflows/`). + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Make script executable | `chmod +x scripts/smoke.sh` | exit 0 | +| Run smoke locally | `./scripts/smoke.sh` | prints `OK: config loaded cleanly`, exit 0 | +| Parse-check the script | `bash -n scripts/smoke.sh` | exit 0, no output | +| Validate workflow YAML | `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/smoke.yml'))"` | exit 0, no output | + +(`nvim` is the only runtime tool. `python3` is used only for a one-off YAML +sanity check during this plan; it is not a runtime dependency.) + +## Scope + +**In scope** (the only files you should create): +- `scripts/smoke.sh` (new) +- `.github/workflows/smoke.yml` (new) + +**Out of scope** (do NOT touch): +- `init.lua`, anything under `lua/` — this plan adds tooling, it does not + change config behavior. +- Any attempt to make the smoke test exercise keymaps, LSP, or linting. That + belongs in the bug-fix plans (002–004), not here. + +## Git workflow + +- Branch: `advisor/001-smoke-ci` +- One commit is fine (two small new files). Message style — the repo uses + conventional commits (see `git log --oneline`): `ci: add headless config + smoke test`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Create `scripts/smoke.sh` + +Create `scripts/smoke.sh` with exactly this content: + +```bash +#!/usr/bin/env bash +# Headless load smoke test for BASEDDvim. +# +# Loads the full config in a clean, isolated Neovim instance and fails if +# Neovim emits any error patterns while doing so. +# +# Why inspect output (not exit code): `nvim --headless +qa` returns 0 even +# when the config errors at load time, so the exit code is not a reliable +# signal. We grep for Neovim's own error markers instead. +# +# Limitations: this is a LOAD test. It will not catch bugs that only trigger +# on a keypress, on opening a specific filetype, or inside an LSP callback. +# See plans/002..004 for those. +set -uo pipefail + +# Isolate the config so the host machine's personal ~/.config/nvim does not +# leak in and create false failures. Point XDG_CONFIG_HOME at this repo's +# parent layout: /nvim -> . +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ISOLATED="$(mktemp -d)" +trap 'rm -rf "$ISOLATED"' EXIT +ln -s "$REPO_ROOT" "$ISOLATED/nvim" + +LOG="$(mktemp)" + +XDG_CONFIG_HOME="$ISOLATED" nvim --headless "+qa" >"$LOG" 2>&1 + +# Neovim error markers. Tuned to Neovim's exact output formats observed in +# recon: "Error in :", "E5xxx:" codes, "stack traceback:", and the +# common nil-access messages. +if grep -nE 'Error in |E5[0-9]{3}:|stack traceback:|attempt to (call|index) a nil value' "$LOG"; then + echo "FAIL: errors detected while loading BASEDDvim config" >&2 + echo "--- captured output ---" >&2 + cat "$LOG" >&2 + exit 1 +fi + +echo "OK: config loaded cleanly" +``` + +**Verify**: +- `bash -n scripts/smoke.sh` → exit 0, no output (syntax OK). +- `chmod +x scripts/smoke.sh` → exit 0. + +### Step 2: Run the smoke test locally + +**Verify**: `./scripts/smoke.sh` → prints exactly `OK: config loaded cleanly`, +exit code 0. + +If it FAILS here on a clean checkout, the most likely cause is that the +executor's machine doesn't have the lazy plugins cloned yet. In that case, +first run `nvim --headless "+Lazy! sync" +qa` once (with the same isolated +`XDG_CONFIG_HOME`) to populate `~/.local/share/nvim/lazy`, then re-run +`./scripts/smoke.sh`. (See STOP conditions.) + +### Step 3: Create `.github/workflows/smoke.yml` + +Create `.github/workflows/smoke.yml` with exactly this content: + +```yaml +name: smoke + +on: + push: + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: stable + + - name: Install plugins (lazy.nvim) + run: | + ISOLATED="$(mktemp -d)" + ln -s "$PWD" "$ISOLATED/nvim" + XDG_CONFIG_HOME="$ISOLATED" nvim --headless "+Lazy! sync" +qa + + - name: Smoke test + run: scripts/smoke.sh +``` + +**Verify**: +- `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/smoke.yml'))"` → + exit 0, no output (valid YAML). +- `grep -n 'smoke.sh' .github/workflows/smoke.yml` → one match in the + `Smoke test` step. + +### Step 4: Commit and report + +- Stage `scripts/smoke.sh` and `.github/workflows/smoke.yml`. +- Commit: `ci: add headless config smoke test`. +- Report the local `./scripts/smoke.sh` output in the PR/verdict. + +**Verify**: `git status --porcelain` → only the two new files staged/committed; +nothing under `lua/` or `init.lua` changed. + +## Done criteria + +Machine-checkable. ALL must hold: + +- [ ] `scripts/smoke.sh` exists, is executable (`ls -l scripts/smoke.sh` shows + `x` bits), and `bash -n scripts/smoke.sh` exits 0. +- [ ] `./scripts/smoke.sh` prints `OK: config loaded cleanly` and exits 0. +- [ ] `.github/workflows/smoke.yml` exists and parses as valid YAML. +- [ ] `git status --porcelain` shows no changes to `init.lua` or `lua/`. +- [ ] `plans/README.md` status row for 001 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- `./scripts/smoke.sh` fails even after running `Lazy! sync` in the isolated + config dir — the config may have a genuine load error (which would itself be + a new finding; report it, do not patch it here). +- The smoke output contains matching error lines that are clearly **benign + plugin noise** rather than real errors (e.g. a plugin printing the literal + word "error" in normal output). Report the lines; do not silently loosen the + grep. The operator will decide whether to tune the pattern. +- `nvim --headless "+Lazy! sync" +qa` hangs for more than ~2 minutes — likely a + plugin prompting interactively. Report which plugin; do not modify any + `lua/plugins/*.lua` to work around it in this plan. +- `rhysd/action-setup-vim@v1` is unavailable or the chosen `version: stable` + is too old for `vim.lsp.enable` (needs Neovim 0.11+). Report; the operator + will pin a different version. + +## Maintenance notes + +- **What future changes interact with this**: any new plugin added under + `lua/plugins/` is automatically covered by the load smoke. Bug fixes that + need *runtime* verification (keymaps, filetype handlers, LSP callbacks) + should add their own targeted checks — do not assume the load smoke covers + them. +- **Reviewer focus**: confirm the grep pattern wasn't loosened just to make + the test pass; a green run with a gutted pattern is worse than no test. +- **Deferred follow-ups** (out of scope here): a `luacheck` step would + statically catch undefined globals (e.g. it would flag the `bufnr` bug in + plan 004). Not added now because luacheck isn't in the toolchain; consider + `:MasonInstall luacheck` + a `.luacheckrc` as a separate plan. diff --git a/plans/002-flash-tresitter-typo.md b/plans/002-flash-tresitter-typo.md new file mode 100644 index 0000000..6ff794d --- /dev/null +++ b/plans/002-flash-tresitter-typo.md @@ -0,0 +1,156 @@ +# Plan 002: Fix the `flash.nvim` keymap typo (`tresitter_search`) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat baf9a2c..HEAD -- lua/plugins/flash.lua` +> If `lua/plugins/flash.lua` changed since this plan was written, compare the +> "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 001, 003, 004, 005) +- **Category**: bug +- **Planned at**: commit `baf9a2c`, 2026-06-18 +- **Finding**: BUG-1 + +## Why this matters + +The `R` keymap (operator-pending + visual modes) calls +`require('flash').tresitter_search()`. That method does not exist — the real +API is `treesitter_search`. So pressing `R` throws +`attempt to call a nil value (method 'tresitter_search')` every time, instead +of performing the "Treesitter Search" the keymap advertises. A one-character +class of typo that a load test cannot catch (the function body only runs on +keypress), which is why it survived. + +## Current state + +`lua/plugins/flash.lua` — the flash.nvim plugin spec. The bug is on line 10. +The four sibling keymaps (lines 7, 8, 9, 11) call the correct method names and +are fine. Current file in full: + +```lua +return { + { + 'folke/flash.nvim', + --@type Flash.Config + opts = {}, + keys = { + { 's', mode = {'n', 'x', 'o'}, function() require('flash').jump() end, desc = 'Flash' }, + { 'S', mode = {'n', 'x', 'o'}, function() require('flash').treesitter() end, desc = 'Flash Treesitter' }, + { 'r', mode = {'o'}, function() require('flash').remote() end, desc = 'Remote Flash' }, + { 'R', mode = {'o', 'x'}, function() require('flash').tresitter_search() end, desc = 'Treesitter Search' }, + { '', mode = {'c'}, function() require('flash').toggle() end, desc = 'Toggle Flash Search' }, + }, + } +} +``` + +The correct API method name is confirmed by the sibling keymap on line 8 +(`treesitter()`) and flash.nvim's public API: `jump`, `treesitter`, `remote`, +`treesitter_search`, `toggle`. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm typo gone | `grep -n 'tresitter' lua/plugins/flash.lua` | no matches (exit 1) | +| Confirm correct name present | `grep -n 'treesitter_search' lua/plugins/flash.lua` | one match, line 10 | +| Method exists at runtime | `nvim --headless +'lua assert(type(require("flash").treesitter_search)=="function")' +qa` | exit 0, no output | + +The runtime check requires the `flash.nvim` plugin to be installed (it is, per +`lazy-lock.json`). If running in a fresh environment, run +`nvim --headless "+Lazy! sync" +qa` first. + +## Scope + +**In scope** (the only file you should modify): +- `lua/plugins/flash.lua` — change exactly one token on line 10. + +**Out of scope** (do NOT touch): +- The other four keymaps — they are correct. +- `opts = {}` and anything else in the file. +- Any other plugin's keymaps, even though a repo-wide audit of method-name + typos is tempting; keep this plan surgical. + +## Git workflow + +- Branch: `advisor/002-flash-typo` +- Single commit. Message style (conventional commits, per `git log`): + `fix: correct flash treesitter_search keymap typo`. + +## Steps + +### Step 1: Rename the method call + +In `lua/plugins/flash.lua` line 10, change `tresitter_search` to +`treesitter_search` (insert the missing `e` after the `tr`). The line becomes: + +```lua + { 'R', mode = {'o', 'x'}, function() require('flash').treesitter_search() end, desc = 'Treesitter Search' }, +``` + +Leave the leading whitespace (tabs), the `desc`, and everything else untouched. + +**Verify**: +- `grep -n 'tresitter' lua/plugins/flash.lua` → no matches. +- `grep -n 'treesitter_search' lua/plugins/flash.lua` → exactly one match on + line 10. + +### Step 2: Confirm the method resolves at runtime + +**Verify**: +`nvim --headless +'lua assert(type(require("flash").treesitter_search)=="function")' +qa` +→ exit 0, no output. (If flash isn't installed in this env, run `Lazy! sync` +first — see STOP conditions.) + +### Step 3: Commit + +- Stage `lua/plugins/flash.lua`. +- Commit: `fix: correct flash treesitter_search keymap typo`. + +**Verify**: `git show --stat HEAD` → exactly one file changed +(`lua/plugins/flash.lua`), one line. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'tresitter' lua/plugins/flash.lua` returns no matches. +- [ ] `grep -n 'treesitter_search' lua/plugins/flash.lua` returns one match + (line 10). +- [ ] `nvim --headless +'lua assert(type(require("flash").treesitter_search)=="function")' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/plugins/flash.lua` changed. +- [ ] `plans/README.md` status row for 002 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The code at `lua/plugins/flash.lua:10` does not match the excerpt above (the + line has drifted — e.g. someone already fixed it, or the keymap was + removed). Report what you see. +- The runtime assertion fails because `require("flash").treesitter_search` is + `nil` even after `Lazy! sync`. That would mean the installed flash.nvim + version exposes a different API — report the version and stop; do not guess + an alternate method name. +- Any other keymap in the file also looks wrong. Report it; do not expand + scope. + +## Maintenance notes + +- This is the kind of typo a future static check could catch if flash.nvim + shipped LuaLS type annotations (it does ship `meta/` annotations in some + versions). If `lazydev.nvim` + flash annotations are ever wired up, + `lua_ls` would flag `tresitter_search` as an unknown field automatically. +- Reviewer focus: confirm only the single token changed. diff --git a/plans/003-web-linter-biomejs.md b/plans/003-web-linter-biomejs.md new file mode 100644 index 0000000..2659c82 --- /dev/null +++ b/plans/003-web-linter-biomejs.md @@ -0,0 +1,253 @@ +# Plan 003: Fix nvim-lint linter name (`biomejs` → `biome`) and honor `opts.events` + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat baf9a2c..HEAD -- lua/plugins/web-linter.lua` +> If `lua/plugins/web-linter.lua` changed since this plan was written, compare +> the "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 001, 002, 004, 005) +- **Category**: bug +- **Planned at**: commit `baf9a2c`, 2026-06-18 +- **Finding**: BUG-3 + +## Why this matters + +`web-linter.lua` registers JS/TS linting under the linter name `'biomejs'`, but +nvim-lint has no linter by that name — the linter nvim-lint ships (and the +Mason package this file installs, see line 3) is registered as `'biome'`. +Because nvim-lint silently skips unknown linter names, `lint.try_lint()` runs +**nothing** for JavaScript/TypeScript. The entire `nvim-lint` setup is a +no-op — with no error to tip you off. Separately, the file declares +`opts.events = { "BufWritePost", "BufReadPost", "InsertLeave" }` but the +`config` function ignores it and hardcodes `{ 'BufWritePost', 'BufEnter' }`, +so the declared event list is misleading dead config. + +## Current state + +`lua/plugins/web-linter.lua` — the nvim-lint plugin spec. Current file in +full: + +```lua +local linters = { + -- web + 'biome', +} + +return { + { + 'mfussenegger/nvim-lint', + ft = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact' }, + opts = { + -- Events to trigger linter + events = { "BufWritePost", "BufReadPost", "InsertLeave" }, + linters_by_ft = { + javascript = { 'biomejs' }, + javascriptreact = { 'biomejs' }, + typescript = { 'biomejs' }, + typescriptreact = { 'biomejs' }, + }, + }, + config = function(_, opts) + local mason_registry = require('mason-registry') + for _, linter in ipairs(linters) do + if not mason_registry.is_installed(linter) then + vim.cmd('MasonInstall ' .. linter) + end + end + + local lint = require('lint') + lint.linters_by_ft = opts.linters_by_ft + + vim.api.nvim_create_autocmd( + { 'BufWritePost', 'BufEnter' }, + { + callback = function() + lint.try_lint() + end + } + ) + end, + dependencies = { + { 'williamboman/mason.nvim' } + } + }, +} +``` + +Note the Mason install list at line 3 uses `'biome'` (the correct package +name) — so the tool gets installed correctly, but it is never wired to a +linter name that nvim-lint recognizes. The fix is to make `linters_by_ft` +match the registered linter name `'biome'`. + +### Repo conventions to match + +- The `mason_lsp_mapping` style in `lua/plugins/lspconfig.lua` shows the + repo's pattern of keeping names consistent between Mason packages and their + consumers. +- Keep the existing structure (top-level `local linters`, `opts` table, + `config = function(_, opts)`). Do not restructure. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm bad name gone | `grep -n 'biomejs' lua/plugins/web-linter.lua` | no matches (exit 1) | +| Confirm good name present | `grep -n "'biome'" lua/plugins/web-linter.lua` | 5 matches (1 install list + 4 by_ft) | +| Linter registered | `nvim --headless +'lua assert(require("lint").linters.biome ~= nil)' +qa` | exit 0, no output | + +The runtime check requires `nvim-lint` (and its `biome` linter module) to be +installed. Per `lazy-lock.json` nvim-lint is a plugin; `biome` is installed via +Mason by this very file. In a fresh env run `nvim --headless "+Lazy! sync" +qa` +and ensure `:MasonInstall biome` has run. + +## Scope + +**In scope** (the only file you should modify): +- `lua/plugins/web-linter.lua` — rename the 4 `'biomejs'` entries to + `'biome'`, and wire `opts.events` into the autocmd. + +**Out of scope** (do NOT touch): +- `lua/plugins/lspconfig.lua` — the conform formatter table there also uses + biome (`biome` vs `biome-check` inconsistency, finding TECH-7). That is a + *formatter* concern in a different file; it is deferred, not part of this + plan. Do not "fix" it here. +- The Mason auto-install loop (lines 21–26). It already installs `biome` + correctly. + +## Git workflow + +- Branch: `advisor/003-linter-biome-name` +- Single commit. Message style (conventional commits): `fix: use correct + nvim-lint 'biome' linter name and honor configured events`. + +## Steps + +### Step 1: Rename the linter (the actual bug) + +In `lua/plugins/web-linter.lua`, change every occurrence of `{ 'biomejs' }` in +the `linters_by_ft` table to `{ 'biome' }`. There are four (javascript, +javascriptreact, typescript, typescriptreact). The table becomes: + +```lua + linters_by_ft = { + javascript = { 'biome' }, + javascriptreact = { 'biome' }, + typescript = { 'biome' }, + typescriptreact = { 'biome' }, + }, +``` + +**Verify**: +- `grep -n 'biomejs' lua/plugins/web-linter.lua` → no matches. +- `grep -n "'biome'" lua/plugins/web-linter.lua` → 5 matches (the existing + `'biome'` in the `linters` list at line 3, plus the 4 you just changed). + +### Step 2: Honor `opts.events` in the autocmd + +The `opts.events` field is currently dead config. Wire it into the autocmd so +the declared events are actually used (and so the option isn't misleading). +Replace the hardcoded event list in the `nvim.api.nvim_create_autocmd` call +with `opts.events`: + +Before: +```lua + vim.api.nvim_create_autocmd( + { 'BufWritePost', 'BufEnter' }, + { + callback = function() + lint.try_lint() + end + } + ) +``` + +After: +```lua + vim.api.nvim_create_autocmd( + opts.events, + { + callback = function() + lint.try_lint() + end + } + ) +``` + +`opts.events` is `{ "BufWritePost", "BufReadPost", "InsertLeave" }` (line 12), +which is a reasonable lint trigger set and matches the file's own comment +("Events to trigger linter"). This makes linting also run on read and on +leaving insert mode, which is the intended behavior the option was declaring. + +**Verify**: +- `grep -n 'BufEnter' lua/plugins/web-linter.lua` → no matches (the hardcoded + `'BufEnter'` is gone; `opts.events` does not contain it). +- `grep -n 'opts.events' lua/plugins/web-linter.lua` → 2 matches (the + declaration on line 12 and the autocmd use). + +### Step 3: Confirm the linter resolves at runtime + +**Verify**: +`nvim --headless +'lua assert(require("lint").linters.biome ~= nil)' +qa` +→ exit 0, no output. (Requires nvim-lint installed; run `Lazy! sync` first if +needed — see STOP conditions.) + +### Step 4: Commit + +- Stage `lua/plugins/web-linter.lua`. +- Commit: `fix: use correct nvim-lint 'biome' linter name and honor configured events`. + +**Verify**: `git show --stat HEAD` → exactly one file changed. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'biomejs' lua/plugins/web-linter.lua` returns no matches. +- [ ] `grep -n "'biome'" lua/plugins/web-linter.lua` returns 5 matches. +- [ ] `grep -n 'opts.events' lua/plugins/web-linter.lua` returns 2 matches + (declaration + autocmd use); `'BufEnter'` no longer appears hardcoded. +- [ ] `nvim --headless +'lua assert(require("lint").linters.biome ~= nil)' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/plugins/web-linter.lua` changed. +- [ ] `plans/README.md` status row for 003 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The code at the cited lines does not match the excerpts (drift). Report what + you see. +- `require("lint").linters.biome` is `nil` even after `Lazy! sync` and + `:MasonInstall biome`. That would mean this nvim-lint version registers the + linter under a different name — run + `nvim --headless +'lua print(vim.inspect(vim.tbl_keys(require("lint").linters)))' +qa` + and report the keys; do not guess. +- Step 2's change to `opts.events` causes a load error (e.g. `opts` shape + differs). Report; do not revert to hardcoding silently — ask the operator. + +## Maintenance notes + +- **Related but separate**: `lspconfig.lua`'s conform formatter table mixes + `biome` and `biome-check` across JS/TS filetypes (finding TECH-7, deferred). + If that is later reconciled, keep the *linter* name (here, `biome`) and the + *formatter* name (there, `biome`/`biome-check`) distinct in your head — they + are different tools' registrations. +- **Reviewer focus**: confirm Step 2 didn't accidentally change which events + fire beyond what `opts.events` declares, and that no other autocmd was + touched. +- Wiring `opts.events` (rather than deleting the misleading field) was chosen + over removal because the repo's convention is to keep trigger config in the + `opts` table; if the maintainer prefers fewer events, they can edit the one + `opts.events` line. diff --git a/plans/004-java-bufnr-args-buf.md b/plans/004-java-bufnr-args-buf.md new file mode 100644 index 0000000..5e1e3a0 --- /dev/null +++ b/plans/004-java-bufnr-args-buf.md @@ -0,0 +1,210 @@ +# Plan 004: Fix undefined `bufnr` in the jdtls `LspAttach` callback + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat baf9a2c..HEAD -- lua/plugins/java.lua` +> If `lua/plugins/java.lua` changed since this plan was written, compare the +> "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 001, 002, 003, 005) +- **Category**: bug +- **Planned at**: commit `baf9a2c`, 2026-06-18 +- **Finding**: BUG-2 + +## Why this matters + +In `java.lua`, the `LspAttach` autocmd callback calls +`keymaps.lsp({ buffer = bufnr })` and `keymaps.lsp_format({ buffer = bufnr })`, +but **`bufnr` is never defined in that scope**. It is not a parameter and not +a local. With `buffer = nil`, `vim.keymap.set` drops the buffer scoping and +binds the LSP keys (`gd`, `K`, `rn`, `ca`, etc.) to the +**current window globally** instead of to the Java buffer that jdtls attached +to — so the keymaps land on whatever buffer was focused when jdtls attached, +and leak beyond Java files. The correct value is `args.buf`, the buffer number +Neovim passes to every `LspAttach` callback (see `:help LspAttach`). + +## Current state + +`lua/plugins/java.lua` — the `nvim-jdtls` plugin spec. The bug is inside the +`LspAttach` autocmd registered in the `config` function. Relevant excerpt +(line numbers are approximate; rely on the text match): + +```lua + -- Setup keymap and dap after the lsp is fully attached. + -- https://github.com/mfussenegger/nvim-jdtls#nvim-dap-configuration + -- https://neovim.io/doc/user/lsp.html#LspAttach + vim.api.nvim_create_autocmd("LspAttach", { + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + if client and client.name == "jdtls" then + local keymaps = require("keymaps") + keymaps.lsp({ buffer = bufnr }) + keymaps.lsp_format({ buffer = bufnr }) + + -- User can set additional keymaps in opts.on_attach + if opts.on_attach then + opts.on_attach(args) + end + end + end, + }) +``` + +The callback's only parameter is `args` (an autocmd event object). Per +`:help LspAttach`, the buffer number is `args.buf`. `bufnr` is an undefined +global read here (resolves to `nil`). + +### How the repo does this correctly elsewhere + +`lua/plugins/lspconfig.lua` does the same thing right — its `on_attach` +receives `bufnr` as a real parameter: + +```lua +local default_lspconfig = function(capabilities) + return { + on_attach = function(_, bufnr) + keymaps.lsp({ buffer = bufnr }) + keymaps.lsp_format({ buffer = bufnr }) + end, + ... +``` + +There, `bufnr` is the second argument to `on_attach`, so it is defined. In +`java.lua` the equivalent context is an autocmd callback, which does **not** +receive a `bufnr` parameter — it receives `args`, and the buffer is `args.buf`. +That is the only change needed. + +### Repo conventions to match + +- `keymaps.lsp({ buffer = bufnr })` / `keymaps.lsp_format({ buffer = bufnr })` + is the established pattern (see `lspconfig.lua` and `lua/keymaps.lua`). Keep + the call shape identical; only the source of the buffer number changes. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm bare `bufnr` gone | `grep -n 'bufnr' lua/plugins/java.lua` | no matches (exit 1) | +| Confirm `args.buf` present | `grep -n 'args.buf' lua/plugins/java.lua` | ≥2 matches (the two keymap calls) | +| Parse-check file | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/java.lua"); assert(fn,err)' +qa` | exit 0, no output | + +A full runtime proof (open a `.java` file, let jdtls attach, assert the keymaps +are buffer-local) requires a JDK project and jdtls installed — heavy and out of +scope for verification here. The static checks above plus comparison to the +known-good `lspconfig.lua` pattern are sufficient evidence; the optional +runtime check is described in "Test plan" for whoever wants it. + +## Scope + +**In scope** (the only file you should modify): +- `lua/plugins/java.lua` — replace `bufnr` with `args.buf` in the two keymap + calls inside the `LspAttach` callback. + +**Out of scope** (do NOT touch): +- The `opts.on_attach(args)` call further down — it correctly passes `args`; + not a bug. +- The `attach_jdtls` function, the `FileType` autocmd, and everything else in + the file. +- Other files. `lspconfig.lua` and `rustaceanvim` already do this correctly. + +## Git workflow + +- Branch: `advisor/004-java-bufnr` +- Single commit. Message style (conventional commits): `fix: scope jdtls LSP + keymaps to args.buf in LspAttach`. + +## Steps + +### Step 1: Replace `bufnr` with `args.buf` + +Inside the `LspAttach` callback in `lua/plugins/java.lua`, change both +occurrences of `{ buffer = bufnr }` to `{ buffer = args.buf }`: + +```lua + local keymaps = require("keymaps") + keymaps.lsp({ buffer = args.buf }) + keymaps.lsp_format({ buffer = args.buf }) +``` + +Change nothing else (not the `client` lookup, not the `opts.on_attach(args)` +line, not the comments). + +**Verify**: +- `grep -n 'bufnr' lua/plugins/java.lua` → no matches. +- `grep -n 'args.buf' lua/plugins/java.lua` → at least 2 matches on the two + keymap calls. + +### Step 2: Parse-check the file + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/java.lua"); assert(fn,err)' +qa` +→ exit 0, no output. (This confirms the edit didn't break Lua syntax. It does +NOT execute the file's top-level `return`, so it won't trigger plugin loads.) + +### Step 3: Commit + +- Stage `lua/plugins/java.lua`. +- Commit: `fix: scope jdtls LSP keymaps to args.buf in LspAttach`. + +**Verify**: `git show --stat HEAD` → exactly one file changed +(`lua/plugins/java.lua`); `git show HEAD` shows only the two `bufnr`→`args.buf` +edits. + +## Test plan + +- **Static (required, see Done criteria)**: `grep` and parse checks above. +- **Optional runtime proof** (only if a Java project + jdtls are handy): open a + `.java` file in a project with a build file jdtls recognizes, wait for + `LspAttach`, then run + `:lua print(vim.inspect(vim.tbl_map(function(m) return m.lhs end, vim.api.nvim_buf_get_keymap(0, "n"))))` + — `gd`, `K`, `gr`, etc. should be listed as buffer-local keymaps for *that* + buffer, and switching to a non-Java buffer should NOT carry them. Before the + fix they leak globally; after it they are scoped to the Java buffer. This is + confirmation only — not required to mark the plan done. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'bufnr' lua/plugins/java.lua` returns no matches. +- [ ] `grep -n 'args.buf' lua/plugins/java.lua` returns ≥2 matches. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/java.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] `git show HEAD` shows only the two `bufnr` → `args.buf` edits in + `lua/plugins/java.lua`; no other file changed. +- [ ] `plans/README.md` status row for 004 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The code at the `LspAttach` callback does not match the excerpt (drift — + e.g. someone already fixed it, or the callback signature changed). Report + what you see. +- `bufnr` appears anywhere else in `java.lua` for a legitimate reason (it + doesn't today, but if a future edit added one, do not blindly delete it — + report and ask). +- The parse check fails after the edit — report the exact error; do not + "fix" surrounding code to make it parse. + +## Maintenance notes + +- **What interacts with this later**: if jdtls on_attach logic is ever + consolidated with the `lspconfig.lua` `on_attach` helper, remember the + buffer-number source differs by context (`on_attach(_, bufnr)` vs + autocmd `args.buf`). A future `luacheck` step (see plan 001's deferred + follow-up) would catch undefined-global reads like this one automatically — + that is the highest-leverage guard against recurrence. +- **Reviewer focus**: confirm exactly two tokens changed and the keymaps are + now scoped to `args.buf`, not applied globally. diff --git a/plans/005-readme-stale-theme-sidekick.md b/plans/005-readme-stale-theme-sidekick.md new file mode 100644 index 0000000..f7d0b32 --- /dev/null +++ b/plans/005-readme-stale-theme-sidekick.md @@ -0,0 +1,189 @@ +# Plan 005: Update the stale README (colorscheme + sidekick fork) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat baf9a2c..HEAD -- README.md` +> If `README.md` changed since this plan was written, compare the "Current +> state" excerpt against the live file before proceeding; on a mismatch, +> treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 001–004) +- **Category**: docs +- **Planned at**: commit `baf9a2c`, 2026-06-18 +- **Finding**: DOCS-12 + +## Why this matters + +`README.md` advertises a different stack than the code actually ships. Two +factual errors: + +1. **Colorscheme**: README lists `monokai-pro.nvim` (machine filter) under + "Editor UI & Theme", but the active colorscheme is `gruvbox.nvim` — + `monokai-pro` is commented out in `lua/plugins/colorscheme.lua`. New users + get wrong theme expectations. +2. **sidekick.nvim fork**: README links `qapquiz/sidekick.nvim`, but commit + `474e8af` ("chore: change from qapquiz/sidekick.nvim => folke/sidekick.nvim") + switched the source to `folke/sidekick.nvim`. The README link is stale. + +This plan corrects both. It is scoped strictly to these two factual fixes; a +full README/keybindings audit is explicitly out of scope. + +## Current state + +`README.md` — the two stale lines (line numbers from recon): + +Under "### AI & Code Completion": +``` +- **[sidekick.nvim](https://github.com/qapquiz/sidekick.nvim)** - CLI integration for AI tools (Claude, etc.) +``` + +Under "### Editor UI & Theme": +``` +- **[monokai-pro.nvim](https://github.com/gthelding/monokai-pro.nvim)** - Monokai Pro colorscheme (machine filter) +``` + +### Ground truth from the code + +- `lua/plugins/colorscheme.lua:15` — the only **active** (uncommented) + colorscheme spec: + ```lua + "ellisonleao/gruvbox.nvim", + ``` + with `config` calling `vim.cmd("colorscheme gruvbox")`. All other + colorschemes (monokai-pro at line 96, tokyonight, evergarden, bamboo, + aether, koda) are commented out. +- `lua/plugins/ai.lua` — the sidekick spec uses `folke/sidekick.nvim` (the + `474e8af` commit message confirms the migration from `qapquiz/`). +- README "Installed Language Servers" and "Installed Formatters" sections + already match the code (`lua_ls`, `ts_ls`, `rust_analyzer`, `gopls`, + `ruby_lsp`, `jdtls`; `stylua`, `biome`, `google-java-format`) — leave them + alone. + +### Repo conventions to match + +- Keep the existing bullet style: `- **[name](url)** - description.` +- Keep descriptions concise and matching the repo's tone. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Stale refs gone | `grep -nE 'qapquiz\|monokai-pro' README.md` | no matches (exit 1) | +| New refs present | `grep -nE 'folke/sidekick\|gruvbox' README.md` | ≥2 matches | +| Confirm monokai not mentioned elsewhere | `grep -ni 'monokai' README.md` | no matches | + +## Scope + +**In scope** (the only file you should modify): +- `README.md` — two bullets: the sidekick link/owner and the colorscheme + bullet. + +**Out of scope** (do NOT touch, even though related): +- The extensive **Keybindings** tables in README. They are not part of this + finding and a full accuracy audit is a separate effort. Do not rewrite them. +- `lua/plugins/colorscheme.lua` — the commented-out monokai-pro block is an + intentional "alternative palette"; leave it (finding TECH-6, deferred). +- Any other README sections (plugins lists for LSP/Completion/etc. are already + accurate). + +## Git workflow + +- Branch: `advisor/005-readme-fix` +- Single commit. Message style (conventional commits): `docs: fix stale + colorscheme and sidekick fork in README`. + +## Steps + +### Step 1: Fix the sidekick fork reference + +In `README.md`, under "### AI & Code Completion", replace: + +``` +- **[sidekick.nvim](https://github.com/qapquiz/sidekick.nvim)** - CLI integration for AI tools (Claude, etc.) +``` + +with: + +``` +- **[sidekick.nvim](https://github.com/folke/sidekick.nvim)** - CLI integration for AI tools (Claude, etc.) +``` + +(Only the URL owner changes: `qapquiz` → `folke`. Text and description stay the +same.) + +**Verify**: `grep -n 'qapquiz' README.md` → no matches. + +### Step 2: Fix the colorscheme bullet + +In `README.md`, under "### Editor UI & Theme", replace: + +``` +- **[monokai-pro.nvim](https://github.com/gthelding/monokai-pro.nvim)** - Monokai Pro colorscheme (machine filter) +``` + +with: + +``` +- **[gruvbox.nvim](https://github.com/ellisonleao/gruvbox.nvim)** - Gruvbox colorscheme +``` + +(Repo/URL from `lua/plugins/colorscheme.lua:15`: +`ellisonleao/gruvbox.nvim`.) + +**Verify**: +- `grep -n 'monokai' README.md` → no matches (case-insensitive). +- `grep -n 'gruvbox' README.md` → one match on the new bullet. + +### Step 3: Commit + +- Stage `README.md`. +- Commit: `docs: fix stale colorscheme and sidekick fork in README`. + +**Verify**: `git show --stat HEAD` → exactly one file changed (`README.md`). + +## Done criteria + +ALL must hold: + +- [ ] `grep -nE 'qapquiz|monokai-pro' README.md` returns no matches. +- [ ] `grep -ni 'monokai' README.md` returns no matches. +- [ ] `grep -nE 'folke/sidekick|ellisonleao/gruvbox' README.md` returns ≥2 + matches. +- [ ] `git show --stat HEAD` shows only `README.md` changed. +- [ ] `plans/README.md` status row for 005 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The two target lines in `README.md` do not match the excerpts (drift — e.g. + README was already updated, or reworded). Report what you see. +- You find additional stale entries while editing (e.g. another plugin link + that no longer matches the code). Report them as a list; do **not** expand + scope to fix them in this commit. +- The "Keybindings" tables turn out to contain errors — note it for a + follow-up; do not edit them here. + +## Maintenance notes + +- **What interacts with this later**: whenever the colorscheme or AI stack + changes, the README's "Editor UI & Theme" and "AI & Code Completion" bullets + must be updated in lockstep. The repo has a recurring pattern of the README + lagging the code (this very finding, plus the `474e8af` sidekick migration + that didn't update the README). A future contributor should check these two + sections against `colorscheme.lua` and `ai.lua` on any such change. +- **Reviewer focus**: confirm exactly two bullets changed and no keybindings + or other sections were touched. +- **Deferred follow-up**: a full README accuracy audit (keybindings tables vs. + the actual `keys =` specs across `lua/plugins/*.lua`) is worth a separate + plan if the maintainer wants the docs to be authoritative. diff --git a/plans/006-rust-leader-ca-clobber.md b/plans/006-rust-leader-ca-clobber.md new file mode 100644 index 0000000..02088ab --- /dev/null +++ b/plans/006-rust-leader-ca-clobber.md @@ -0,0 +1,227 @@ +# Plan 006: Stop `keymaps.lsp()` from clobbering Rust's `ca` + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat f5c7d00..HEAD -- lua/plugins/lspconfig.lua` +> If `lua/plugins/lspconfig.lua` changed since this plan was written, compare +> the "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 007, 008, 009) +- **Category**: bug +- **Planned at**: commit `f5c7d00`, 2026-06-18 +- **Finding**: N1 + +## Why this matters + +The `rustaceanvim` `on_attach` sets `ca` to `vim.cmd.RustLsp("codeAction")` +— rust-analyzer's richer code-action menu (runnables, rebuild proc-macros, +expand macro, etc.). It then calls `keymaps.lsp({ buffer = bufnr })`, which +re-sets the **same** `ca` to the generic `vim.lsp.buf.code_action`. +Because both are buffer-local and last-set wins, the Rust-specific action is +**silently overwritten**. Rust users — a primary supported language for this +config — get the generic LSP menu and never see rust-analyzer's extras, with +no error to tip them off. The fix is a one-block reorder: apply the generic +LSP keymaps *first*, then the Rust-specific override. + +## Current state + +`lua/plugins/lspconfig.lua` — the `rustaceanvim` spec's `server.on_attach` +(around lines 175–182). The bug is the ordering: the Rust-specific `ca` +is set first, then `keymaps.lsp()` clobbers it. + +Current code: + +```lua + opts = { + server = { + on_attach = function(_, bufnr) + vim.keymap.set("n", "ca", function() + vim.cmd.RustLsp("codeAction") + end, { desc = "Code Action", buffer = bufnr }) + -- vim.keymap.set( + -- 'n', + -- 'dr', + -- ... + -- ) + + -- lsp keymap + keymaps.lsp({ buffer = bufnr }) + end, + }, +``` + +`keymaps.lsp` is defined in `lua/keymaps.lua`. The clobbering entry is: + +```lua + { { "n", "v" }, "ca", vim.lsp.buf.code_action, opts }, +``` + +So after `keymaps.lsp` runs, normal-mode `ca` is `vim.lsp.buf.code_action` +(overriding the `RustLsp("codeAction")` set above it). + +### How the repo does ordering correctly elsewhere + +The pattern "apply generic keymaps, then let the specific caller override" is +the natural one and matches how `lspconfig.lua`'s own `default_lspconfig` +applies `keymaps.lsp` once in `on_attach` with no later override (so there's +nothing to clobber). For rustaceanvim the override exists, so it must come +*after* the generic call. + +### Repo conventions to match + +- Keep the block structure and the `{ desc = "Code Action", buffer = bufnr }` + option table verbatim. +- Keep the commented-out `dr` debuggables block in place (it's an + intentional disabled-alternative; see plan backlog TECH-6 for comment-cruft + policy — leave it here). + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm order: RustLsp set AFTER keymaps.lsp | `grep -n 'keymaps.lsp\|RustLsp("codeAction")' lua/plugins/lspconfig.lua` | the `keymaps.lsp` line number is LOWER than the `RustLsp("codeAction")` line number | +| File still parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/lspconfig.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | + +A full runtime proof (open a Rust project, attach rust-analyzer, press +`ca`, see the rust-analyzer menu) needs a Rust toolchain + rust-analyzer +installed — heavy and out of scope for verification here. The ordering check + +parse check are sufficient; the change is purely a reorder of two existing, +individually-correct blocks. + +## Scope + +**In scope** (the only file you should modify): +- `lua/plugins/lspconfig.lua` — reorder the `on_attach` body so + `keymaps.lsp({ buffer = bufnr })` runs BEFORE the RustLsp `ca>` set. + +**Out of scope** (do NOT touch): +- `lua/keymaps.lua` — the generic `ca` entry is correct as-is; the + collision is resolved by ordering, not by changing the shared helper. +- The missing `keymaps.lsp_format` call in this same `on_attach` (it only + calls `keymaps.lsp`, not `keymaps.lsp_format`). That is a separate, minor + observation; do not add it here — Rust formatting is handled by conform's + `format_on_save` and the global `fM>`. +- Any other plugin's on_attach. + +## Git workflow + +- Branch: `advisor/006-rust-ca-order` +- Single commit. Message style (conventional commits, per `git log`): + `fix: apply rustaceanvim ca after generic LSP keymaps`. + +## Steps + +### Step 1: Reorder the on_attach body + +In `lua/plugins/lspconfig.lua`, inside the `rustaceanvim` `server.on_attach` +function, move the `-- lsp keymap` / `keymaps.lsp({ buffer = bufnr })` block to +BEFORE the `vim.keymap.set("n", "ca", ...)` RustLsp block. Keep the +commented-out `dr` debuggables block where it is (it's commented, so +position doesn't matter functionally — leave it adjacent to the RustLsp +mapping for context). + +Target shape of the `on_attach` body: + +```lua + on_attach = function(_, bufnr) + -- lsp keymap (generic first, so the Rust-specific ca below wins) + keymaps.lsp({ buffer = bufnr }) + + vim.keymap.set("n", "ca", function() + vim.cmd.RustLsp("codeAction") + end, { desc = "Code Action", buffer = bufnr }) + -- vim.keymap.set( + -- 'n', + -- 'dr', + -- function() + -- vim.cmd.RustLsp('debuggables') + -- end, + -- { desc = "Rust debuggables", buffer = bufnr } + -- ) + end, +``` + +(Use the live file's exact indentation — tabs. Only the *order* of the two +blocks changes; no tokens are added or removed except the new comment line +noted above, which explains *why* the order matters so a future edit doesn't +re-clobber it.) + +**Verify**: +- `grep -n 'keymaps.lsp\|RustLsp("codeAction")' lua/plugins/lspconfig.lua` → + the `keymaps.lsp` match's line number is **less than** the + `RustLsp("codeAction")` match's line number. (Both appear; confirm order.) + +### Step 2: Parse-check the file + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/lspconfig.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`, exit 0. + +### Step 3: Commit + +- Stage `lua/plugins/lspconfig.lua`. +- Commit: `fix: apply rustaceanvim ca after generic LSP keymaps`. + +**Verify**: `git show --stat HEAD` → exactly one file changed +(`lua/plugins/lspconfig.lua`). + +## Test plan + +- **Static (required)**: ordering check + parse check above. +- **Optional runtime proof** (only if a Rust project + rust-analyzer are + handy): open a Rust file in a Cargo project, wait for `LspAttach`, press + `ca`, and confirm rust-analyzer's action menu appears (with entries + like "Run" / "Debug" for `fn main`, or "Rebuild proc-macros") rather than + the generic `vim.lsp.buf.code_action` menu. Confirmation only — not required + to mark the plan done. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'keymaps.lsp\|RustLsp("codeAction")' lua/plugins/lspconfig.lua` + shows the `keymaps.lsp` line number LOWER than the `RustLsp("codeAction")` + line number. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/lspconfig.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/plugins/lspconfig.lua` changed. +- [ ] `plans/README.md` status row for 006 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The `on_attach` body at the cited location does not match the excerpt (drift). + Report what you see. +- `keymaps.lsp` is called more than once in this `on_attach`, or the + `ca` RustLsp mapping is missing — the fix assumes exactly one of + each. Report and stop. +- The reorder would require touching the commented `dr` block's + *content* (it shouldn't — only its surrounding position). If something about + the comment block makes a clean reorder impossible, report instead of + editing the commented code. + +## Maintenance notes + +- **What interacts with this later**: if `keymaps.lsp` ever gains a new + Rust-relevant binding, the "generic first, specific override after" ordering + is what keeps overrides winning. The comment added in Step 1 documents the + invariant; preserve it. +- **Reviewer focus**: confirm only the two blocks swapped position and the new + explanatory comment is the only added line. No keymap lhs/lhs-string or + desc text should have changed. +- **Related deferred item**: this `on_attach` calls `keymaps.lsp` but not + `keymaps.lsp_format`, so Rust buffers don't get a buffer-local `fm`. + Out of scope here (conform `format_on_save` + global `fM>` cover + Rust formatting); noted for awareness. diff --git a/plans/007-ck-collision-tmux-sighelp.md b/plans/007-ck-collision-tmux-sighelp.md new file mode 100644 index 0000000..fd93ce3 --- /dev/null +++ b/plans/007-ck-collision-tmux-sighelp.md @@ -0,0 +1,217 @@ +# Plan 007: Resolve the `` collision between tmux-nav and LSP signature help + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat f5c7d00..HEAD -- lua/keymaps.lua lua/plugins/tmux.lua README.md` +> If any of those files changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 006, 008, 009) +- **Category**: dx +- **Planned at**: commit `f5c7d00`, 2026-06-18 +- **Finding**: N3 + +## Why this matters + +`` is mapped by two plugins in **the same mode**: +- `vim-tmux-navigator` binds `` (normal mode, global) → `TmuxNavigateUp`. +- `keymaps.lsp()` binds `` (normal mode, buffer-local in every LSP + buffer) → `vim.lsp.buf.signature_help`. + +Buffer-local maps beat global maps, so in **every code buffer with an LSP +attached**, `` stops navigating to the tmux pane above and instead shows +signature help. The h/j/k/l nav cluster is meant to work uniformly everywhere; +losing "up" specifically inside code files is surprising. Both bindings are +documented in `README.md`, so the collision is unintentional. + +**Chosen resolution: a mode split, not a key remap.** Signature help is most +useful *while typing a call* (insert mode); tmux navigation is a normal-mode +operation. Moving the signature-help binding from `n` to `i` mode keeps +`` on both functions in their natural modes and preserves both +documented bindings with zero new keys. (The alternative — remap signature +help to a free key like `gK` — is noted in STOP/maintenance for the maintainer +who prefers normal-mode sig help.) + +## Current state + +Three relevant lines: + +`lua/keymaps.lua` line 12 — the colliding LSP binding (normal mode): +```lua + { "n", "", vim.lsp.buf.signature_help, opts }, +``` + +`lua/plugins/tmux.lua` line 14 — the tmux binding (normal mode, global): +```lua + { "", "TmuxNavigateUp" }, +``` + +`README.md` documents both, both as `n` mode: +``` +| `` | n | Signature help | (line 104, under "Code Navigation (LSP)") +... +| `` | n | Navigate up | (line 160, under "Tmux Integration") +``` + +### Verified free + +Confirmed during planning: no existing insert-mode `` binding anywhere in +`lua/`, so moving signature help to insert mode will not itself collide. +`vim.lsp.buf.signature_help` works in insert mode (it is the conventional +trigger point — while entering arguments). + +### Repo conventions to match + +- `keymaps.lua` stores each LSP keymap as a `{ modes, lhs, rhs, opts }` row. + Change only the mode field of the one row. +- README keybinding rows are `| | | |`. Update the + mode cell to match the new mode. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm sig-help now insert mode | `grep -n '' lua/keymaps.lua` | line shows `{ "i", "", vim.lsp.buf.signature_help, opts },` | +| Confirm tmux binding unchanged | `grep -n '' lua/plugins/tmux.lua` | unchanged line 14 | +| README mode cell updated | `grep -n '' README.md` | the "Signature help" row shows `\| i \|`; the "Navigate up" row still `\| n \|` | +| Files parse | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | + +## Scope + +**In scope** (the only files you should modify): +- `lua/keymaps.lua` — change the `` row's mode from `"n"` to `"i"`. +- `README.md` — update the "Signature help" row's mode cell from `n` to `i`. + +**Out of scope** (do NOT touch): +- `lua/plugins/tmux.lua` — the tmux binding is correct; the fix is on the LSP + side. +- Other rows in `keymaps.lua` or other README keybinding rows. (A full + keybindings-vs-code accuracy audit is a separate, deferred effort.) +- Remapping signature help to a brand-new key (e.g. `gK`). The mode split is + the chosen resolution. If the maintainer later prefers normal-mode sig help, + that's a one-line change documented in Maintenance — not this plan. + +## Git workflow + +- Branch: `advisor/007-ck-conflict` +- Single commit. Message style (conventional commits): + `fix: move LSP signature help to insert mode to free for tmux`. + +## Steps + +### Step 1: Change the signature-help mode to insert + +In `lua/keymaps.lua` line 12, change the mode `"n"` to `"i"` for the +signature-help row only: + +Before: +```lua + { "n", "", vim.lsp.buf.signature_help, opts }, +``` + +After: +```lua + { "i", "", vim.lsp.buf.signature_help, opts }, +``` + +Leave every other row in the `keymaps` table untouched. + +**Verify**: `grep -n '' lua/keymaps.lua` → the single match reads +`{ "i", "", vim.lsp.buf.signature_help, opts },`. + +### Step 2: Update the README mode cell + +In `README.md`, the "Code Navigation (LSP)" table has (line ~104): + +``` +| `` | n | Signature help | +``` + +Change the mode cell `n` → `i`: + +``` +| `` | i | Signature help | +``` + +Do NOT touch the "Tmux Integration" row (`| | n | Navigate up |`) — it +stays normal mode. + +**Verify**: +- `grep -n '' README.md` → two matches; the "Signature help" row shows + `| i |`, the "Navigate up" row still shows `| n |`. + +### Step 3: Parse-check and commit + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +- Stage `lua/keymaps.lua` and `README.md`. +- Commit: `fix: move LSP signature help to insert mode to free for tmux`. + +**Verify**: `git show --stat HEAD` → exactly two files changed +(`lua/keymaps.lua`, `README.md`). + +## Test plan + +- **Static (required)**: the grep checks above. +- **Manual runtime check** (cheap, recommended): in nvim with this config and + any LSP attached, in normal mode press `` and confirm tmux navigation + up fires (or no-op if not in tmux — importantly, it should NOT pop signature + help). Then in insert mode inside a function call, press `` and confirm + signature help appears. Both behaviors holding = collision resolved. + (Requires the plugins installed; not a CI gate.) + +## Done criteria + +ALL must hold: + +- [ ] `grep -n '' lua/keymaps.lua` shows `{ "i", "", vim.lsp.buf.signature_help, opts },`. +- [ ] `grep -n '' lua/plugins/tmux.lua` is unchanged (still line 14, + `TmuxNavigateUp`). +- [ ] `grep -n '' README.md` shows the "Signature help" row with `| i |` + and the "Navigate up" row still `| n |`. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/keymaps.lua` and `README.md`. +- [ ] `plans/README.md` status row for 007 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The `keymaps.lua` row at line 12 does not match the excerpt (drift). Report. +- There is now, or turns out to be, an existing insert-mode `` binding + elsewhere in `lua/` (re-verify with + `grep -rnE '"i".*|.*"i"' lua/`). If one exists, the mode split + would itself collide — STOP and report; do not proceed to a different + resolution without operator input. +- The README "Signature help" row is not at the expected text (e.g. it was + reworded). Report the actual line; do not guess which cell to edit. + +## Maintenance notes + +- **What interacts with this later**: any future mapping of `` in insert + mode (by another plugin or a personal addition) would re-introduce a + collision. blink.cmp's keymap table (`lua/plugins/blink.lua`) does NOT use + ``, but check there if completion keymaps change. +- **Reviewer focus**: confirm the tmux binding was not touched and only one + mode character + one README cell changed. +- **Alternative resolution (not taken)**: if the maintainer prefers + normal-mode signature help, revert this and instead remap signature help to + a free key. `gK` was verified free during planning and is a recognizable + nvim convention. That change would be: keep `{ "n", "", ... }` → change + lhs to `"gK"`, and update the README lhs cell accordingly. One-line either + way; the mode split was chosen because it preserves the documented `` + key for both features. diff --git a/plans/008-99-md-files-agents.md b/plans/008-99-md-files-agents.md new file mode 100644 index 0000000..7c04cab --- /dev/null +++ b/plans/008-99-md-files-agents.md @@ -0,0 +1,188 @@ +# Plan 008: Make 99's `md_files` find the repo's actual `AGENTS.md` + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat f5c7d00..HEAD -- lua/plugins/ai.lua` +> If `lua/plugins/ai.lua` changed since this plan was written, compare the +> "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 006, 007, 009) +- **Category**: bug +- **Planned at**: commit `f5c7d00`, 2026-06-18 +- **Finding**: N4 + +## Why this matters + +The `99` plugin (ThePrimeagen/99) is configured with `md_files = { "AGENT.md" }` +— the list of filenames it walks up the directory tree to auto-attach as +context for AI refactors. But this repository's guidelines file is named +**`AGENTS.md`** (plural), and there is **no `AGENT.md`**. So 99's auto-context +discovery finds nothing in this very repo: the project's own agent guidelines +are invisible to `99` requests, defeating the feature. The fix adds +`"AGENTS.md"` so 99 finds the repo's actual file, while keeping `"AGENT.md"` +for other projects that follow 99's documented default. + +## Current state + +`lua/plugins/ai.lua` — inside the `99` setup call, the `md_files` table +(around lines 370–372): + +```lua + md_files = { + "AGENT.md", + }, +``` + +The in-file comment above it (lines 364–369) documents the walk-up behavior +and uses `AGENT.md` as the example, matching 99's own example convention. + +### Ground truth from the repo + +- `AGENTS.md` exists at the repo root (this is the file the maintainers and + other agents use — pi, Claude Code, etc.). +- `AGENT.md` does **not** exist (`ls AGENT.md` → "No such file or directory", + confirmed during planning). +- So `md_files = { "AGENT.md" }` matches zero files in this repo. + +### How 99 uses `md_files` + +Per the in-file comment: from the originating buffer's path, 99 walks upward +looking for any file whose name is in `md_files`, stopping at the project +root. Listing multiple names means it will match whichever exists. + +### Repo conventions to match + +- Keep the table literal style (`{ "…", "…" },`). +- The repo standardizes on `AGENTS.md` (plural) — list it first so the local + convention takes precedence. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Confirm AGENTS.md added | `grep -n 'AGENTS.md\|AGENT.md' lua/plugins/ai.lua` | both `"AGENTS.md"` and `"AGENT.md"` present; `"AGENTS.md"` listed first | +| File parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/ai.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | + +A full runtime proof (make a `99` request and confirm `AGENTS.md` content is +injected) requires the `99` plugin + an AI backend configured — out of scope +for verification here. The static check plus the documented walk-up semantics +are sufficient. + +## Scope + +**In scope** (the only file you should modify): +- `lua/plugins/ai.lua` — add `"AGENTS.md"` as the first entry in `md_files`. + +**Out of scope** (do NOT touch): +- The in-file comment that uses `AGENT.md` as its example. It is illustrative + of 99's walk-up behavior, not a statement about this repo; leaving it avoids + churn. (If you want it updated, that's a separate docs edit.) +- The repo's `AGENTS.md` filename — do not rename it to `AGENT.md`. The + plural form is the cross-tool standard (pi/Claude Code/etc.) and was + intentionally chosen. +- Other `99` config fields (`custom_rules`, `completion`, etc.). + +## Git workflow + +- Branch: `advisor/008-99-md-files` +- Single commit. Message style (conventional commits): + `fix: include AGENTS.md in 99 md_files for repo context`. + +## Steps + +### Step 1: Add `"AGENTS.md"` to md_files + +In `lua/plugins/ai.lua`, change the `md_files` table to list `"AGENTS.md"` +first, then `"AGENT.md"`: + +Before: +```lua + md_files = { + "AGENT.md", + }, +``` + +After: +```lua + md_files = { + "AGENTS.md", + "AGENT.md", + }, +``` + +(Indentation is tabs — match the surrounding lines exactly.) + +**Verify**: +- `grep -n 'AGENTS.md\|AGENT.md' lua/plugins/ai.lua` → at least two matches + in the `md_files` block: `"AGENTS.md",` appearing before `"AGENT.md",`. + +### Step 2: Parse-check and commit + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/ai.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +- Stage `lua/plugins/ai.lua`. +- Commit: `fix: include AGENTS.md in 99 md_files for repo context`. + +**Verify**: `git show --stat HEAD` → exactly one file changed +(`lua/plugins/ai.lua`); the diff shows one line added (`"AGENTS.md",`). + +## Test plan + +- **Static (required)**: the grep + parse checks above. +- **Optional runtime proof** (only if `99` + an AI backend are configured): + from a buffer in this repo, trigger a `99` request (e.g. `9f` on a + function) and inspect 99's debug log (`/tmp/.99.debug`, per the + config's logger path) to confirm `AGENTS.md` content appears in the request + context. Confirmation only — not required to mark the plan done. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n '"AGENTS.md"' lua/plugins/ai.lua` returns a match inside the + `md_files` table, listed before `"AGENT.md"`. +- [ ] `grep -n '"AGENT.md"' lua/plugins/ai.lua` still returns a match (the + fallback entry is preserved). +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/ai.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/plugins/ai.lua` changed, with a + one-line addition. +- [ ] `plans/README.md` status row for 008 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The `md_files` block does not match the excerpt (drift). Report what you see. +- The repo has been changed to also contain an `AGENT.md` (re-check with + `ls AGENT.md AGENTS.md`). If both exist, the plan still works (99 finds + whichever), but report it so the maintainer can decide on a single source of + truth. +- The `99` setup signature has changed such that `md_files` is no longer a + simple list of filenames (e.g. it now expects `{ name = ..., path = ... }` + entries). Report; do not guess the new shape. + +## Maintenance notes + +- **What interacts with this later**: if the repo ever standardizes on a + different guidelines filename, update `md_files` in lockstep. The two-name + list is deliberately lenient so it works across this repo (AGENTS.md) and + any project following 99's AGENT.md example. +- **Reviewer focus**: confirm exactly one line was added and order is + `AGENTS.md` then `AGENT.md`. +- **Related deferred item**: BUG-4 (the same `99` config sets + `source = "cmp"` but the repo uses blink.cmp, so 99's completion is + non-functional). That is a separate spike, not touched here. diff --git a/plans/009-keymap-desc-99-toggleterm.md b/plans/009-keymap-desc-99-toggleterm.md new file mode 100644 index 0000000..019e600 --- /dev/null +++ b/plans/009-keymap-desc-99-toggleterm.md @@ -0,0 +1,245 @@ +# Plan 009: Add missing `desc` to the 99 and toggleterm keymaps + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat f5c7d00..HEAD -- lua/plugins/ai.lua lua/plugins/terminal.lua` +> If either file changed since this plan was written, compare the "Current +> state" excerpts against the live code before proceeding; on a mismatch, +> treat it as a STOP condition. +> +> **Note on plan 008**: plan 008 also edits `lua/plugins/ai.lua` (the +> `md_files` table, around lines 370–372). This plan edits DIFFERENT lines of +> the same file (the `vim.keymap.set` calls at the bottom, lines ~376–399). +> If 008 has already been applied, the `md_files` change is well above the +> edits here and will not conflict. If both are applied to the same branch, +> expect a clean non-overlapping merge. Verify the line numbers by text match, +> not absolute position. + +## Status + +- **Priority**: P3 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none (independent of 006, 007, 008; edits non-overlapping + regions of `ai.lua` vs plan 008) +- **Category**: dx / tech-debt +- **Planned at**: commit `f5c7d00`, 2026-06-18 +- **Finding**: TECH-8 (partial — vetted subset) + +## Why this matters + +`AGENTS.md` sets a repo convention: every `vim.keymap.set` call should include +a `desc` field for which-key compatibility. Five keymaps currently omit it: +four `99` keymaps (`9f`, `9v`, `9s`, `9fd`) +and the toggleterm float keymap (`tf`). Without `desc`, which-key +shows a blank or auto-generated label for these, and the keybinding inventory +is incomplete. This plan adds concise, accurate descriptions to each. + +**Scope correction from the original finding**: an earlier line-based scan +also flagged the rust `ca` (`lspconfig.lua:177`) and `fM` +(`lspconfig.lua:260`). Verified during planning — **both already have `desc`** +("Code Action" and "Format file or range (in visual mode)"). Those are +false positives and are NOT edited here. Only the five genuinely-missing ones +are in scope. + +## Current state + +`lua/plugins/ai.lua` — four `99` keymap calls (around lines 376–399), each +missing the 4th `opts` argument: + +```lua + vim.keymap.set("n", "9f", function() + _99.fill_in_function() + end) + -- ... (comments) + vim.keymap.set("v", "9v", function() + _99.visual() + end) + + --- if you have a request you dont want to make any changes, just cancel it + vim.keymap.set("v", "9s", function() + _99.stop_all_requests() + end) + + --- Example: Using rules + actions for custom behaviors + --- ... (comment) + vim.keymap.set("n", "9fd", function() + _99.fill_in_function() + end) +``` + +`lua/plugins/terminal.lua` line 7: + +```lua + vim.keymap.set('n', 'tf', 'ToggleTerm direction=float') +``` + +### Repo conventions to match + +`AGENTS.md` → Keybindings: +> Include `desc` field for which-key compatibility + +Exemplars that already follow the convention (do not touch, just match style): +- `lua/plugins/img-clip.lua`: + `{ "p", "PasteImage", desc = "Paste image from system clipboard" }` +- `lua/plugins/lspconfig.lua:177-180`: + `vim.keymap.set("n", "ca", function() ... end, { desc = "Code Action", buffer = bufnr })` + +Description wording should match the README keybindings table where one exists: +- README lists `tf` | n | "Toggle terminal (float)" — use that text. +- The `99` keys are not in the README keybindings table; use the in-file + comment intent (see Step 1 for exact text). + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| No desc-less keymaps remain in scope | see Done criteria greps | all return no matches | +| Files parse | `nvim --headless -u NONE +'lua for _,f in ipairs({"lua/plugins/ai.lua","lua/plugins/terminal.lua"}) do local fn,err=loadfile(f); assert(fn,err) end; print("PARSE_OK")' +qa` | prints `PARSE_OK` | + +## Scope + +**In scope** (the only files you should modify): +- `lua/plugins/ai.lua` — add `{ desc = "…" }` opts to the four `99` + `vim.keymap.set` calls. +- `lua/plugins/terminal.lua` — add `{ desc = "…" }` to the `tf` call. + +**Out of scope** (do NOT touch): +- `lua/plugins/lspconfig.lua` — the rust `ca` and `fM` calls + already have `desc`; they are not missing it. Do not edit them. +- Any keymap inside a lazy `keys = { ... }` spec table (those use the + `desc = "…"` field directly, e.g. flash/snacks; they're fine). +- The `99` functional behavior, lhs, or mode — only the missing opts table is + added. + +## Git workflow + +- Branch: `advisor/009-keymap-desc` +- Single commit. Message style (conventional commits): + `style: add desc to 99 and toggleterm keymaps`. + +## Steps + +### Step 1: Add desc to the four 99 keymaps in ai.lua + +For each of the four `99` `vim.keymap.set` calls, add a 4th argument +`{ desc = "…" }`. Use these exact descriptions (derived from the in-file +comments and the `_99` method names): + +1. `9f` (n, `_99.fill_in_function`) → + `{ desc = "99: Fill in function" }` +2. `9v` (v, `_99.visual`) → + `{ desc = "99: Visual selection" }` +3. `9s` (v, `_99.stop_all_requests`) → + `{ desc = "99: Stop all requests" }` +4. `9fd` (n, `_99.fill_in_function`) → + `{ desc = "99: Fill in function (debug rule)" }` + +Example of the target shape for the first one (apply the same pattern to all +four; keep the existing function bodies and modes verbatim): + +```lua + vim.keymap.set("n", "9f", function() + _99.fill_in_function() + end, { desc = "99: Fill in function" }) +``` + +**Verify**: +- `grep -n '9f"' lua/plugins/ai.lua` → the call now ends (after the + `end`) with `, { desc = "99: Fill in function" })`. (Check the closing paren + follows the opts table.) +- Each of the four lhs strings (`9f`, `9v`, `9s`, + `9fd`) appears in a `vim.keymap.set` whose call has a `{ desc = ... }` + arg. + +### Step 2: Add desc to the toggleterm keymap in terminal.lua + +In `lua/plugins/terminal.lua` line 7, add the opts table. Use the README's +wording ("Toggle terminal (float)"): + +Before: +```lua + vim.keymap.set('n', 'tf', 'ToggleTerm direction=float') +``` + +After: +```lua + vim.keymap.set('n', 'tf', 'ToggleTerm direction=float', { desc = 'Toggle terminal (float)' }) +``` + +(Match the file's existing single-quote style for strings.) + +**Verify**: `grep -n "tf" lua/plugins/terminal.lua` → the line ends +with `, { desc = 'Toggle terminal (float)' })`. + +### Step 3: Parse-check and commit + +**Verify**: +`nvim --headless -u NONE +'lua for _,f in ipairs({"lua/plugins/ai.lua","lua/plugins/terminal.lua"}) do local fn,err=loadfile(f); assert(fn,err) end; print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +- Stage `lua/plugins/ai.lua` and `lua/plugins/terminal.lua`. +- Commit: `style: add desc to 99 and toggleterm keymaps`. + +**Verify**: `git show --stat HEAD` → exactly two files changed +(`lua/plugins/ai.lua`, `lua/plugins/terminal.lua`). + +## Test plan + +- **Static (required)**: the Done criteria greps confirm every in-scope call + now has a `desc`, and both files parse. +- **Optional which-key check** (requires plugins installed): in nvim, press + `` and then `9` / `t` and confirm which-key now shows the labels + instead of blanks. Confirmation only. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n '9f"' lua/plugins/ai.lua`, `9v"`, `9s"`, + `9fd"` each show a `vim.keymap.set` whose call includes a + `{ desc = ... }` argument. +- [ ] `grep -n "tf" lua/plugins/terminal.lua` shows the call includes + `{ desc = 'Toggle terminal (float)' }`. +- [ ] `nvim --headless -u NONE +'lua for _,f in ipairs({"lua/plugins/ai.lua","lua/plugins/terminal.lua"}) do local fn,err=loadfile(f); assert(fn,err) end' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/plugins/ai.lua` and + `lua/plugins/terminal.lua` changed. +- [ ] `lua/plugins/lspconfig.lua` is NOT modified (the rust `ca` / + `fM` false positives stay untouched). +- [ ] `plans/README.md` status row for 009 updated (TODO → DONE). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- Any of the four `99` keymap calls or the `terminal.lua` call does not match + its excerpt (drift). Report what you see. +- One of the in-scope keymaps already has a `desc` (re-check with + `grep -A3 'vim.keymap.set' lua/plugins/ai.lua`). If so, skip that one and + note it; do not overwrite an existing desc. +- Plan 008 has been applied to the same working tree and the `md_files` edit + is closer to these lines than expected, risking a fuzzy match. Resolve by + text-match on the `vim.keymap.set` lines only; if a match is ambiguous, + STOP and report rather than editing the wrong region. +- A `99` method name has changed (`fill_in_function`, `visual`, + `stop_all_requests`) such that the descriptions no longer describe the + behavior. Report; do not invent behavior. + +## Maintenance notes + +- **What interacts with this later**: any new `99` or toggleterm keymap should + include `desc` from the start (AGENTS.md convention). The smoke test from + plan 001 does NOT enforce `desc` — a future lint rule (luacheck or a custom + grep in CI) could, but that's deferred. +- **Reviewer focus**: confirm exactly five `vim.keymap.set` calls gained an + opts table and nothing else (no lhs/mode/function-body changes), and that + `lspconfig.lua` was not touched. +- **Vetting note for the record**: the original TECH-8 finding also named + `lspconfig.lua:177` and `:260`; both were verified to already have `desc` + and are correctly excluded from this plan. diff --git a/plans/010-lsp-toggle-ui.md b/plans/010-lsp-toggle-ui.md new file mode 100644 index 0000000..9986dd0 --- /dev/null +++ b/plans/010-lsp-toggle-ui.md @@ -0,0 +1,525 @@ +# Plan 010: LSP server toggle UI with persisted config + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat d9a6fcc..HEAD -- lua/plugins/lspconfig.lua lua/configs/init.lua` +> If `lua/plugins/lspconfig.lua` changed since this plan was written, compare +> the "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P3 +- **Effort**: M +- **Risk**: MED (touches the core LSP enable path; additive, but central) +- **Depends on**: none (independent of 001–009) +- **Category**: direction (feature) +- **Planned at**: commit `d9a6fcc`, 2026-06-18 +- **Finding**: user-requested feature (not from the audit) + +## Why this matters + +Today, which LSP servers run is hardcoded: `lspconfig.lua` enables everything +in `mason_options.ensure_installed` unconditionally on every load. There is no +way to turn a server off without editing source. This plan adds a runtime +toggle (`:LspToggle`) backed by a small persisted file, so you can disable a +noisy or slow server (e.g. turn off `gopls` while not on a Go project) without +touching config — and the choice survives restart. + +### Five design decisions (locked; overriding requires editing this plan) + +1. **Persistence = a JSON "disabled set" at `~/.local/share/nvim/basedddvim-lsp-toggles.json`.** + Format is a plain array of *disabled* server names, e.g. `["gopls","ts_ls"]`. + **Why a blacklist (disabled-set), not a whitelist (enabled-set):** a missing + or empty file means "everything enabled" = today's behavior = **zero + migration**. A whitelist would silently turn OFF any new server you later add + to `ensure_installed` until you toggle it on — surprising. Blacklist is the + safe default. Machine-local (under `stdpath("data")`) on purpose: toggles are + a per-machine preference, not something to commit. +2. **Scope = global**, all projects. Per-project scoping (via `root_dir`) is far + more code and not needed for v1. +3. **Effect timing = persist now, takes full effect on next restart.** Disabling + also calls `vim.lsp.stop_client` for immediate "off" feedback in the current + session. **Re-enabling requires restart** — there is no `vim.lsp.disable()` + in Neovim 0.12 (verified: `tostring(vim.lsp.disable) == "false"`), and the + enable autocommands only register at load. This is a documented v1 limit, + shown in the UI message. +4. **Coverage = the 4 loop-managed servers only:** `lua_ls`, `ts_ls`, `gopls`, + `ruby_lsp`. `rust_analyzer` is co-managed by **rustaceanvim** (via + `vim.g.rustaceanvim`) and **jdtls** is managed by **nvim-jdtls** + (`start_or_attach` in a FileType autocmd); both bypass the simple + enable-loop toggle and are **excluded from v1**. The UI lists them as + "managed by plugin — not toggleable". +5. **`MasonInstallAll` stays orthogonal to toggles.** This is intentional, not + a bug: `MasonInstallAll` installs *binaries on disk*; the toggle controls + *whether a server runs*. Three independent layers: + | Layer | Concern | Controlled by | + |---|---|---| + | Installed | server binary on disk | `MasonInstallAll` / Mason | + | Configured | Neovim knows its cmd/ft/settings | `vim.lsp.config` (always runs) | + | Enabled | autocmd to auto-start on filetypes | `vim.lsp.enable` (← toggle gates this) | + `MasonInstallAll` reads the **static** `mason_options.ensure_installed` + table, NOT the toggle file, so toggling `gopls` off does NOT remove its + binary — it just stops `vim.lsp.enable("gopls")`. This is a **feature**: + re-enabling later is instant (no Mason re-download). Do NOT make + `MasonInstallAll` filter by toggles — that would break the instant-re-enable + property (see STOP conditions). + +## Current state + +### The enable loop (`lua/plugins/lspconfig.lua`) + +`mason_options.ensure_installed` (lines 20–31) lists the servers; the enable +loop (lines 79–132) iterates them. The relevant tail of the loop: + +```lua + for _, lsp in ipairs(mason_options.ensure_installed) do + if lsp == "ts_ls" then + -- ... vim.lsp.config(ts_ls, ...) with Vue plugin ... + elseif lsp == "pyright" then + -- ... + elseif lsp ~= "rust_analyzer" then + vim.lsp.config(lsp, default_lspconfig(capabilities)) + end + + -- enable LSP + vim.lsp.enable(lsp) -- ← line 131: this is what the toggle gates + end +``` + +Note: `vim.lsp.enable(lsp)` currently runs for **every** server including +`rust_analyzer` (line 131 is outside the `elseif` chain). The toggle gate must +preserve rust_analyzer's current "always enabled" behavior — see the +`is_disabled` defensive design in Step 2. + +### The MasonInstallAll command (`lua/plugins/lspconfig.lua` lines 150–163) + +```lua + vim.api.nvim_create_user_command("MasonInstallAll", function() + local mason_servers = {} + for _, mason_server in ipairs(mason_options.ensure_installed) do + table.insert(mason_servers, mason_lsp_mapping[mason_server]) + end + vim.cmd("MasonInstall " .. ... ) + end, {}) +``` + +This reads the hardcoded list and must **stay exactly like this** (decision 5). + +### Feature-flag precedent (`lua/configs/init.lua:1-2`) + +```lua +local M = { + ai = { enabled = true }, + ... +``` + +That's a *load-time* flag. This feature needs *runtime + persisted* state, which +the repo does not do anywhere yet (verified: no `writefile`/`vim.json`/`stdpath("data")` +usage exists in `lua/`). So this plan introduces the repo's first persistence +pattern — keep it isolated in one new module. + +### Repo conventions to match + +- New module with functions → top-level `lua/` (like `lua/keymaps.lua`), not + `lua/configs/` (which holds constants only). So: `lua/lsp-toggles.lua`. +- Module pattern: `local M = {} … return M` (see `lua/keymaps.lua`). +- User commands are created inside a plugin's `config` function (see + `MasonInstallAll`). `:LspToggle` goes in the lspconfig `config` function. +- `pcall` around anything that can fail (file IO, JSON decode) — see + `lua/keymaps.lua:29` for the `pcall(require, ...)` exemplar. +- Tabs for indentation (repo standard; no `.stylua.toml`). + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Module loads in plain nvim | `nvim --headless -u NONE +'lua package.path="./lua/?.lua;"..package.path; assert(require("lsp-toggles")); print("OK")' +qa` | prints `OK` | +| JSON round-trip works | see Step 1 verify block | writes `false false true false ROUNDTRIP_OK` to a file | +| Enable-loop file parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/lspconfig.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | + +The JSON round-trip test (Step 1) runs in `nvim -u NONE` because it uses only +core APIs (`vim.fn.readfile`, `vim.json`, `vim.fn.writefile`, `vim.fn.stdpath`) +— **no plugins required**. That is the cheap, deterministic core of this plan's +verification. **Important:** `-u NONE` does NOT auto-add `./lua` to the +runtimepath, so every `-u NONE` test that `require`s the module must prepend +`package.path = "./lua/?.lua;" .. package.path` first (shown above and below). +This was confirmed during planning — without the prepend, `require("lsp-toggles")` +fails with `module not found`. + +## Scope + +**In scope** (the only files you should create or modify): +- `lua/lsp-toggles.lua` (new) — persistence + toggle logic + server list. +- `lua/plugins/lspconfig.lua` — gate `vim.lsp.enable` for toggleable servers; + add the `:LspToggle` user command. + +**Out of scope** (do NOT touch): +- `lua/plugins/java.lua` (jdtls) and the rustaceanvim spec — they are + non-toggleable by design (decision 4). Do NOT add toggle hooks to them. +- The `MasonInstallAll` command — must stay orthogonal (decision 5). +- `mason_options.ensure_installed` / `mason_lsp_mapping` — unchanged. +- A snacks-specific multi-toggle picker UI is an **optional** Step 5; the + robust `vim.ui.select` v1 (Step 4) is the committed deliverable. + +## Git workflow + +- Branch: `advisor/010-lsp-toggle` +- Commit per logical step is fine (module, loop wiring, command/UI). Message + style (conventional commits): `feat: add :LspToggle with persisted config`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Create `lua/lsp-toggles.lua` + +Create `lua/lsp-toggles.lua` with this content (tabs for indentation). It is +pure core-API code (testable without plugins): + +```lua +-- Persisted LSP server toggle state. +-- +-- Stores a "disabled set" (blacklist) as JSON at stdpath("data"). A missing +-- or empty file = everything enabled = the repo's default behavior (zero +-- migration). Only the servers in `toggleable` can ever be gated; rust_analyzer +-- and jdtls are managed by their own plugins and are never disabled here. +local M = {} + +local PATH = vim.fn.stdpath("data") .. "/basedddvim-lsp-toggles.json" + +-- Servers this feature can toggle. Must match the loop-managed set in +-- lspconfig.lua (i.e. mason_options.ensure_installed MINUS rust_analyzer, +-- which is co-managed by rustaceanvim, and jdtls, which is managed by nvim-jdtls). +M.toggleable = { "lua_ls", "ts_ls", "gopls", "ruby_lsp" } + +-- Read the disabled set from disk. Missing/corrupt file = {} (all enabled). +-- Returns a lookup table { server_name = true }. +function M.get_disabled() + local disabled = {} + local ok, lines = pcall(vim.fn.readfile, PATH) + if not ok or not lines or #lines == 0 then + return disabled + end + local ok2, data = pcall(vim.json.decode, table.concat(lines, "\n")) + if not ok2 or type(data) ~= "table" then + return disabled + end + for _, name in ipairs(data) do + if type(name) == "string" then + disabled[name] = true + end + end + return disabled +end + +-- True only if `name` is toggleable AND currently disabled. Non-toggleable +-- servers (rust_analyzer, jdtls) always return false, so the enable loop +-- enables them unconditionally — preserving current behavior. +function M.is_disabled(name) + if not vim.tbl_contains(M.toggleable, name) then + return false + end + return M.get_disabled()[name] == true +end + +-- Persist the given disabled list (array of names). Creates the data dir if needed. +function M._write(disabled_list) + vim.fn.mkdir(vim.fs.dirname(PATH), "p") + local json = vim.json.encode(disabled_list) + vim.fn.writefile({ json }, PATH) +end + +-- Flip `name`'s state, persist, return the new "is disabled?" boolean. +function M.toggle(name) + if not vim.tbl_contains(M.toggleable, name) then + vim.notify("lsp-toggles: '" .. name .. "' is not toggleable", vim.log.levels.WARN) + return false + end + local disabled = M.get_disabled() + local list = {} + for srv, _ in pairs(disabled) do + list[#list + 1] = srv + end + if disabled[name] then + -- enable: omit from list + M._write(vim.tbl_filter(function(s) + return s ~= name + end, list)) + return false + else + -- disable: add to list + list[#list + 1] = name + M._write(list) + return true + end +end + +return M +``` + +**Verify** (run from the repo root). `-u NONE` does NOT add `./lua` to the +runtimepath, so the prepend below is required (confirmed during planning): + +```bash +nvim --headless -u NONE +'lua package.path="./lua/?.lua;"..package.path; local T=require("lsp-toggles"); local f=io.open(os.getenv("HOME").."/.lsp-spike-out","w"); local function b(x) return x and "true" or "false" end; f:write(b(T.is_disabled("rust_analyzer")).." "..b(T.is_disabled("gopls")).." "); T.toggle("gopls"); f:write(b(T.is_disabled("gopls")).." "); T.toggle("gopls"); f:write(b(T.is_disabled("gopls")).." ROUNDTRIP_OK\n"); f:close()' +qa +cat ~/.lsp-spike-out && rm ~/.lsp-spike-out +``` + +Expected output (writes to a file because headless `print` capture is +unreliable — also confirmed during planning; file IO is the ground truth): +``` +false false true false ROUNDTRIP_OK +``` + +Read each value as a contract assertion: +- 1st `false` = `is_disabled("rust_analyzer")` → **the critical guard.** + rust_analyzer is non-toggleable, so `is_disabled` must return `false` and the + enable loop enables it unconditionally (preserving current behavior). If you + see `true` here, STOP — the `tbl_contains` guard is broken. +- 2nd `false` = `is_disabled("gopls")` before any toggle (default enabled). +- 3rd `true` = after `toggle("gopls")` → now disabled, persisted to disk. +- 4th `false` = after a second `toggle("gopls")` → re-enabled, removed from + the disabled set. + +The persisted file at `~/.local/share/nvim/basedddvim-lsp-toggles.json` should +end as `[]` (empty array) after the round-trip, or be absent — either is +correct (`get_disabled` decodes both to "all enabled"). + +### Step 2: Gate the enable loop + +In `lua/plugins/lspconfig.lua`, require the module near the top (next to +`local keymaps = require("keymaps")`): + +```lua +local lsp_toggles = require("lsp-toggles") +``` + +Then change the enable call at the bottom of the loop (line ~131). Replace: + +```lua + -- enable LSP + vim.lsp.enable(lsp) +``` + +with: + +```lua + -- enable LSP (toggleable servers can be disabled via :LspToggle; + -- rust_analyzer is non-toggleable so is_disabled returns false for it) + if not lsp_toggles.is_disabled(lsp) then + vim.lsp.enable(lsp) + end +``` + +Leave `vim.lsp.config(...)` calls above UNCHANGED for every server — config is +cheap and means re-enabling a toggled-off server later needs no re-configuration. + +**Verify**: +- `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/plugins/lspconfig.lua"); assert(fn,err); print("PARSE_OK")' +qa` + → prints `PARSE_OK`. +- `grep -n 'lsp_toggles.is_disabled\|vim.lsp.enable' lua/plugins/lspconfig.lua` + → `is_disabled` appears once, guarding `vim.lsp.enable`. + +### Step 3: Add the `:LspToggle` user command + immediate `stop_client` + +Still in the `lspconfig` `config = function()`, after the enable loop (and after +the mason dependency block, anywhere at the top level of the config function), +register the command: + +```lua + -- :LspToggle [server] — toggle a server on/off (persisted). + -- No arg opens a picker. Disabling stops its clients now; enabling + -- takes full effect on next restart (no vim.lsp.disable in 0.12). + vim.api.nvim_create_user_command("LspToggle", function(opts) + local name = opts.args + if name == "" then + require("lsp-toggles").pick() + return + end + local now_disabled = require("lsp-toggles").toggle(name) + if now_disabled then + vim.lsp.stop_client(vim.lsp.get_clients({ name = name })) + vim.notify("LSP '" .. name .. "' disabled (restart to fully stop; re-enable via :LspToggle)", vim.log.levels.INFO) + else + vim.notify("LSP '" .. name .. "' enabled — restart Neovim to attach", vim.log.levels.INFO) + end + end, { + nargs = "?", + complete = function() + return require("lsp-toggles").toggleable + end, + desc = "Toggle an LSP server on/off (persisted)", + }) +``` + +**Verify**: +- File parses: same `loadfile` check → `PARSE_OK`. +- `grep -n 'create_user_command("LspToggle"' lua/plugins/lspconfig.lua` → one match. + +### Step 4: Add the picker (robust v1 using `vim.ui.select`) + +Add a `pick` function to `lua/lsp-toggles.lua` (snacks overrides `vim.ui.select` +to render a nice picker, so this is both robust AND pretty with zero snacks +API risk): + +```lua +-- Open a picker to toggle one server. Uses vim.ui.select so it works with any +-- provider (snacks.nvim overrides it for a nicer UI — see lua/plugins/snacks.lua). +function M.pick() + local disabled = M.get_disabled() + local items = {} + for _, name in ipairs(M.toggleable) do + items[#items + 1] = { + name = name, + label = string.format("[%s] %s", disabled[name] and "x" or " ", name), + } + end + -- Also surface the non-toggleable ones, read-only, for discoverability. + local note = { label = "— rust_analyzer / jdtls: managed by plugin (not toggleable)", name = "" } + table.insert(items, note) + + vim.ui.select(items, { + prompt = "Toggle LSP server (select to flip; re-invoke for another):", + format_item = function(item) + return item.label + end, + }, function(choice) + if not choice or choice.name == "" then + return + end + local now_disabled = M.toggle(choice.name) + if now_disabled then + vim.lsp.stop_client(vim.lsp.get_clients({ name = choice.name })) + end + vim.notify( + "LSP '" .. choice.name .. "' " .. (now_disabled and "disabled" or "enabled") + .. " — restart for full effect", + vim.log.levels.INFO + ) + end) +end +``` + +**Verify**: +- `nvim --headless -u NONE +'lua package.path="./lua/?.lua;"..package.path; local T=require("lsp-toggles"); assert(type(T.pick)=="function"); print("OK")' +qa` + (from repo root) → prints `OK`. +- File parses. + +### Step 5 (OPTIONAL enhancement — skip if anything is unclear): snacks.picker multi-toggle + +The v1 above is single-toggle-per-invocation. If you want a picker that stays +open and flips multiple servers before confirming, this step investigates +`Snacks.picker`. **This is a spike with a fallback** — if the API is awkward, +ship Step 4 as the deliverable and record the finding. + +Spike approach: `Snacks.picker.pick({ items = ..., format = ..., actions = { [""] = function(picker, item) ... flip and refresh ... end } })`. +The risk: the exact `actions`/refresh API differs across snacks versions. Read +`~/.local/share/nvim/lazy/snacks.nvim/lua/snacks/picker/` on a machine where +the config runs before deciding. + +**Escape hatch**: if after 30 minutes the snacks multi-toggle isn't clean, STOP +enhancing, keep Step 4, and note in `plans/README.md` that the multi-toggle is +deferred. Step 4 fully satisfies the feature request. + +### Step 6: Commit + +- Stage `lua/lsp-toggles.lua` and `lua/plugins/lspconfig.lua`. +- Commit: `feat: add :LspToggle with persisted config`. + +**Verify**: `git show --stat HEAD` → only those two files (plus optionally a +3rd if you did Step 5 in a separate file — you didn't; Step 5 edits +`lsp-toggles.lua`). + +## Test plan + +- **Module JSON round-trip (required, plugin-free)**: Step 1 verify block. +- **Enable-loop gating logic (required)**: temporarily add a fake disabled + entry by toggling a server in a headless nvim, then confirm the loop would + skip it. Lightweight version: `nvim --headless -u NONE +'lua package.path="./lua/?.lua;"..package.path; require("lsp-toggles").toggle("gopls"); print(require("lsp-toggles").is_disabled("gopls"))' +qa` + → `true`; then clean up with another toggle. (This proves persistence; the + loop wiring is verified by parse + grep.) +- **Manual runtime check (recommended, requires plugins)**: with the full + config loaded, run `:LspToggle gopls`, confirm the notify message and that + any running gopls client stops (`:LspInfo` or `:lua print(#vim.lsp.get_clients({name="gopls"}))`). + Restart, open a Go file, confirm gopls does NOT attach. `:LspToggle gopls` + again, restart, confirm it DOES attach. +- **MasonInstallAll orthogonality (recommended)**: after toggling gopls off, + run `:MasonInstallAll` — it should still install/update gopls (no error), and + gopls should STILL not auto-start (toggle won). Confirms decision 5. + +## Done criteria + +ALL must hold: + +- [ ] `lua/lsp-toggles.lua` exists; from repo root + `nvim --headless -u NONE +'lua package.path="./lua/?.lua;"..package.path; assert(require("lsp-toggles")); print("OK")' +qa` + prints `OK`. +- [ ] The Step 1 round-trip verify prints `false false true false ROUNDTRIP_OK` + (rust_analyzer=false is the key assertion: non-toggleable servers are never + disabled). +- [ ] `lua/plugins/lspconfig.lua` parses (`loadfile` → exits 0) and + `vim.lsp.enable` is guarded by `lsp_toggles.is_disabled`. +- [ ] `:LspToggle` command is registered (`grep` shows one `create_user_command("LspToggle"`). +- [ ] `MasonInstallAll` command body is UNCHANGED (decision 5): + `git diff d9a6fcc..HEAD -- lua/plugins/lspconfig.lua` shows the MasonInstallAll + loop (`for _, mason_server in ipairs(mason_options.ensure_installed)`) untouched. +- [ ] `git show --stat HEAD` shows only `lua/lsp-toggles.lua` and + `lua/plugins/lspconfig.lua`. +- [ ] `lua/plugins/java.lua` and the rustaceanvim spec are NOT modified + (decision 4). +- [ ] `plans/README.md` status row for 010 updated. + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **The Step 1 round-trip does not print exactly `false false true false`.** + In particular if `is_disabled("rust_analyzer")` returns `true`, the defensive + guard is wrong and the loop would disable rust — STOP and report; do not + "fix" it by special-casing rust in the loop (the guard is the right place). +- **`vim.json` / `vim.fn.writefile` / `vim.fs.dirname` are unavailable in your + nvim** (re-check: the box this was planned on is nvim 0.12.3 and they are + core). If something is missing, report; do not swap in a different + persistence mechanism without operator input. +- **You are tempted to make `MasonInstallAll` respect the toggle.** Do not. + Re-read decision 5: orthogonality is the point (instant re-enable). If the + operator later wants a separate `:MasonInstallEnabled`, that's a different + plan. +- **The snacks.picker multi-toggle (Step 5) proves fragile.** Take the escape + hatch: ship Step 4 and report that Step 5 is deferred. Do not ship a + fragile custom picker. +- **`vim.lsp.get_clients({ name = ... })` errors in your nvim version.** Report; + the stop_client call is a nice-to-have, not load-bearing (restart is the real + mechanism). It can be wrapped in pcall if needed, but ask first. + +## Maintenance notes + +- **What interacts with this later**: + - Adding a new server to `mason_options.ensure_installed`: if it's + loop-managed (not rust/jdtls), ALSO add it to `M.toggleable` in + `lsp-toggles.lua` or it won't appear in the toggle UI (it'll still work, + just not be toggleable). The two lists are intentionally separate so + rust_analyzer stays in `ensure_installed` but out of `toggleable`. + - The persistence file is machine-local and gitignored-adjacent (under + `stdpath("data")`, outside the repo) — it will NOT be committed and will + differ per machine. That's by design (decision 1). +- **Reviewer focus**: + - The `is_disabled` defensive guard (non-toggleable → false) is the + load-bearing safety check. Confirm it. + - `MasonInstallAll` body must be byte-identical to before (decision 5). + - The two-list invariant: `toggleable ⊆ ensure_installed \ {rust_analyzer, + jdtls}`. +- **Known v1 limits (documented in UI messages)**: + - Re-enabling requires restart (no `vim.lsp.disable` in 0.12). + - rust_analyzer and jdtls are not toggleable. + - Toggles are global, not per-project. +- **Deferred follow-ups** (out of scope here): + - snacks multi-toggle picker (Step 5 escape hatch). + - Per-project toggles via `root_dir`. + - Toggle UI for rust/jdtls (needs plugin-specific hooks: a `vim.g.rustaceanvim` + disable flag, a `attach_jdtls` short-circuit). diff --git a/plans/README.md b/plans/README.md new file mode 100644 index 0000000..71fff14 --- /dev/null +++ b/plans/README.md @@ -0,0 +1,102 @@ +# Implementation Plans + +Generated by the `improve` skill on 2026-06-18 against commit `baf9a2c`. +Execute in the order below unless dependencies say otherwise. Each executor: +read the plan fully before starting, honor its STOP conditions, run every +verification command, and update your row in this table when done. + +The audit found **3 confirmed runtime bugs** (a keymap that throws, a linter +that silently does nothing, and LSP keymaps attaching to the wrong buffer), a +**stale README**, and **no way to detect any of this automatically**. These 5 +plans address exactly that set. Several other real findings exist but were +deferred — see "Deferred" at the bottom. + +## Execution order & status + +| Plan | Title | Priority | Effort | Depends on | Status | +|------|-------|----------|--------|------------|--------| +| 001 | Add headless smoke test + CI | P1 | M | — | DONE (branch `advisor/001-smoke-ci` @ `89e4b75`, verdict APPROVE) | +| 002 | Fix flash `tresitter_search` typo | P1 | S | — | DONE (branch `advisor/002-flash-typo` @ `5d6c947`, verdict APPROVE) | +| 003 | Fix nvim-lint `biomejs` → `biome` (+ wire events) | P1 | S | — | DONE (branch `advisor/003-linter-biome` @ `b021096`, verdict APPROVE) | +| 004 | Fix jdtls `bufnr` → `args.buf` in LspAttach | P1 | S | — | DONE (branch `advisor/004-java-bufnr` @ `66923ac`, verdict APPROVE) | +| 005 | Update stale README (theme + sidekick fork) | P2 | S | — | DONE (branch `advisor/005-readme` @ `f5c7d00`, verdict APPROVE) | +| 006 | Stop `keymaps.lsp()` clobbering Rust's `ca` | P1 | S | — | DONE (branch `advisor/006-rust-ca` @ `bd19732`, verdict APPROVE) | +| 007 | Resolve `` collision (tmux-nav vs LSP signature help) | P2 | S | — | DONE (branch `advisor/007-ck` @ `a1c7abf`, verdict APPROVE) | +| 008 | Make 99's `md_files` find the repo's `AGENTS.md` | P2 | S | — | DONE (branch `advisor/008-mdfiles` @ `e262471`, verdict APPROVE) | +| 009 | Add missing `desc` to 99 + toggleterm keymaps | P3 | S | — | DONE (branch `advisor/009-desc` @ `d9a6fcc`, verdict APPROVE) | +| 010 | `:LspToggle` UI with persisted config (feature) | P3 | M | — | TODO | + +Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | +REJECTED (with one-line rationale). + +**Second audit pass (2026-06-18, post-001–005)** added plans 006–009 from a +deepened correctness/consistency pass. See their files for the new findings +(N1 Rust `ca` clobber, N3 `` collision, N4 99 `AGENT.md`), plus +the vetted subset of TECH-8. + +**Plan 010** is a user-requested feature (not an audit finding): a `:LspToggle` +command backed by a persisted JSON disabled-set, with 5 locked design +decisions (notably: blacklist-not-whitelist persistence, rust/jdtls excluded +from v1, and `MasonInstallAll` kept intentionally orthogonal to toggles). It +carries a spike step for the optional snacks multi-toggle picker with a robust +`vim.ui.select` fallback as the committed v1. + +## Dependency notes + +- **001 first, then 002–005 in any order.** 001 has no *code* dependency on the + fixes, but doing it first lands the CI scaffold so the bug-fix PRs are + actually guarded going forward, and its `scripts/smoke.sh` is the model for + the per-plan verifications. +- 002, 003, 004, 005 are independent of each other (different files) and can be + executed in parallel or in any sequence. +- None of the runtime bugs are caught by a *load-only* smoke test (a keymap + function body, a table string, and a callback closure don't execute at load + time) — that is why each bug plan carries its **own** targeted verification + rather than relying on 001. +- **006–009 are mutually independent** and also independent of 001–005. One + overlap to mind: **008 and 009 both edit `lua/plugins/ai.lua`** but in + non-overlapping regions (`md_files` ~line 370 vs the keymap calls ~lines + 376–399). If applied to the same branch in sequence, expect a clean + non-overlapping merge; both plans flag this and instruct text-match over + absolute line numbers. + +## Findings considered and rejected + +- **BUG-4 — `99` configured with `source = "cmp"` while the repo uses + blink.cmp**: not written as a fix plan because 99's author notes in-file that + "we currently only support cmp right now." Needs a spike on whether 99 has + gained blink support or should be disabled. Re-audit before planning. +- **Direction items (headless CI, AI-stack consolidation, theme-toggle + mechanism)**: these are maintainer-judgement calls, not problems to rank + against bugs. 001 effectively absorbs the "headless CI" direction item. + +## Deferred (real findings, not selected this run) + +Recorded so they are not lost and not blindly re-audited next time: + +- **BUG-5 — `vim.opt_local.conceallevel = 1` at startup** (`lua/vim-options.lua:44`): + only affects the transient startup buffer; intent was a global default. Low + impact, trivial fix (`vim.opt.conceallevel = 1`). Fold into any future + `vim-options.lua` edit. +- **TECH-6 — ~558 lines (33%) of commented-out "alternative palette" code** + (`ai.lua`: 257, `colorscheme.lua`: 145): best done *after* the maintainer + decides the AI-stack and theme direction, otherwise it just regrows. +- **TECH-7 — conform formatter inconsistency** (`lspconfig.lua:250` + `typescript = { "biome" }` vs `biome-check` elsewhere): MED confidence it's + unintended. Related to but separate from plan 003 (different file, formatter + vs linter). +- **TECH-8 — several `vim.keymap.set` calls omit `desc`**: PARTIALLY planned + as 009. Vetting corrected the record: `lspconfig.lua:177` (rust `ca`) + and `:260` (`fM`) ALREADY have `desc` — they were false positives. + 009 covers the genuinely-missing four `99` keymaps + toggleterm. +- **N2 (held out) — ts_ls Vue plugin `location` captured once at setup** + (`lspconfig.lua:88`): `vim.fn.getcwd()` runs inside the one-shot `config()`, + freezing the `node_modules/@vue/typescript-plugin` path to the launch dir. + Vue TS support silently breaks when opening `.vue` from a different project + or after `:cd`. Held out because the fix carries MED risk + a design choice + (resolve at config time vs per-attach via `root_dir`). Worth its own plan + when the maintainer can weigh in on the resolution. +- **DX-10 — no `.stylua.toml`**: AGENTS.md documents the workaround but nothing + enforces tabs. A 3-line `.stylua.toml` fixes it. +- **DX-11 — `bufferline.nvim` not lazy-loaded** (`bufferline.lua`): loads on + every startup; minor. From 3e2da8a377f3bad9cf66ce1d8640d0e693d8bb02 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 22:23:51 +0700 Subject: [PATCH 11/26] fix: move LSP signature help to gK, restore for tmux-nav --- README.md | 2 +- lua/keymaps.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b74841..e55cd03 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ git clone https://github.com/baseddxyz/BASEDDvim.git ~/.config/nvim | `gi` | n | Go to implementation | | `gr` | n | Find references | | `K` | n | Hover documentation | -| `` | i | Signature help | +| `gK` | n | Signature help | | `D` | n | Go to type definition | | `ca` | n, v | Code action | | `rn` | n | Rename | diff --git a/lua/keymaps.lua b/lua/keymaps.lua index c8d8ae0..83b55e4 100644 --- a/lua/keymaps.lua +++ b/lua/keymaps.lua @@ -9,7 +9,7 @@ function M.lsp(opts) { "n", "gd", vim.lsp.buf.definition, opts }, { "n", "K", vim.lsp.buf.hover, opts }, { "n", "gi", vim.lsp.buf.implementation, opts }, - { "i", "", vim.lsp.buf.signature_help, opts }, + { "n", "gK", vim.lsp.buf.signature_help, opts }, { "n", "D", vim.lsp.buf.type_definition, opts }, { "n", "rn", vim.lsp.buf.rename, opts }, { { "n", "v" }, "ca", vim.lsp.buf.code_action, opts }, From c343dc66bc4b14c14e78b5647afdce985ce91623 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Thu, 18 Jun 2026 22:24:07 +0700 Subject: [PATCH 12/26] docs: add plan 011 (gK signature help, supersedes 007) --- plans/011-sighelp-gK-restore-ck.md | 244 +++++++++++++++++++++++++++++ plans/README.md | 15 +- 2 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 plans/011-sighelp-gK-restore-ck.md diff --git a/plans/011-sighelp-gK-restore-ck.md b/plans/011-sighelp-gK-restore-ck.md new file mode 100644 index 0000000..724469d --- /dev/null +++ b/plans/011-sighelp-gK-restore-ck.md @@ -0,0 +1,244 @@ +# Plan 011: Switch signature help to `gK` and restore `` to tmux-navigate-up + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat be48dcc..HEAD -- lua/keymaps.lua README.md` +> If either file changed since this plan was written, compare the "Current +> state" excerpts against the live code before proceeding; on a mismatch, +> treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none +- **Category**: bug (corrects a defect introduced by plan 007) +- **Planned at**: commit `be48dcc`, 2026-06-18 +- **Supersedes**: plan 007's mode-split resolution + +## Why this matters + +Plan 007 resolved the `` collision (tmux-navigate-up vs LSP signature +help) by moving signature help from `n` to `i` mode. That fix was flawed: it +**shadowed Vim's built-in insert-mode digraph key** (`i_CTRL-K`, `:help i_CTRL-K` +— `e'` types `é`, etc.), which is a core editing feature, not an optional +plugin. So 007 traded a cross-plugin collision for a collision with a built-in. + +This plan corrects it with the cleanest resolution (Option 3 from the 007 +review): **keep `` for tmux navigation everywhere (normal mode, as the +tmux plugin defines it), and move LSP signature help to `gK` in normal mode.** +Both features win in their natural form, insert mode stays free for digraphs, +and `gK` is a recognizable nvim convention (verified free during planning). + +The user confirmed they DO use tmux navigation (``), which makes +this the right resolution over a simple revert. + +## Current state + +`lua/keymaps.lua` line 12 — currently the flawed mode split from plan 007: + +```lua + { "i", "", vim.lsp.buf.signature_help, opts }, +``` + +`README.md` line 104 — currently documents the flawed state: + +``` +| `` | i | Signature help | +``` + +`lua/plugins/tmux.lua` line 14 — unchanged and correct (do not touch): + +```lua + { "", "TmuxNavigateUp" }, +``` + +### Why the fix is fully isolated to keymaps.lua + +`signature_help` appears in exactly ONE place in the repo — line 12 of +`keymaps.lua` (verified: `grep -rn signature_help lua/` → only that line). All +three LSP on_attach callers get the binding through the shared +`keymaps.lsp({...})` helper: +- `lua/plugins/lspconfig.lua:56` (generic on_attach) +- `lua/plugins/lspconfig.lua:178` (rustaceanvim on_attach) +- `lua/plugins/java.lua:139` (jdtls LspAttach) + +So changing the one row in `keymaps.lua` propagates the `gK` binding to all +LSP buffers automatically. No other file needs a binding edit. + +### Verified free + +`gK` is not used anywhere in `lua/` or `README.md` (confirmed during planning). +It is a recognizable nvim convention (some configs use `gK` for signature help +or hover alternatives). + +### Repo conventions to match + +- `keymaps.lua` rows are `{ mode, lhs, rhs, opts }`. Change the mode and lhs of + the one row; keep `vim.lsp.buf.signature_help` and `opts` verbatim. +- README keybinding rows are `| | | |`. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| `` row reverted to tmux-only (no LSP row) | `grep -n '' lua/keymaps.lua` | no matches | +| `gK` row present in normal mode | `grep -n '"gK"' lua/keymaps.lua` | one match: `{ "n", "gK", vim.lsp.buf.signature_help, opts },` | +| File parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | +| tmux binding untouched | `grep -n '' lua/plugins/tmux.lua` | unchanged line 14 | +| README sig-help row updated | `grep -n 'Signature help' README.md` | line shows `\| \`gK\` \| n \|` | + +## Scope + +**In scope** (the only files you should modify): +- `lua/keymaps.lua` — change the one signature-help row from + `{ "i", "", ... }` to `{ "n", "gK", ... }`. +- `README.md` — change the "Signature help" row from + `| \`\` | i | Signature help |` to `| \`gK\` | n | Signature help |`. + +**Out of scope** (do NOT touch): +- `lua/plugins/tmux.lua` — the tmux `` binding is correct and should stay. +- The three `keymaps.lsp(...)` call sites (lspconfig, rustaceanvim, jdtls) — + they pick up the `gK` binding via the shared helper; no edit needed. +- Other rows in `keymaps.lua` or other README keybinding rows. +- Digraphs (`i_CTRL-K`) — no mapping touches insert mode after this plan, so + the built-in digraph is automatically restored. Do NOT add an explicit + digraph mapping. + +## Git workflow + +- Branch: `advisor/011-sighelp-gK` +- Single commit. Message style (conventional commits): + `fix: move LSP signature help to gK, restore for tmux-nav`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Change the signature-help row in keymaps.lua + +In `lua/keymaps.lua` line 12, change both the mode and the lhs: + +Before: +```lua + { "i", "", vim.lsp.buf.signature_help, opts }, +``` + +After: +```lua + { "n", "gK", vim.lsp.buf.signature_help, opts }, +``` + +Leave every other row in the `keymaps` table and the rest of the file +untouched. + +**Verify**: +- `grep -n '' lua/keymaps.lua` → no matches (the LSP `` row is gone; + tmux's `` lives in `tmux.lua`, not here). +- `grep -n '"gK"' lua/keymaps.lua` → one match reading + `{ "n", "gK", vim.lsp.buf.signature_help, opts },`. + +### Step 2: Update the README signature-help row + +In `README.md` line ~104, change the "Signature help" row: + +Before: +``` +| `` | i | Signature help | +``` + +After: +``` +| `gK` | n | Signature help | +``` + +Do NOT touch the "Tmux Integration" `| | n | Navigate up |` row (~line 160) +— it stays. + +**Verify**: +- `grep -n 'Signature help' README.md` → the row shows `` `gK` `` and `| n |`. +- `grep -n '' README.md` → only the "Navigate up" row (one match). + +### Step 3: Parse-check and commit + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +- Stage `lua/keymaps.lua` and `README.md`. +- Commit: `fix: move LSP signature help to gK, restore for tmux-nav`. + +**Verify**: `git show --stat HEAD` → exactly two files changed +(`lua/keymaps.lua`, `README.md`); the `lua/keymaps.lua` diff is exactly one +token-pair change (`"i"`→`"n"` and `""`→`"gK"`). + +## Test plan + +- **Static (required)**: the grep checks above. +- **Manual runtime check** (cheap, recommended, requires plugins): + 1. In a code buffer with an LSP attached, normal mode, press `gK` → signature + help popup appears. + 2. Press `` in normal mode → tmux pane moves up (or no-ops outside tmux; + importantly it does NOT pop signature help). + 3. In insert mode, press `` then a digraph (e.g. `e'`) → the digraph + character (`é`) is inserted, confirming the built-in is restored. + All three holding = collision resolved cleanly. Requires the plugins + installed; not a CI gate. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n '' lua/keymaps.lua` returns no matches. +- [ ] `grep -n '"gK"' lua/keymaps.lua` returns one match: + `{ "n", "gK", vim.lsp.buf.signature_help, opts },`. +- [ ] `grep -n '' lua/plugins/tmux.lua` unchanged (line 14, `TmuxNavigateUp`). +- [ ] `grep -n 'Signature help' README.md` shows `` `gK` `` and `| n |`. +- [ ] `grep -n '' README.md` shows only the "Navigate up" row. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/keymaps.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] `git show --stat HEAD` shows only `lua/keymaps.lua` and `README.md`. +- [ ] `plans/README.md` status row for 011 updated (TODO → DONE), and plan + 007's maintenance notes reference 011 (see Maintenance below). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The `keymaps.lua` row at line 12 does not match the excerpt (drift). Report. +- `gK` turns out to already be in use somewhere (re-check with + `grep -rn '"gK"' lua/ README.md`). If it is, this plan's resolution collides + — STOP and report; do not pick a different key without operator input. +- `signature_help` is found bound anywhere else besides `keymaps.lua:12` + (re-check `grep -rn signature_help lua/`). The plan assumes the single + binding; if there's another, the tmux-nav restoration would be incomplete + for that buffer. Report. +- The README "Signature help" row is not at the expected text (reworded, etc.). + Report the actual line; do not guess the cell to edit. + +## Maintenance notes + +- **This supersedes plan 007's resolution.** Plan 007's file should be updated + (by the operator or a follow-up) to note that its mode-split approach was + replaced by 011's `gK` remap because 007 shadowed `i_CTRL-K` digraphs. The + 007 commit (`a1c7abf`) stays in history as a reverted approach. +- **What interacts with this later**: any future mapping of `gK` (by another + plugin or personal addition) would re-introduce a collision on signature + help. Check `lua/plugins/blink.lua` and any new plugin's keymaps. `` + in insert mode must stay free for digraphs — do NOT map it in insert mode + anywhere. +- **Reviewer focus**: confirm only the one `keymaps.lua` row changed + (mode + lhs), the tmux binding was untouched, and no insert-mode mapping was + introduced. The diff for `keymaps.lua` should be exactly two tokens changed + on one line. +- **Lesson recorded**: plan 007's "verified free" check only looked for + existing *user* mappings, not built-in Vim features. `i_CTRL-K` is a + built-in digraph entry that a user mapping shadows. Future "is this key + free?" checks must include built-in mode-specific features (`:help i_*`, + `:help c_*`, etc.), not just `grep` for existing mappings. This is noted + here so the vetting gap is on the record. diff --git a/plans/README.md b/plans/README.md index 71fff14..32acd10 100644 --- a/plans/README.md +++ b/plans/README.md @@ -25,6 +25,7 @@ deferred — see "Deferred" at the bottom. | 008 | Make 99's `md_files` find the repo's `AGENTS.md` | P2 | S | — | DONE (branch `advisor/008-mdfiles` @ `e262471`, verdict APPROVE) | | 009 | Add missing `desc` to 99 + toggleterm keymaps | P3 | S | — | DONE (branch `advisor/009-desc` @ `d9a6fcc`, verdict APPROVE) | | 010 | `:LspToggle` UI with persisted config (feature) | P3 | M | — | TODO | +| 011 | Switch signature help to `gK`; restore `` for tmux-nav | P2 | S | — | DONE (branch `advisor/011-gK` @ `3e2da8a`, verdict APPROVE; supersedes 007) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). @@ -34,12 +35,14 @@ deepened correctness/consistency pass. See their files for the new findings (N1 Rust `ca` clobber, N3 `` collision, N4 99 `AGENT.md`), plus the vetted subset of TECH-8. -**Plan 010** is a user-requested feature (not an audit finding): a `:LspToggle` -command backed by a persisted JSON disabled-set, with 5 locked design -decisions (notably: blacklist-not-whitelist persistence, rust/jdtls excluded -from v1, and `MasonInstallAll` kept intentionally orthogonal to toggles). It -carries a spike step for the optional snacks multi-toggle picker with a robust -`vim.ui.select` fallback as the committed v1. +**Plan 011 corrects a defect from plan 007.** Plan 007 moved LSP signature +help to insert mode to free `` for tmux-nav, but that shadowed Vim's +built-in insert-mode digraph key (`i_CTRL-K`). 011 instead remaps signature +help to `gK` (normal mode, verified free) and leaves `` for tmux-nav and +`i_CTRL-K` for digraphs. Root-cause of the 007 mistake: its "key free?" check +only looked for existing user mappings, not built-in Vim features — recorded as +a vetting lesson. The 007 commit (`a1c7abf`) stays in history; 011 is the live +resolution. ## Dependency notes From e0721d500c7a5567bca07b5a5709e4be67cad72e Mon Sep 17 00:00:00 2001 From: qapquiz Date: Fri, 19 Jun 2026 00:34:44 +0700 Subject: [PATCH 13/26] perf: lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native virtual_lines --- lua/plugins/ai.lua | 2 +- lua/plugins/bufferline.lua | 1 + lua/plugins/lspconfig.lua | 1 - lua/vim-options.lua | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/plugins/ai.lua b/lua/plugins/ai.lua index 698f72e..c252aa7 100644 --- a/lua/plugins/ai.lua +++ b/lua/plugins/ai.lua @@ -230,7 +230,7 @@ return configs.ai { "sourcegraph/amp.nvim", branch = "main", - lazy = false, + cmd = { "Amp" }, opts = { auto_start = true, log_level = "info" }, }, -- keys = { diff --git a/lua/plugins/bufferline.lua b/lua/plugins/bufferline.lua index 512f1ef..a386951 100644 --- a/lua/plugins/bufferline.lua +++ b/lua/plugins/bufferline.lua @@ -2,6 +2,7 @@ return { { 'akinsho/bufferline.nvim', version = "*", + event = 'VimEnter', dependencies = 'nvim-tree/nvim-web-devicons', config = function () vim.opt.termguicolors = true diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index 49b6aba..82057f7 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -169,7 +169,6 @@ return { { "mrcjkb/rustaceanvim", version = false, - lazy = false, ft = { "rust" }, opts = { server = { diff --git a/lua/vim-options.lua b/lua/vim-options.lua index 0b244a6..8caf03e 100644 --- a/lua/vim-options.lua +++ b/lua/vim-options.lua @@ -37,8 +37,8 @@ map("n", "]w", diagnostic_goto(true, "WARN"), { desc = "Next Warning" }) map("n", "[w", diagnostic_goto(false, "WARN"), { desc = "Prev Warning" }) vim.diagnostic.config({ - virtual_text = true, - -- virtual_lines = true, + virtual_text = false, + virtual_lines = true, }) vim.opt_local.conceallevel = 1 From 2f10ac4d73b8feb48e743060ee8f6bf1f7137ac4 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Fri, 19 Jun 2026 00:34:58 +0700 Subject: [PATCH 14/26] docs: add plan 012 (lazy-load + native virtual_lines) --- plans/012-lazy-native-bundle.md | 353 ++++++++++++++++++++++++++++++++ plans/README.md | 1 + 2 files changed, 354 insertions(+) create mode 100644 plans/012-lazy-native-bundle.md diff --git a/plans/012-lazy-native-bundle.md b/plans/012-lazy-native-bundle.md new file mode 100644 index 0000000..8dc9c93 --- /dev/null +++ b/plans/012-lazy-native-bundle.md @@ -0,0 +1,353 @@ +# Plan 012: Lazy-loading hygiene + native `virtual_lines` diagnostics + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat c343dc6..HEAD -- lua/plugins/ai.lua lua/plugins/bufferline.lua lua/plugins/lspconfig.lua lua/vim-options.lua` +> If any of those files changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S (4 small, independent edits across 4 files) +- **Risk**: LOW +- **Depends on**: none +- **Category**: perf / dx +- **Planned at**: commit `c343dc6`, 2026-06-18 + +## Why this matters + +Four small, independent wins bundled into one plan: + +1. **`amp.nvim` lazy-loads instead of spawning at startup.** It's `lazy = false` + with `auto_start = true`, so its AI backend starts on every Neovim launch — + real startup cost plus a background process even when you won't use AI. + Lazy-load it on first invocation. +2. **`bufferline.nvim` lazy-loads.** It has no lazy trigger at all, so it loads + on every startup. It only needs to exist once the UI is up. +3. **`rustaceanvim`'s redundant `lazy = false` is removed.** It already has + `ft = { "rust" }` which lazy-loads it on Rust files; the explicit + `lazy = false` contradicts that and is cargo-cult noise. (This is a clarity + fix, not a behavior change — rustaceanvim stays lazy-loaded via `ft`.) +4. **Enable native `virtual_lines` diagnostics.** `vim.diagnostic.config` in + `vim-options.lua` has `virtual_text = true` with a commented-out + `-- virtual_lines = true` you clearly wanted. Native `virtual_lines` is + verified supported in this nvim (0.12.3). It renders multi-line diagnostics + inline (modern LSP style). + +All four are S-effort, LOW-risk, independent. Bundled to avoid four tiny PRs. + +## Current state + +### `lua/plugins/ai.lua` lines 231–236 (amp.nvim) + +```lua + { + "sourcegraph/amp.nvim", + branch = "main", + lazy = false, + opts = { auto_start = true, log_level = "info" }, + }, +``` + +`amp.nvim` is the Sourcegraph AI assistant. The commented-out `keys = {...}` +block below it (lines 238+) shows the maintainer once planned manual triggers. +It has no `cmd`/`keys`/`event`/`ft` today — only the eager `lazy = false`. + +### `lua/plugins/bufferline.lua` (full file) + +```lua +return { + { + 'akinsho/bufferline.nvim', + version = "*", + dependencies = 'nvim-tree/nvim-web-devicons', + config = function () + vim.opt.termguicolors = true + require('bufferline').setup{} + end + } +} +``` + +No `event`/`keys`/`cmd`/`ft` → loads at startup. + +### `lua/plugins/lspconfig.lua` lines 170–174 (rustaceanvim head) + +```lua + { + "mrcjkb/rustaceanvim", + version = false, + lazy = false, + ft = { "rust" }, +``` + +`ft = { "rust" }` already lazy-loads; `lazy = false` is contradictory. + +### `lua/vim-options.lua` lines 39–44 + +```lua +vim.diagnostic.config({ + virtual_text = true, + -- virtual_lines = true, +}) + +vim.opt_local.conceallevel = 1 +``` + +`virtual_lines = true` is commented out; verified `vim.diagnostic.config({virtual_lines=true})` +is accepted without error in this nvim. + +### Repo conventions to match + +- Lazy triggers use `event`/`ft`/`cmd`/`keys` (see exemplars throughout + `lua/plugins/` — e.g. `lspconfig.lua:68` `event = { "BufReadPre", "BufNewFile" }`). +- One plugin spec per entry; keep `opts`/`config` structure as-is. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| amp no longer eager | `grep -n 'lazy = false' lua/plugins/ai.lua` | no `lazy = false` on the amp block (only on rustaceanvim, which P3 also removes — see STOP if rust still has it) | +| amp has a trigger | `grep -nE 'event =|cmd =|keys =' lua/plugins/ai.lua \| grep -A0 amp` | amp spec has `cmd`/`keys`/`event` | +| bufferline has event | `grep -n 'event\|VeryLazy\|VimEnter' lua/plugins/bufferline.lua` | one match | +| rustaceanvim `lazy = false` gone | `grep -n 'lazy = false' lua/plugins/lspconfig.lua` | no matches | +| virtual_lines enabled | `grep -n 'virtual_lines\|virtual_text' lua/vim-options.lua` | `virtual_lines = true` active, `virtual_text` handled per Step 4 | +| Files parse | see Done criteria | exit 0 for each | + +## Scope + +**In scope** (the only files you should modify): +- `lua/plugins/ai.lua` — convert the `amp.nvim` spec from eager to lazy. +- `lua/plugins/bufferline.lua` — add a lazy `event`. +- `lua/plugins/lspconfig.lua` — remove `lazy = false` from rustaceanvim. +- `lua/vim-options.lua` — enable `virtual_lines`. + +**Out of scope** (do NOT touch): +- The rest of `ai.lua` (supermaven, sidekick, 99) — only the amp block changes. +- `rustaceanvim`'s `ft`, `opts`, `on_attach`, etc. — only the `lazy = false` line goes. +- `vim-options.lua`'s other settings (tabs, relativenumber, leader, keymaps, + the `vim.opt_local.conceallevel` line which is a separate deferred finding). +- Other diagnostic-rendering plugins (trouble.nvim, mini.notify) — unaffected. + +## Git workflow + +- Branch: `advisor/012-lazy-native` +- One commit covering all four (or per-step commits are fine). Message style + (conventional commits): `perf: lazy-load amp/bufferline, drop redundant + rustaceanvim lazy, enable native virtual_lines`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Lazy-load amp.nvim + +In `lua/plugins/ai.lua`, change the `amp.nvim` spec so it loads on a command +instead of eagerly. Replace: + +```lua + { + "sourcegraph/amp.nvim", + branch = "main", + lazy = false, + opts = { auto_start = true, log_level = "info" }, + }, +``` + +with: + +```lua + { + "sourcegraph/amp.nvim", + branch = "main", + cmd = { "Amp" }, + opts = { auto_start = true, log_level = "info" }, + }, +``` + +(Drop `lazy = false`; add `cmd = { "Amp" }`. When the user runs `:Amp` for the +first time, amp loads and its `auto_start` then takes over for that session. +`auto_start = true` keeps working — it governs behavior *after* the plugin is +loaded, not whether it loads.) + +**Verify**: +- `grep -n 'lazy = false' lua/plugins/ai.lua` → no matches. +- `grep -nA4 'amp.nvim"' lua/plugins/ai.lua` → the amp block shows + `cmd = { "Amp" }` and no `lazy = false`. + +### Step 2: Lazy-load bufferline + +In `lua/plugins/bufferline.lua`, add an `event` so it loads once the UI is up. +Replace the whole spec with: + +```lua +return { + { + 'akinsho/bufferline.nvim', + version = "*", + event = 'VimEnter', + dependencies = 'nvim-tree/nvim-web-devicons', + config = function () + vim.opt.termguicolors = true + require('bufferline').setup{} + end + } +} +``` + +(Adds `event = 'VimEnter'`. Keeps everything else identical. `VimEnter` fires +once after startup completes — bufferline draws the tabline immediately after, +no perceptible delay, but it's no longer on the critical startup path. This is +the documented bufferline convention.) + +**Verify**: +- `grep -n "event = 'VimEnter'" lua/plugins/bufferline.lua` → one match. + +### Step 3: Drop rustaceanvim's redundant `lazy = false` + +In `lua/plugins/lspconfig.lua` lines 170–174, remove the `lazy = false,` line +ONLY. `ft = { "rust" }` already lazy-loads the plugin on Rust files. + +Before: +```lua + { + "mrcjkb/rustaceanvim", + version = false, + lazy = false, + ft = { "rust" }, +``` + +After: +```lua + { + "mrcjkb/rustaceanvim", + version = false, + ft = { "rust" }, +``` + +**Verify**: +- `grep -n 'lazy = false' lua/plugins/lspconfig.lua` → no matches. +- `grep -n 'ft = { "rust" }' lua/plugins/lspconfig.lua` → unchanged, one match. + +### Step 4: Enable native `virtual_lines` + +In `lua/vim-options.lua` lines 39–43, switch from `virtual_text` to +`virtual_lines`. Replace: + +```lua +vim.diagnostic.config({ + virtual_text = true, + -- virtual_lines = true, +}) +``` + +with: + +```lua +vim.diagnostic.config({ + virtual_text = false, + virtual_lines = true, +}) +``` + +**Why `virtual_text = false`:** the two render styles overlap (both try to draw +inline diagnostic text). `virtual_lines` is the newer multi-line inline style; +disabling `virtual_text` avoids duplicate/confusing rendering. If you later +prefer the old style, flip both back. + +**Verify**: +- `grep -n 'virtual_lines\|virtual_text' lua/vim-options.lua` → + `virtual_text = false` and `virtual_lines = true`. +- `nvim --headless -u NONE +'lua vim.diagnostic.config({virtual_lines=true, virtual_text=false}); assert(true); print("DIAG_OK")' +qa` + → prints `DIAG_OK` (confirms the nvim accepts both options — already verified + in planning, re-check here). + +### Step 5: Parse-check and commit + +**Verify** (each parses): +```bash +for f in lua/plugins/ai.lua lua/plugins/bufferline.lua lua/plugins/lspconfig.lua lua/vim-options.lua; do + nvim --headless -u NONE +"lua local fn,err=loadfile('$f'); assert(fn,err)" +qa && echo "OK $f" || echo "FAIL $f" +done +``` +All four should print `OK`. + +- Stage all four files. +- Commit: `perf: lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native virtual_lines`. + +**Verify**: `git show --stat HEAD` → exactly four files changed. + +## Test plan + +- **Static (required)**: all the grep checks above + the parse loop. +- **Manual startup check (recommended)**: with the full config loaded, time + startup (`nvim --startuptime /tmp/s.txt +qa; tail -1 /tmp/s.txt`) before and + after on your machine. Expect a reduction (amp especially). Not a hard gate — + the lazy-loading is correct by construction; timing is confirmation. +- **amp behavior**: run `:Amp` and confirm it loads + starts (auto_start kicks + in post-load). If `:Amp` isn't the real command name, see STOP conditions. +- **virtual_lines**: open a buffer with diagnostics (e.g. an intentionally + wrong Lua file with lua_ls) and confirm diagnostics render as inline + multi-line virtual lines instead of trailing virtual text. +- **bufferline/rustaceanvim**: confirm the tabline still appears after startup + and rust LSP still attaches on `.rs` files. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'lazy = false' lua/plugins/ai.lua` returns no matches. +- [ ] The amp spec has `cmd = { "Amp" }` (`grep -A4 'amp.nvim"'` shows it). +- [ ] `grep -n "event = 'VimEnter'" lua/plugins/bufferline.lua` returns one match. +- [ ] `grep -n 'lazy = false' lua/plugins/lspconfig.lua` returns no matches. +- [ ] `grep -n 'ft = { "rust" }' lua/plugins/lspconfig.lua` still returns one match. +- [ ] `grep -n 'virtual_lines' lua/vim-options.lua` shows `virtual_lines = true`. +- [ ] All four in-scope files parse (the Step 5 loop prints `OK` for each). +- [ ] `git show --stat HEAD` shows only the four in-scope files. +- [ ] `plans/README.md` status row for 012 updated. + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **`:Amp` is not the correct command name** for amp.nvim. This is the one real + unknown in the plan (amp.nvim isn't installed on the planning box). If the + documented entrypoint differs (e.g. `:AmpStart`, or it's key-only), STOP and + report — pick the right trigger before proceeding. Do not guess a command + name that doesn't exist (that would make the plugin un-loadable). +- **`virtual_lines` errors in your nvim** despite the planning-box check + (re-verify: the `DIAG_OK` check in Step 4). If it errors, leave + `virtual_text = true` and report; do not ship a diagnostic config that breaks + rendering. +- **`bufferline` fails to draw after `VimEnter` lazy-load** (e.g. tabline blank + on first window). Report; `VeryLazy` is an alternative event — but confirm + the problem first, don't pre-emptively switch. +- **`rustaceanvim` stops attaching to `.rs` files** after removing + `lazy = false`. It shouldn't (`ft` handles it), but if it does, report — + the `ft` trigger is the correct mechanism; restoring `lazy = false` would + just re-hide the real issue. +- Any file at the cited locations doesn't match its excerpt (drift). Report. + +## Maintenance notes + +- **amp `auto_start` semantics**: `auto_start = true` means "start the AI + backend once amp is loaded", not "load amp at startup". After this plan, amp + loads on `:Amp`, THEN auto_start runs. If you want amp available without a + manual `:Amp`, change `cmd` to `event = "VimEnter"` (still lazy vs the old + eager load) — but that re-introduces startup cost, so the plan defaults to + the explicit command. +- **virtual_lines vs virtual_text is a preference.** `virtual_lines` is newer + and renders multi-line diagnostics better; `virtual_text` is the classic + trailing style. Both native; flip the two booleans to switch. +- **Reviewer focus**: each of the four changes is one-line-ish and surgical. + Confirm rustaceanvim lost only the `lazy = false` line (not `ft`), and + virtual_lines/virtual_text are the correct pair (one true, one false). +- **Deferred**: the `vim.opt_local.conceallevel = 1` line right below the + diagnostic config is a separate finding (BUG-5, deferred) — do not touch it + here. diff --git a/plans/README.md b/plans/README.md index 32acd10..bd51757 100644 --- a/plans/README.md +++ b/plans/README.md @@ -26,6 +26,7 @@ deferred — see "Deferred" at the bottom. | 009 | Add missing `desc` to 99 + toggleterm keymaps | P3 | S | — | DONE (branch `advisor/009-desc` @ `d9a6fcc`, verdict APPROVE) | | 010 | `:LspToggle` UI with persisted config (feature) | P3 | M | — | TODO | | 011 | Switch signature help to `gK`; restore `` for tmux-nav | P2 | S | — | DONE (branch `advisor/011-gK` @ `3e2da8a`, verdict APPROVE; supersedes 007) | +| 012 | Lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native `virtual_lines` | P2 | S | — | DONE (branch `advisor/012-lazy` @ `e0721d5`, verdict APPROVE) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). From a5fddf11d3a9be09a39341bfcf3841f8a6bb181b Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 15:37:58 +0700 Subject: [PATCH 15/26] refactor: tier diagnostic display (signs/underline/current-line virtual_text) --- lua/vim-options.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lua/vim-options.lua b/lua/vim-options.lua index 8caf03e..4cedc5e 100644 --- a/lua/vim-options.lua +++ b/lua/vim-options.lua @@ -36,9 +36,17 @@ map("n", "[e", diagnostic_goto(false, "ERROR"), { desc = "Prev Error" }) map("n", "]w", diagnostic_goto(true, "WARN"), { desc = "Next Warning" }) map("n", "[w", diagnostic_goto(false, "WARN"), { desc = "Prev Warning" }) +-- Tiered diagnostic display (model borrowed from baseddxyz/paketo): +-- - underline everything (HINT..ERROR) so anything off is always visible +-- - signs in the gutter for WARN+ERROR +-- - inline virtual text ONLY for ERROR, ONLY on the current line +-- - no updates while typing (stable view) vim.diagnostic.config({ - virtual_text = false, - virtual_lines = true, + signs = { priority = 9999, severity = { min = "WARN", max = "ERROR" } }, + underline = { severity = { min = "HINT", max = "ERROR" } }, + virtual_lines = false, + virtual_text = { current_line = true, severity = { min = "ERROR", max = "ERROR" } }, + update_in_insert = false, }) vim.opt_local.conceallevel = 1 From 2afa5c75354204d1ea5f62ee8689f42d5daeef11 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 15:39:34 +0700 Subject: [PATCH 16/26] refactor: move LSP server configs to lsp/ files via native wildcard defaults --- after/ftplugin/markdown.lua | 4 ++ lsp/gopls.lua | 2 + lsp/lua_ls.lua | 5 +++ lsp/ruby_lsp.lua | 2 + lsp/ts_ls.lua | 31 +++++++++++++++ lua/plugins/lspconfig.lua | 78 +++++++------------------------------ 6 files changed, 59 insertions(+), 63 deletions(-) create mode 100644 after/ftplugin/markdown.lua create mode 100644 lsp/gopls.lua create mode 100644 lsp/lua_ls.lua create mode 100644 lsp/ruby_lsp.lua create mode 100644 lsp/ts_ls.lua diff --git a/after/ftplugin/markdown.lua b/after/ftplugin/markdown.lua new file mode 100644 index 0000000..ae6f8a7 --- /dev/null +++ b/after/ftplugin/markdown.lua @@ -0,0 +1,4 @@ +-- Markdown filetype config. Borrowed from baseddxyz/paketo. +-- Establishes the after/ftplugin/ pattern for per-filetype settings. +vim.cmd("setlocal spell wrap") +vim.cmd("setlocal foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr()") diff --git a/lsp/gopls.lua b/lsp/gopls.lua new file mode 100644 index 0000000..4a3d930 --- /dev/null +++ b/lsp/gopls.lua @@ -0,0 +1,2 @@ +-- gopls config. Shared on_attach + capabilities come from the wildcard default. +return {} diff --git a/lsp/lua_ls.lua b/lsp/lua_ls.lua new file mode 100644 index 0000000..b957968 --- /dev/null +++ b/lsp/lua_ls.lua @@ -0,0 +1,5 @@ +-- lua_ls (lua-language-server) config. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. Add LuaLS-specific settings here as needed. +-- See :h vim.lsp.Config for available fields. +return {} diff --git a/lsp/ruby_lsp.lua b/lsp/ruby_lsp.lua new file mode 100644 index 0000000..37a89ce --- /dev/null +++ b/lsp/ruby_lsp.lua @@ -0,0 +1,2 @@ +-- ruby_lsp config. Shared on_attach + capabilities come from the wildcard default. +return {} diff --git a/lsp/ts_ls.lua b/lsp/ts_ls.lua new file mode 100644 index 0000000..63b465b --- /dev/null +++ b/lsp/ts_ls.lua @@ -0,0 +1,31 @@ +-- ts_ls (typescript-language-server) config, with @vue/typescript-plugin for .vue support. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. +-- +-- NOTE (deferred N2): the `location` below is resolved once at config load +-- via vim.fn.getcwd(), so it is frozen to the launch dir. Moving it here +-- isolates the bug; a per-attach fix via `before_init` is a follow-up. +return { + filetypes = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + init_options = { + plugins = { + { + name = "@vue/typescript-plugin", + location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", + languages = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + }, + }, + }, +} diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index 82057f7..8cf9e83 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -50,16 +50,6 @@ local rust_diagnostics = "rust-analyzer" local keymaps = require("keymaps") -local default_lspconfig = function(capabilities) - return { - on_attach = function(_, bufnr) - keymaps.lsp({ buffer = bufnr }) - keymaps.lsp_format({ buffer = bufnr }) - end, - capabilities = capabilities, - } -end - return { -- treesitter { @@ -76,60 +66,22 @@ return { config = function() local capabilities = require("blink.cmp").get_lsp_capabilities() - for _, lsp in ipairs(mason_options.ensure_installed) do - if lsp == "ts_ls" then - vim.lsp.config( - lsp, - vim.tbl_deep_extend("force", default_lspconfig(capabilities), { - init_options = { - plugins = { - { - name = "@vue/typescript-plugin", - location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", - languages = { - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - }, - }, - }, - }, - filetypes = { - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - }, - }) - ) - elseif lsp == "pyright" then - vim.lsp.config( - lsp, - vim.tbl_deep_extend("force", default_lspconfig(capabilities), { - settings = { - pyright = { - -- Using Ruff's import organizer - disableOrganizeImports = true, - }, - python = { - analysis = { - -- Ignore all files for analysis to exclusively use Ruff for linting - ignore = { "*" }, - }, - }, - }, - }) - ) - elseif lsp ~= "rust_analyzer" then - vim.lsp.config(lsp, default_lspconfig(capabilities)) - end + -- Shared defaults applied to every server via wildcard merge. + -- Each server's specifics live in lsp/.lua (auto-discovered). + -- Verified in nvim 0.12: wildcard on_attach + capabilities propagate + -- to named configs loaded from lsp/, while server-specific fields win. + vim.lsp.config("*", { + on_attach = function(_, bufnr) + keymaps.lsp({ buffer = bufnr }) + keymaps.lsp_format({ buffer = bufnr }) + end, + capabilities = capabilities, + }) - -- enable LSP - vim.lsp.enable(lsp) - end + -- Enable the loop-managed servers. rust_analyzer is intentionally + -- omitted: rustaceanvim manages it via vim.g.rustaceanvim. + -- (Verified: vim.lsp.enable accepts a list.) + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }) end, dependencies = { { "saghen/blink.cmp" }, From 98f2e3a6077924a7e671e2b2fe71ff5278d27630 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 15:39:47 +0700 Subject: [PATCH 17/26] docs: add plans 013 (lsp restructure) + 014 (tiered diagnostics) --- plans/013-lsp-restructure.md | 421 ++++++++++++++++++++++++++++++++ plans/014-diagnostics-tiered.md | 203 +++++++++++++++ plans/README.md | 4 +- 3 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 plans/013-lsp-restructure.md create mode 100644 plans/014-diagnostics-tiered.md diff --git a/plans/013-lsp-restructure.md b/plans/013-lsp-restructure.md new file mode 100644 index 0000000..9dea546 --- /dev/null +++ b/plans/013-lsp-restructure.md @@ -0,0 +1,421 @@ +# Plan 013: Restructure LSP config into `lsp/.lua` files + establish `after/ftplugin/` + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 2f10ac4..HEAD -- lua/plugins/lspconfig.lua` +> If `lua/plugins/lspconfig.lua` changed since this plan was written, compare +> the "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: M +- **Risk**: MED (rewrites the core LSP enable path; additive but central) +- **Depends on**: none +- **Category**: tech-debt / architecture +- **Planned at**: commit `2f10ac4`, 2026-06-19 +- **Finding**: Tier-1 borrow from `baseddxyz/paketo` (same author's leaner config) + +## Why this matters + +BASEDDvim's `lua/plugins/lspconfig.lua` holds the entire LSP setup in one +~130-line `config = function()`: a `for` loop over `mason_options.ensure_installed` +with an `elseif lsp == "ts_ls" / pyright / ~= rust_analyzer` chain, a +`default_lspconfig(capabilities)` helper, and `vim.lsp.enable(lsp)` at the +bottom. It's the clunkiest file in the repo, and the deferred N2 finding +(ts_ls Vue plugin path frozen to `vim.fn.getcwd()` at setup) exists *because* +that chain crams per-server config into a loop. + +The same author's other config, `baseddxyz/paketo`, uses the native Neovim 0.11+ +pattern: each server's config lives in its own `lsp/.lua` file, and +`vim.lsp.enable()` auto-discovers it. This plan adopts that pattern — +**verified on this box (nvim 0.12.3)** to also use two features paketo doesn't: + +1. **`vim.lsp.config("*", { on_attach=..., capabilities=... })` wildcard + defaults MERGE into every named server config.** Verified: `on_attach`, + capabilities, and arbitrary shared fields all propagate to a named config + loaded from `lsp/.lua`, while server-specific fields (cmd, settings) + still win. So the shared `keymaps.lsp()`/`keymaps.lsp_format()` on_attach + is set ONCE, not duplicated per server. +2. **`vim.lsp.enable({ "lua_ls", "ts_ls", ... })` accepts a list** — verified. + The whole loop collapses to one call. + +Net result: the `elseif` chain and `default_lspconfig` helper disappear; each +server's specifics live in a focused file you can read and edit in isolation; +the ts_ls N2 bug becomes a one-file follow-up instead of buried in a loop. + +### What this is NOT + +- Not a `vim.pack` migration — lazy.nvim stays. This is a config-organization + change that works with lazy.nvim and is the natural first step IF you ever + do migrate. +- Not touching rustaceanvim or jdtls — they manage their own clients + (`vim.g.rustaceanvim`, `start_or_attach`) and stay in `lspconfig.lua`/`java.lua`. +- Not fixing N2 in this plan — the ts_ls config moves VERBATIM (preserving + current behavior, including the `getcwd()` freeze). N2 is now trivially + fixable in `lsp/ts_ls.lua` as a follow-up; recorded in Maintenance. + +## Current state + +`lua/plugins/lspconfig.lua` — the enable loop (lines ~76–132). Current shape: + +```lua + config = function() + local capabilities = require("blink.cmp").get_lsp_capabilities() + + for _, lsp in ipairs(mason_options.ensure_installed) do + if lsp == "ts_ls" then + vim.lsp.config( + lsp, + vim.tbl_deep_extend("force", default_lspconfig(capabilities), { + init_options = { plugins = { { name = "@vue/typescript-plugin", + location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", + languages = { "javascript", "javascriptreact", "typescript", "typescriptreact", "vue" } } } }, + filetypes = { "javascript", "javascriptreact", "typescript", "typescriptreact", "vue" }, + }) + ) + elseif lsp == "pyright" then + -- ... (pyright is commented out of ensure_installed; dead branch) + elseif lsp ~= "rust_analyzer" then + vim.lsp.config(lsp, default_lspconfig(capabilities)) + end + + -- enable LSP + vim.lsp.enable(lsp) + end + end, +``` + +`default_lspconfig` (lines ~53–61): +```lua +local default_lspconfig = function(capabilities) + return { + on_attach = function(_, bufnr) + keymaps.lsp({ buffer = bufnr }) + keymaps.lsp_format({ buffer = bufnr }) + end, + capabilities = capabilities, + } +end +``` + +`mason_options.ensure_installed` (the loop source): +```lua +ensure_installed = { "lua_ls", "ts_ls", "rust_analyzer", "gopls", "ruby_lsp" } +``` + +### Verified API behavior (nvim 0.12.3, tested during planning) + +- A file at `/lsp/.lua` returning a table is auto-loaded + by `vim.lsp.enable(name)` and `vim.lsp.config[""]`. (Tested with a + fake server; `_marker` field resolved correctly.) +- `vim.lsp.enable({ "a", "b" })` — accepts a list, no error. +- `vim.lsp.config("*", { on_attach=..., capabilities=... })` — wildcard SET + works, and the fields MERGE into named configs loaded from `lsp/.lua` + (verified: `on_attach`, shared markers, and capabilities all propagate; the + named config's own `cmd`/`settings` still win). + +The canonical path is **`lsp/.lua` at the repo root** (on the +runtimepath), NOT `lua/lsp/`. paketo uses `after/lsp/`; both work, but +top-level `lsp/` is the documented canonical location. + +### Repo conventions to match + +- Module pattern `local M = {} … return M` for `lua/`; but `lsp/.lua` + files just `return { ... }` (they're config tables, not modules — same as + paketo's `after/lsp/lua_ls.lua`). +- `keymaps.lsp({ buffer = bufnr })` / `keymaps.lsp_format(...)` is the shared + on_attach pattern (see `lua/keymaps.lua`, used at `lspconfig.lua:56` and + `:178`, `java.lua:139`). +- Tabs for indentation. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| Server config files exist | `ls lsp/lua_ls.lua lsp/ts_ls.lua lsp/gopls.lua lsp/ruby_lsp.lua` | all four listed | +| No `default_lspconfig` helper left | `grep -n 'default_lspconfig' lua/plugins/lspconfig.lua` | no matches | +| No enable loop / `elseif` chain left | `grep -nE 'for _, lsp in ipairs\|elseif lsp ==' lua/plugins/lspconfig.lua` | no matches | +| Wildcard defaults present | `grep -n 'vim.lsp.config("\*"' lua/plugins/lspconfig.lua` | one match | +| List-enable present | `grep -n 'vim.lsp.enable({' lua/plugins/lspconfig.lua` | one match with 4 servers | +| MasonInstallAll command body unchanged | see Done criteria | verbatim | +| Each file parses | see Done criteria loop | exit 0 each | + +## Scope + +**In scope** (the only files you should create or modify): +- `lsp/lua_ls.lua` (new) — `return {}` (LuaLS-specific settings slot for the future). +- `lsp/ts_ls.lua` (new) — ts_ls specifics: Vue plugin + filetypes (moved verbatim). +- `lsp/gopls.lua` (new) — `return {}`. +- `lsp/ruby_lsp.lua` (new) — `return {}`. +- `after/ftplugin/markdown.lua` (new) — establishes the ftplugin pattern (spell + wrap + treesitter fold), borrowed from paketo. +- `lua/plugins/lspconfig.lua` — replace the enable loop + `default_lspconfig` with wildcard defaults + list-enable. + +**Out of scope** (do NOT touch): +- `lua/plugins/java.lua` (jdtls) and the rustaceanvim spec in `lspconfig.lua` — + they manage their own clients and keep their own on_attach. The wildcard + does NOT apply to them (rustaceanvim uses `vim.g.rustaceanvim`; jdtls uses + `start_or_attach`). +- `mason_options.ensure_installed`, `mason_lsp_mapping`, `mason_formatters`, and + the `MasonInstallAll` command — unchanged (Mason install is orthogonal to + LSP enable; see plan 010 decision 5). +- `lua/keymaps.lua` — the shared `keymaps.lsp`/`keymaps.lsp_format` helpers are + used as-is by the new wildcard on_attach. +- nvim-treesitter and conform specs in `lspconfig.lua` — unchanged. +- Do NOT create `lsp/rust_analyzer.lua` — rustaceanvim owns rust-analyzer. + +## Git workflow + +- Branch: `advisor/013-lsp-restructure` +- One commit. Message style (conventional commits): + `refactor: move LSP server configs to lsp/ files via native wildcard defaults`. + +## Steps + +### Step 1: Create `lsp/lua_ls.lua` + +Create `lsp/lua_ls.lua` (repo root, NOT in `lua/`): + +```lua +-- lua_ls (lua-language-server) config. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. Add LuaLS-specific settings here as needed. +-- See :h vim.lsp.Config for available fields. +return {} +``` + +**Verify**: `test -f lsp/lua_ls.lua && echo OK`. + +### Step 2: Create `lsp/ts_ls.lua` (move Vue config VERBATIM) + +Create `lsp/ts_ls.lua` with the ts_ls specifics, moved verbatim from the +current loop (do NOT "fix" the `getcwd()` — that's N2, a separate follow-up): + +```lua +-- ts_ls (typescript-language-server) config, with @vue/typescript-plugin for .vue support. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. +-- +-- NOTE (deferred N2): the `location` below is resolved once at config load +-- via vim.fn.getcwd(), so it is frozen to the launch dir. Moving it here +-- isolates the bug; a per-attach fix via `before_init` is a follow-up. +return { + filetypes = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + init_options = { + plugins = { + { + name = "@vue/typescript-plugin", + location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", + languages = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + }, + }, + }, +} +``` + +**Verify**: `test -f lsp/ts_ls.lua && echo OK`. The `location` line must be +byte-identical to the current `lspconfig.lua` version (preserves behavior). + +### Step 3: Create `lsp/gopls.lua` and `lsp/ruby_lsp.lua` + +`lsp/gopls.lua`: +```lua +-- gopls config. Shared on_attach + capabilities come from the wildcard default. +return {} +``` + +`lsp/ruby_lsp.lua`: +```lua +-- ruby_lsp config. Shared on_attach + capabilities come from the wildcard default. +return {} +``` + +**Verify**: `ls lsp/gopls.lua lsp/ruby_lsp.lua` → both listed. + +### Step 4: Rewrite the enable section in `lua/plugins/lspconfig.lua` + +In the nvim-lspconfig spec's `config = function()` (currently lines ~76–132), +REPLACE the entire `local capabilities = ... for ... end` block with: + +```lua + config = function() + local capabilities = require("blink.cmp").get_lsp_capabilities() + + -- Shared defaults applied to every server via wildcard merge. + -- Each server's specifics live in lsp/.lua (auto-discovered). + -- Verified in nvim 0.12: wildcard on_attach + capabilities propagate + -- to named configs loaded from lsp/, while server-specific fields win. + vim.lsp.config("*", { + on_attach = function(_, bufnr) + keymaps.lsp({ buffer = bufnr }) + keymaps.lsp_format({ buffer = bufnr }) + end, + capabilities = capabilities, + }) + + -- Enable the loop-managed servers. rust_analyzer is intentionally + -- omitted: rustaceanvim manages it via vim.g.rustaceanvim. + -- (Verified: vim.lsp.enable accepts a list.) + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }) + end, +``` + +Also DELETE the now-unused `default_lspconfig` helper (lines ~53–61, the +`local default_lspconfig = function(capabilities) ... end` block) — nothing +references it after this change. + +Leave `local keymaps = require("keymaps")` at the top of the file (still used +by the wildcard on_attach and by the rustaceanvim on_attach). + +**Verify**: +- `grep -n 'default_lspconfig' lua/plugins/lspconfig.lua` → no matches. +- `grep -nE 'for _, lsp in ipairs|elseif lsp ==' lua/plugins/lspconfig.lua` → + no matches. +- `grep -n 'vim.lsp.config("\*")' lua/plugins/lspconfig.lua` → one match. +- `grep -n 'vim.lsp.enable({' lua/plugins/lspconfig.lua` → one match. +- `grep -n 'MasonInstallAll\|mason_options.ensure_installed' lua/plugins/lspconfig.lua` + → MasonInstallAll command body still present and reads + `mason_options.ensure_installed` (unchanged). + +### Step 5: Establish `after/ftplugin/markdown.lua` (pattern + small improvement) + +Create `after/ftplugin/markdown.lua` (borrowed from paketo, establishes the +ftplugin pattern for future per-filetype config): + +```lua +-- Markdown filetype config. Borrowed from baseddxyz/paketo. +-- Establishes the after/ftplugin/ pattern for per-filetype settings. +vim.cmd("setlocal spell wrap") +vim.cmd("setlocal foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr()") +``` + +(This is additive behavior — spell + wrap + treesitter fold for markdown. If +the maintainer doesn't want spell-checking, drop the first line; keep the +rest. Treesitter fold needs the markdown parser, which is in +`treesitter_options.ensure_installed`.) + +**Verify**: `test -f after/ftplugin/markdown.lua && echo OK`. + +### Step 6: Parse-check and commit + +**Verify** (each parses — note `lsp/*.lua` use `loadfile` since they're not +on rtp from the worktree root without nvim's config resolution): +```bash +for f in lua/plugins/lspconfig.lua lsp/lua_ls.lua lsp/ts_ls.lua lsp/gopls.lua lsp/ruby_lsp.lua after/ftplugin/markdown.lua; do + nvim --headless -u NONE +"lua local fn,err=loadfile('$f'); assert(fn,err)" +qa && echo "OK $f" || echo "FAIL $f" +done +``` +All six should print `OK`. + +- Stage all six files. +- Commit: `refactor: move LSP server configs to lsp/ files via native wildcard defaults`. + +**Verify**: `git show --stat HEAD` → six files (4 new lsp/, 1 new ftplugin, 1 modified lspconfig.lua). + +## Test plan + +- **Static (required)**: all the grep checks + the parse loop above. +- **Structural merge test (recommended, plugin-free)**: in a throwaway config + dir, confirm the wildcard + `lsp/fakelsp.lua` merge produces a config with + both the shared `on_attach` AND the file's own field. The planning box + verified this exact merge behavior; a re-run in the executor's environment + is cheap confirmation. (Not a hard gate — the behavior was verified during + planning.) +- **Manual runtime check (requires plugins)**: load the full config, open a + Lua file → lua_ls attaches and `gd`/`K`/`gr`/`gK` (signature help) work + (proves wildcard on_attach propagated). Open a `.ts`/`.vue` file → ts_ls + attaches with Vue plugin. Open a `.go` → gopls. Open a `.rb` → ruby_lsp. + Open a `.rs` → rust_analyzer still attaches (rustaceanvim, unaffected). + Open a `.md` → spell + treesitter fold active. +- **MasonInstallAll orthogonality**: `:MasonInstallAll` still installs all 5 + servers + formatters (it reads the unchanged `mason_options.ensure_installed`). + +## Done criteria + +ALL must hold: + +- [ ] `ls lsp/lua_ls.lua lsp/ts_ls.lua lsp/gopls.lua lsp/ruby_lsp.lua` → all four exist. +- [ ] `test -f after/ftplugin/markdown.lua` succeeds. +- [ ] `grep -n 'default_lspconfig' lua/plugins/lspconfig.lua` → no matches. +- [ ] `grep -nE 'for _, lsp in ipairs|elseif lsp ==' lua/plugins/lspconfig.lua` → no matches. +- [ ] `grep -n 'vim.lsp.config("\*")' lua/plugins/lspconfig.lua` → one match. +- [ ] `grep -n 'vim.lsp.enable({' lua/plugins/lspconfig.lua` → one match listing + exactly `{ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }` (no rust_analyzer). +- [ ] The `MasonInstallAll` command body is byte-identical to before + (`git show 2f10ac4:lua/plugins/lspconfig.lua | sed -n '/MasonInstallAll/,/},/p'` + matches the new file's same region). +- [ ] All six in-scope files parse (the Step 6 loop prints `OK` each). +- [ ] `git show --stat HEAD` shows exactly: 4 new `lsp/`, 1 new + `after/ftplugin/markdown.lua`, 1 modified `lua/plugins/lspconfig.lua`. +- [ ] `lua/plugins/java.lua` and the rustaceanvim spec are NOT modified beyond + what was already there. +- [ ] `plans/README.md` status row for 013 updated. + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **`vim.lsp.config("*", ...)` does not merge into named configs in your + environment.** This was verified on the planning box (nvim 0.12.3): wildcard + `on_attach`/capabilities propagated to a named config loaded from + `lsp/.lua`. If a re-check shows the merge doesn't happen, the whole + "shared defaults via wildcard" approach fails and each `lsp/.lua` + would need its own `on_attach`. STOP and report; do not duplicate on_attach + silently (that hides the real issue). +- **`lsp/.lua` is not auto-discovered.** Verified on the planning box + (a fake server's `_marker` resolved). If your nvim requires `after/lsp/` + (paketo's path) instead of top-level `lsp/`, move the four files to + `after/lsp/` and report — but verify first; top-level `lsp/` is canonical. +- **rust-analyzer stops attaching to `.rs` files** after removing it from the + enable list. It shouldn't (rustaceanvim manages it via `vim.g.rustaceanvim`, + independent of `vim.lsp.enable`). If it does, report — re-adding + `"rust_analyzer"` to the enable list would just paper over the real issue. +- **MasonInstallAll changes.** The command body must stay byte-identical + (orthogonality, plan 010 decision 5). If your refactor touched it, STOP. +- Any file at the cited locations doesn't match its excerpt (drift). Report. + +## Maintenance notes + +- **The two-list invariant:** the set passed to `vim.lsp.enable({...})` + (loop-managed servers) is a SUBSET of `mason_options.ensure_installed` + (servers to install), MINUS `rust_analyzer` (rustaceanvim-owned) and + `jdtls` (nvim-jdtls-owned). If you add a new loop-managed server: (a) add + it to `mason_options.ensure_installed` + `mason_lsp_mapping` (Mason), + (b) create `lsp/.lua`, (c) add it to the `vim.lsp.enable({...})` list. + Three places, documented here so they stay in sync. +- **N2 (ts_ls Vue `getcwd()`) is now a one-file follow-up.** It lives in + `lsp/ts_ls.lua` and is flagged in a comment there. The fix: replace the + frozen `location = vim.fn.getcwd() .. ...` with a `before_init` hook that + resolves the path relative to the project root at attach time. Out of scope + here; the isolation is the win. +- **Wildcard on_attach vs rust/jdtls:** the wildcard applies ONLY to servers + enabled via `vim.lsp.enable` (lua_ls, ts_ls, gopls, ruby_lsp). rustaceanvim + and jdtls set their OWN on_attach (in `lspconfig.lua` rust spec and + `java.lua`), which correctly call `keymaps.lsp()` themselves. Keep that + distinction when adding servers. +- **Reviewer focus**: confirm the MasonInstallAll body is untouched, the + enable list has exactly the 4 servers, the wildcard has the shared + on_attach + capabilities, and `default_lspconfig` is fully removed. +- **Lesson from planning**: the wildcard-merge API (`vim.lsp.config("*", ...)`) + was verified empirically before writing this plan, not assumed — same + discipline that caught the plan-007 digraph and plan-010 `package.path` + issues. The `lsp/.lua` auto-discovery and list-enable were likewise + tested with a fake server. diff --git a/plans/014-diagnostics-tiered.md b/plans/014-diagnostics-tiered.md new file mode 100644 index 0000000..f741080 --- /dev/null +++ b/plans/014-diagnostics-tiered.md @@ -0,0 +1,203 @@ +# Plan 014: Refine diagnostics to paketo's tiered model (supersedes plan 012's diagnostic choice) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 2f10ac4..HEAD -- lua/vim-options.lua` +> If `lua/vim-options.lua` changed since this plan was written, compare the +> "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P3 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: none +- **Category**: dx +- **Planned at**: commit `2f10ac4`, 2026-06-19 +- **Supersedes**: the diagnostic-rendering portion of plan 012 (the + `virtual_lines = true` choice). Plan 012's three lazy-loading changes + (amp, bufferline, rustaceanvim) are UNAFFECTED and stay. + +## Why this matters + +Plan 012 (`e0721d5`) enabled blanket `virtual_lines = true` for diagnostics — +the "modern" style that renders every diagnostic inline as multi-line text. +It works, but it's **noisy**: every hint/warn/error on every visible line +expands inline, which crowds the buffer. + +`baseddxyz/paketo` (same author's leaner config) uses a more deliberate, +**severity-tiered** model: + +```lua +signs = { priority = 9999, severity = { min = "WARN", max = "ERROR" } }, +underline = { severity = { min = "HINT", max = "ERROR" } }, +virtual_text = { current_line = true, severity = { min = "ERROR", max = "ERROR" } }, +virtual_lines = false, +update_in_insert = false, +``` + +That is: **underlines everything** (so you always see *something* is off), +**signs** for warn+error (gutter priority), **inline text only for ERRORS and +only on the current line** (the loud stuff you act on), and **no inline text +while typing**. Less noise, clearer severity hierarchy. + +This is a **preference flip, not a bug fix.** Both styles are valid; the +maintainer (after seeing paketo's approach) prefers the tiered model. Verified +during planning that nvim 0.12.3 accepts paketo's full config (including +`virtual_text.current_line` and `severity` ranges) without error. + +## Current state + +`lua/vim-options.lua` lines 39–43 (as set by plan 012): + +```lua +vim.diagnostic.config({ + virtual_text = false, + virtual_lines = true, +}) +``` + +### Repo conventions to match + +- Diagnostics are configured once in `vim-options.lua` (this block). No other + file sets `vim.diagnostic.config`. +- paketo's exact tiered values are the target (borrowed verbatim — it's the + same author's considered choice). + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| virtual_lines off, virtual_text tiered | `grep -n 'virtual_lines\|virtual_text\|signs\|underline\|update_in_insert' lua/vim-options.lua` | all five present with paketo values | +| File parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/vim-options.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | +| Config accepted at runtime | see Step 2 verify | prints `DIAG_OK` | + +## Scope + +**In scope** (the only file you should modify): +- `lua/vim-options.lua` — replace the `vim.diagnostic.config({...})` block. + +**Out of scope** (do NOT touch): +- Everything else in `vim-options.lua` (tabs, relativenumber, leader, keymaps, + the `vim.opt_local.conceallevel` line which is deferred BUG-5). +- Plan 012's three lazy-loading changes (amp/bufferline/rustaceanvim) — they + live in different files and stay. +- Any diagnostic-consuming plugin (trouble.nvim, mini.notify) — unaffected. + +## Git workflow + +- Branch: `advisor/014-diagnostics` +- Single commit. Message style (conventional commits): + `refactor: tier diagnostic display (signs/underline/current-line virtual_text)`. + +## Steps + +### Step 1: Replace the diagnostic config block + +In `lua/vim-options.lua`, replace the current block (lines ~39–43): + +```lua +vim.diagnostic.config({ + virtual_text = false, + virtual_lines = true, +}) +``` + +with: + +```lua +-- Tiered diagnostic display (model borrowed from baseddxyz/paketo): +-- - underline everything (HINT..ERROR) so anything off is always visible +-- - signs in the gutter for WARN+ERROR +-- - inline virtual text ONLY for ERROR, ONLY on the current line +-- - no updates while typing (stable view) +vim.diagnostic.config({ + signs = { priority = 9999, severity = { min = "WARN", max = "ERROR" } }, + underline = { severity = { min = "HINT", max = "ERROR" } }, + virtual_lines = false, + virtual_text = { current_line = true, severity = { min = "ERROR", max = "ERROR" } }, + update_in_insert = false, +}) +``` + +(Tabs for indentation — match the file.) + +**Verify**: +- `grep -n 'virtual_lines' lua/vim-options.lua` → `virtual_lines = false,`. +- `grep -c 'current_line = true' lua/vim-options.lua` → 1. +- `grep -c 'update_in_insert' lua/vim-options.lua` → 1. + +### Step 2: Confirm the config is accepted at runtime + +**Verify**: +`nvim --headless -u NONE +'lua local ok,err=pcall(vim.diagnostic.config, { signs={priority=9999,severity={min="WARN",max="ERROR"}}, underline={severity={min="HINT",max="ERROR"}}, virtual_lines=false, virtual_text={current_line=true,severity={min="ERROR",max="ERROR"}}, update_in_insert=false }); assert(ok,err); print("DIAG_OK")' +qa` +→ prints `DIAG_OK`. (Already verified during planning; re-check here. If it +errors, see STOP conditions.) + +### Step 3: Parse-check and commit + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lua/vim-options.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +- Stage `lua/vim-options.lua`. +- Commit: `refactor: tier diagnostic display (signs/underline/current-line virtual_text)`. + +**Verify**: `git show --stat HEAD` → exactly one file changed +(`lua/vim-options.lua`). + +## Test plan + +- **Static (required)**: the grep + parse + DIAG_OK checks above. +- **Manual runtime check (recommended, requires plugins)**: open a buffer with + diagnostics of mixed severities (e.g. a Lua file with lua_ls hints + an + error). Confirm: underlines on all of them; signs in the gutter for + warn/error; inline virtual text only on the current line and only for the + error; typing doesn't flicker the diagnostics. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'virtual_lines' lua/vim-options.lua` shows `virtual_lines = false`. +- [ ] `grep -c 'current_line = true' lua/vim-options.lua` returns 1. +- [ ] `grep -c 'update_in_insert' lua/vim-options.lua` returns 1. +- [ ] `grep -c 'severity' lua/vim-options.lua` returns ≥3 (signs/underline/virtual_text each have one). +- [ ] The Step 2 `DIAG_OK` check passes. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lua/vim-options.lua"); assert(fn,err)' +qa` exits 0. +- [ ] `git show --stat HEAD` shows only `lua/vim-options.lua`. +- [ ] `plans/README.md` status row for 014 updated, and 012's row notes that + its diagnostic portion is superseded by 014. + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The Step 2 `DIAG_OK` check fails despite the planning-box verification. If + nvim rejects the config, report the error verbatim; do not ship a broken + diagnostic config (revert to the plan-012 `virtual_lines = true` and report). +- The code at the cited block doesn't match the excerpt (drift). Report. +- You're tempted to also "fix" the `vim.opt_local.conceallevel` line below — + that's deferred BUG-5, out of scope. Leave it. + +## Maintenance notes + +- **This supersedes plan 012's diagnostic choice only.** Plan 012's commit + (`e0721d5`) stays in history and still carries the three lazy-loading wins + (amp, bufferline, rustaceanvim); only its `virtual_lines = true` is reverted + here. +- **Preference, not correctness.** If the maintainer later wants the "see + everything inline" style back, flip `virtual_lines = true` and + `virtual_text = false` — that's plan 012's config. The tiered model here is + borrowed from paketo deliberately. +- **Reviewer focus**: confirm only the one config block changed and it matches + paketo's values exactly. +- **Why no `virtual_lines`-and-`virtual_text`-both-on:** they render + overlapping inline text. The tiered model uses virtual_text (finer-grained, + supports `current_line` + severity filtering); virtual_lines is off. diff --git a/plans/README.md b/plans/README.md index bd51757..109dcbc 100644 --- a/plans/README.md +++ b/plans/README.md @@ -26,7 +26,9 @@ deferred — see "Deferred" at the bottom. | 009 | Add missing `desc` to 99 + toggleterm keymaps | P3 | S | — | DONE (branch `advisor/009-desc` @ `d9a6fcc`, verdict APPROVE) | | 010 | `:LspToggle` UI with persisted config (feature) | P3 | M | — | TODO | | 011 | Switch signature help to `gK`; restore `` for tmux-nav | P2 | S | — | DONE (branch `advisor/011-gK` @ `3e2da8a`, verdict APPROVE; supersedes 007) | -| 012 | Lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native `virtual_lines` | P2 | S | — | DONE (branch `advisor/012-lazy` @ `e0721d5`, verdict APPROVE) | +| 012 | Lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native `virtual_lines` | P2 | S | — | DONE (branch `advisor/012-lazy` @ `e0721d5`, verdict APPROVE; **diagnostic portion superseded by 014**) | +| 013 | Restructure LSP config into `lsp/.lua` + `after/ftplugin/` (borrow from paketo) | P2 | M | — | DONE (branch `advisor/013-lsp` @ `2afa5c7`, verdict APPROVE) | +| 014 | Tier diagnostics (signs/underline/current-line virtual_text; supersedes 012's virtual_lines) | P3 | S | — | DONE (branch `advisor/014-diag` @ `a5fddf1`, verdict APPROVE) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). From c28a18803052eab06fce49356af132f4eed6a441 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 16:18:39 +0700 Subject: [PATCH 18/26] fix: resolve ts_ls Vue plugin path per-attach via before_init --- lsp/ts_ls.lua | 64 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/lsp/ts_ls.lua b/lsp/ts_ls.lua index 63b465b..95e0428 100644 --- a/lsp/ts_ls.lua +++ b/lsp/ts_ls.lua @@ -2,9 +2,11 @@ -- Shared on_attach + capabilities come from the wildcard default set in -- lua/plugins/lspconfig.lua. -- --- NOTE (deferred N2): the `location` below is resolved once at config load --- via vim.fn.getcwd(), so it is frozen to the launch dir. Moving it here --- isolates the bug; a per-attach fix via `before_init` is a follow-up. +-- The Vue plugin `location` is resolved PER-ATTACH in before_init from the +-- workspace root the LSP client already computed (params.rootUri), NOT frozen +-- at config load. This fixes N2: the old code used vim.fn.getcwd() at load +-- time, which froze the path to the launch dir and broke .vue support when +-- opening a different project or after :cd. return { filetypes = { "javascript", @@ -13,19 +15,49 @@ return { "typescriptreact", "vue", }, + -- Static plugin scaffolding; the `location` is injected per-attach below. init_options = { - plugins = { - { - name = "@vue/typescript-plugin", - location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", - languages = { - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - }, - }, - }, + plugins = {}, }, + -- Mutate params.initializationOptions (NOT config.init_options): params is + -- the actual initialize payload sent to the server, and mutating it is + -- per-attach (config is shared across attaches; mutating it would leak). + before_init = function(params, config) + -- Reuse the workspace root the client already resolved. Prefer rootUri + -- (file:// URI), fall back to rootPath (deprecated but present). + local root + if params.rootUri then + root = vim.uri_to_fname(params.rootUri) + elseif params.rootPath then + root = params.rootPath + end + if not root or root == "" then + return + end + + -- Only inject the Vue plugin if it's actually installed in this project + -- (check package.json, not just the dir — a failed npm install can leave + -- an empty dir). Plain JS/TS projects without Vue skip this cleanly. + local plugin_path = root .. "/node_modules/@vue/typescript-plugin" + local marker = plugin_path .. "/package.json" + if vim.fn.filereadable(marker) ~= 1 then + return + end + + -- Defensive: ensure the plugins table exists on the params being sent. + params.initializationOptions = params.initializationOptions or {} + params.initializationOptions.plugins = params.initializationOptions.plugins or {} + + table.insert(params.initializationOptions.plugins, { + name = "@vue/typescript-plugin", + location = plugin_path, + languages = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + }) + end, } From 28819ec0827fad13ccc4862808f3fed5f94fabb0 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 16:18:57 +0700 Subject: [PATCH 19/26] docs: add plan 015 (ts_ls Vue per-attach path); mark N2 fixed --- plans/015-ts-ls-vue-path.md | 340 ++++++++++++++++++++++++++++++++++++ plans/README.md | 7 + 2 files changed, 347 insertions(+) create mode 100644 plans/015-ts-ls-vue-path.md diff --git a/plans/015-ts-ls-vue-path.md b/plans/015-ts-ls-vue-path.md new file mode 100644 index 0000000..4ae4863 --- /dev/null +++ b/plans/015-ts-ls-vue-path.md @@ -0,0 +1,340 @@ +# Plan 015: Resolve ts_ls Vue plugin path per-attach (fix N2) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 98f2e3a..HEAD -- lsp/ts_ls.lua` +> If `lsp/ts_ls.lua` changed since this plan was written, compare the +> "Current state" excerpt against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: MED (touches how the Vue plugin path resolves at LSP attach; a + subtle bug here silently breaks `.vue` TS support) +- **Depends on**: plan 013 (the file `lsp/ts_ls.lua` was created by 013) +- **Category**: bug +- **Planned at**: commit `98f2e3a`, 2026-06-19 +- **Finding**: N2 (now isolated to one file by plan 013) + +## Why this matters + +`lsp/ts_ls.lua` (created by plan 013) resolves the `@vue/typescript-plugin` +location once at config load: + +```lua +location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", +``` + +`vim.fn.getcwd()` runs when the config table is evaluated (once, at startup). +So the path is **frozen to the launch directory**. Consequences: +- Open a `.vue` file in project B after launching from project A → Vue TS + plugin points at project A's `node_modules` → silent mis-resolution or no + Vue support. +- Use `:cd` to switch projects → same problem. +- Launch nvim from `$HOME` then open a project → the plugin points at + `~/node_modules/...` (almost certainly nonexistent). + +The fix resolves the path **per-attach**, from the project root the LSP client +already computed, so each workspace gets its own correct plugin location. + +## Current state + +`lsp/ts_ls.lua` (full file, post plan 013): + +```lua +-- ts_ls (typescript-language-server) config, with @vue/typescript-plugin for .vue support. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. +-- +-- NOTE (deferred N2): the `location` below is resolved once at config load +-- via vim.fn.getcwd(), so it is frozen to the launch dir. Moving it here +-- isolates the bug; a per-attach fix via `before_init` is a follow-up. +return { + filetypes = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + init_options = { + plugins = { + { + name = "@vue/typescript-plugin", + location = vim.fn.getcwd() .. "/node_modules/@vue/typescript-plugin", + languages = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + }, + }, + }, +} +``` + +### Verified API contract (nvim 0.12.3 — read from the runtime source) + +The fix hinges on `before_init`. The authoritative contract is in +`runtime/lua/vim/lsp/client.lua`: + +- **Line 564**: `initializationOptions = config.init_options,` builds the + `init_params` table during client start. +- **Lines 571–577**: `_run_callbacks({self._before_init_cb}, ..., init_params, config)` + calls `before_init(params, config)` AFTER that build, passing BOTH the + already-built `init_params` AND the original `config`. +- **Line ~585**: `rpc.request('initialize', init_params, ...)` sends + `init_params` (the FIRST arg to `before_init`) to the server. + +**Two things that follow (the subtle part — confirmed with the advisor model):** + +1. **You MUST mutate `params.initializationOptions` (the first arg), NOT + `config.init_options`.** Line 564 is a *reference* copy, so mutating + `config.init_options` would technically be visible for THIS attach — but + it's the **wrong pattern**: the `config` table is shared across attaches, so + mutating it leaks the resolved path into subsequent attaches (different + project gets the previous project's path, or races on concurrent attaches). + Mutating `params` is per-attach and side-effect-free. A future nvim that + deep-copies `init_options` would silently break the `config` approach too; + mutating `params` is correct by definition. +2. **The root is already in `params` — don't recompute.** `before_init`'s first + arg already contains the resolved workspace root: `params.rootUri` + (a `file://` URI) or `params.rootPath` (deprecated but present). Reusing it + avoids a second `vim.fs.root` walk and guarantees agreement with whatever + `root_dir` lspconfig computed. Verified `vim.uri_to_fname` exists (0.12) to + parse `rootUri`. + +### Edge cases the fix must handle + +- **Plugin not installed** (plain JS/TS project without Vue): don't inject a + nonexistent path. Check `vim.fn.isdirectory(plugin_path) == 1` first; if + absent, skip the Vue entry (types_ls tolerates a missing plugin gracefully). + Prefer checking `/node_modules/@vue/typescript-plugin/package.json` + exists (a dir can exist empty after a failed `npm install`). +- **`params.initializationOptions` or `.plugins` may be nil**: the static + `init_options` table sets them, but be defensive — another config layer + could clear them. + +### Repo conventions to match + +- `lsp/.lua` returns a config table (not a module). `before_init`, + `root_dir`, `on_attach`, `on_init`, `on_exit`, `commands` may be functions; + everything else is a plain table (native LSP constraint — verified with the + advisor: `init_options` as a function is NOT supported). +- Tabs for indentation. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| No `getcwd` left in ts_ls.lua | `grep -n 'getcwd' lsp/ts_ls.lua` | no matches | +| `before_init` present | `grep -n 'before_init' lsp/ts_ls.lua` | one match | +| File parses | `nvim --headless -u NONE +'lua local fn,err=loadfile("lsp/ts_ls.lua"); assert(fn,err); print("PARSE_OK")' +qa` | prints `PARSE_OK` | +| Static `location` under init_options gone | `grep -n 'location =' lsp/ts_ls.lua` | no matches in the static init_options block (only inside before_init) | + +## Scope + +**In scope** (the only file you should modify): +- `lsp/ts_ls.lua` — replace the static `location` with a `before_init` that + resolves the plugin path per-attach from `params.rootUri`/`rootPath`. + +**Out of scope** (do NOT touch): +- `lua/plugins/lspconfig.lua` — the wildcard defaults and enable list are + correct; ts_ls's specifics now live entirely in `lsp/ts_ls.lua`. +- The other `lsp/*.lua` files. +- `root_dir` in this file — it's not set here (nvim-lspconfig's ts_ls default + provides it). Do NOT add a custom `root_dir`; reusing `params.rootUri` + sidesteps the need (advisor-recommended). +- Any formatter/linter config (conform, nvim-lint). + +## Git workflow + +- Branch: `advisor/015-ts-ls-vue-path` +- Single commit. Message style (conventional commits): + `fix: resolve ts_ls Vue plugin path per-attach via before_init`. + +## Steps + +### Step 1: Rewrite `lsp/ts_ls.lua` + +Replace the entire contents of `lsp/ts_ls.lua` with: + +```lua +-- ts_ls (typescript-language-server) config, with @vue/typescript-plugin for .vue support. +-- Shared on_attach + capabilities come from the wildcard default set in +-- lua/plugins/lspconfig.lua. +-- +-- The Vue plugin `location` is resolved PER-ATTACH in before_init from the +-- workspace root the LSP client already computed (params.rootUri), NOT frozen +-- at config load. This fixes N2: the old code used vim.fn.getcwd() at load +-- time, which froze the path to the launch dir and broke .vue support when +-- opening a different project or after :cd. +return { + filetypes = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + -- Static plugin scaffolding; the `location` is injected per-attach below. + init_options = { + plugins = {}, + }, + -- Mutate params.initializationOptions (NOT config.init_options): params is + -- the actual initialize payload sent to the server, and mutating it is + -- per-attach (config is shared across attaches; mutating it would leak). + before_init = function(params, config) + -- Reuse the workspace root the client already resolved. Prefer rootUri + -- (file:// URI), fall back to rootPath (deprecated but present). + local root + if params.rootUri then + root = vim.uri_to_fname(params.rootUri) + elseif params.rootPath then + root = params.rootPath + end + if not root or root == "" then + return + end + + -- Only inject the Vue plugin if it's actually installed in this project + -- (check package.json, not just the dir — a failed npm install can leave + -- an empty dir). Plain JS/TS projects without Vue skip this cleanly. + local plugin_path = root .. "/node_modules/@vue/typescript-plugin" + local marker = plugin_path .. "/package.json" + if vim.fn.filereadable(marker) ~= 1 then + return + end + + -- Defensive: ensure the plugins table exists on the params being sent. + params.initializationOptions = params.initializationOptions or {} + params.initializationOptions.plugins = params.initializationOptions.plugins or {} + + table.insert(params.initializationOptions.plugins, { + name = "@vue/typescript-plugin", + location = plugin_path, + languages = { + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + }, + }) + end, +} +``` + +**Verify**: +- `grep -n 'getcwd' lsp/ts_ls.lua` → no matches. +- `grep -n 'before_init' lsp/ts_ls.lua` → one match. +- `grep -n 'vim.uri_to_fname' lsp/ts_ls.lua` → one match. + +### Step 2: Parse-check + +**Verify**: +`nvim --headless -u NONE +'lua local fn,err=loadfile("lsp/ts_ls.lua"); assert(fn,err); print("PARSE_OK")' +qa` +→ prints `PARSE_OK`. + +### Step 3: Commit + +- Stage `lsp/ts_ls.lua`. +- Commit: `fix: resolve ts_ls Vue plugin path per-attach via before_init`. + +**Verify**: `git show --stat HEAD` → exactly one file changed (`lsp/ts_ls.lua`). + +## Test plan + +- **Static (required)**: the grep checks + parse check above. +- **Structural sanity (recommended, plugin-free)**: load the file in a throwaway + nvim and call the returned `before_init` with a fake `params` table to confirm + it injects/doesn't-inject correctly: + ```bash + nvim --headless -u NONE +'lua local cfg = dofile("lsp/ts_ls.lua"); local params = { rootUri = "file:///nonexistent" }; local ok, err = pcall(cfg.before_init, params, {}); local f=io.open("/tmp/n2.txt","w"); f:write("before_init_runs="..tostring(ok).."\n"); f:write("err="..tostring(err).."\n"); f:write("plugins_count="..#(params.initializationOptions and params.initializationOptions.plugins or {})); f:close()' +qa + cat /tmp/n2.txt + ``` + Expected (because the marker file doesn't exist): + `before_init_runs=true`, `err=nil`, `plugins_count=0` (graceful skip — proves + the existence check + defensive init work without erroring). +- **Manual runtime check (requires the plugin)**: in a real Vue project with + `@vue/typescript-plugin` installed, open a `.vue` file, run + `:lua print(vim.inspect(vim.lsp.get_clients({name="ts_ls"})[1].config.init_options.plugins))` + — the Vue entry should appear with a path under THIS project's `node_modules`. + Then `:cd` to a different Vue project, open a `.vue` file, re-check — the path + should reflect the NEW project. (This is the N2 proof: path now follows the + workspace, not the launch dir.) +- **Plain-TS regression check**: open a `.ts` file in a project WITHOUT the Vue + plugin. ts_ls should attach normally with no Vue-plugin warning (the + `filereadable` check skips injection). + +## Done criteria + +ALL must hold: + +- [ ] `grep -n 'getcwd' lsp/ts_ls.lua` returns no matches. +- [ ] `grep -n 'before_init' lsp/ts_ls.lua` returns one match. +- [ ] `grep -n 'vim.uri_to_fname' lsp/ts_ls.lua` returns one match. +- [ ] `nvim --headless -u NONE +'lua local fn,err=loadfile("lsp/ts_ls.lua"); assert(fn,err)' +qa` + exits 0. +- [ ] The structural sanity check prints `before_init_runs=true`, + `plugins_count=0` (graceful skip on a non-existent root). +- [ ] `git show --stat HEAD` shows only `lsp/ts_ls.lua` changed. +- [ ] `plans/README.md` status row for 015 updated, and N2 removed from the + deferred backlog (now fixed). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **`before_init`'s first arg does NOT contain `rootUri` or `rootPath`** in + your environment. This is the LSP-standard `InitializeParams` and was + confirmed from the nvim runtime source, but if a re-check shows neither is + present, the root-resolution approach fails — STOP and report; do not fall + back to `vim.fn.getcwd()` (that's the original bug) or to recomputing via + `vim.fs.root` without operator input. +- **The structural sanity check errors** (the `pcall` returns `false`). The + before_init must run without error even when the root has no Vue plugin — + if it throws, the defensive init is incomplete; report rather than patching + blindly. +- **You're tempted to mutate `config.init_options` instead of `params`.** + Don't. Re-read "Verified API contract" point 1: mutating config leaks across + attaches and is forward-fragile. Mutating params is correct. +- **You're tempted to make `init_options` a function.** Don't — the native LSP + client does not invoke `init_options(params)`; only `before_init`, + `root_dir`, `on_attach`, etc. may be functions. +- The code at the cited locations doesn't match the excerpt (drift). Report. + +## Maintenance notes + +- **Why `params` not `config`** (on the record): line 564 of + `client.lua` is a reference copy, so mutating `config.init_options` would + appear to work for a single attach — but it mutates shared config state, + leaking the resolved path across attaches (different project inherits the + previous path) and racing on concurrent attaches. Mutating `params` is + per-attach. This is the subtle correctness property; preserve it. +- **The `filereadable` check is load-bearing for plain-JS/TS projects.** Without + it, every TS file would inject a Vue plugin path pointing at a likely-absent + dir, and typescript-language-server would warn on startup. Keep the + `package.json`-not-just-dir check (failed npm installs leave empty dirs). +- **If you later add another ts_ls plugin** (e.g. a tailwind LSP plugin via + `init_options.plugins`), it goes in the static `init_options.plugins = {}` + table OR is appended in `before_init` alongside the Vue entry — both work + because the static table is the default and `before_init` only appends. +- **Reviewer focus**: confirm (a) `getcwd` is gone, (b) `before_init` mutates + `params` not `config`, (c) the `filereadable` guard is present, (d) the + defensive `or {}` chains on params. +- **Lesson**: the N2 fix was de-risked by (1) reading the nvim runtime source + for the `before_init` contract rather than assuming, and (2) consulting the + advisor model on the `params`-vs-`config` mutation subtlety, which caught a + real cross-attach leak the source read alone wouldn't have surfaced. This + matches the discipline that caught the plan-007 digraph and plan-010 + `package.path` gaps. diff --git a/plans/README.md b/plans/README.md index 109dcbc..5e36516 100644 --- a/plans/README.md +++ b/plans/README.md @@ -29,6 +29,7 @@ deferred — see "Deferred" at the bottom. | 012 | Lazy-load amp/bufferline, drop redundant rustaceanvim lazy, enable native `virtual_lines` | P2 | S | — | DONE (branch `advisor/012-lazy` @ `e0721d5`, verdict APPROVE; **diagnostic portion superseded by 014**) | | 013 | Restructure LSP config into `lsp/.lua` + `after/ftplugin/` (borrow from paketo) | P2 | M | — | DONE (branch `advisor/013-lsp` @ `2afa5c7`, verdict APPROVE) | | 014 | Tier diagnostics (signs/underline/current-line virtual_text; supersedes 012's virtual_lines) | P3 | S | — | DONE (branch `advisor/014-diag` @ `a5fddf1`, verdict APPROVE) | +| 015 | Resolve ts_ls Vue plugin path per-attach via `before_init` (fix N2) | P2 | S | — | DONE (branch `advisor/015-vue` @ `c28a188`, verdict APPROVE) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). @@ -102,6 +103,12 @@ Recorded so they are not lost and not blindly re-audited next time: or after `:cd`. Held out because the fix carries MED risk + a design choice (resolve at config time vs per-attach via `root_dir`). Worth its own plan when the maintainer can weigh in on the resolution. + **FIXED by plan 015** (`c28a188`): now resolved per-attach via `before_init` + reading `params.rootUri`, with a `filereadable` guard so plain-JS/TS + projects skip it cleanly. De-risked by reading the nvim runtime source for + the `before_init` contract and consulting the advisor on the + `params`-vs-`config` mutation subtlety (mutating config leaks across + attaches). - **DX-10 — no `.stylua.toml`**: AGENTS.md documents the workaround but nothing enforces tabs. A 3-line `.stylua.toml` fixes it. - **DX-11 — `bufferline.nvim` not lazy-loaded** (`bufferline.lua`): loads on From daac270ccdc87a25e98efdb3adec670cca496ad3 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 21:14:37 +0700 Subject: [PATCH 20/26] fix: switch JS/TS lint+format to oxc stack (oxlint + oxfmt), fixes biome regression --- lua/plugins/lspconfig.lua | 10 +++++----- lua/plugins/web-linter.lua | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index 8cf9e83..52edbd9 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -43,7 +43,7 @@ local mason_lsp_mapping = { } local mason_formatters = { - ensure_installed = { "biome", "stylua" }, + ensure_installed = { "oxfmt", "stylua" }, } local rust_diagnostics = "rust-analyzer" @@ -196,10 +196,10 @@ return { conform.setup({ formatters_by_ft = { lua = { "stylua" }, - javascript = { "biome-check" }, - javascriptreact = { "biome-check" }, - typescript = { "biome" }, - typescriptreact = { "biome-check" }, + javascript = { "oxfmt" }, + javascriptreact = { "oxfmt" }, + typescript = { "oxfmt" }, + typescriptreact = { "oxfmt" }, java = { "google-java-format" }, }, format_on_save = { diff --git a/lua/plugins/web-linter.lua b/lua/plugins/web-linter.lua index 926efee..cbbe747 100644 --- a/lua/plugins/web-linter.lua +++ b/lua/plugins/web-linter.lua @@ -1,6 +1,6 @@ local linters = { -- web - 'biome', + 'oxlint', } return { @@ -11,10 +11,10 @@ return { -- Events to trigger linter events = { "BufWritePost", "BufReadPost", "InsertLeave" }, linters_by_ft = { - javascript = { 'biome' }, - javascriptreact = { 'biome' }, - typescript = { 'biome' }, - typescriptreact = { 'biome' }, + javascript = { 'oxlint' }, + javascriptreact = { 'oxlint' }, + typescript = { 'oxlint' }, + typescriptreact = { 'oxlint' }, }, }, config = function(_, opts) From dc07caea0fe4e05c5f89b435ee3b4863cb9fcbcd Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 21:14:54 +0700 Subject: [PATCH 21/26] docs: add plan 016 (oxc stack); mark 003 superseded, TECH-7 resolved --- plans/016-oxc-stack.md | 373 +++++++++++++++++++++++++++++++++++++++++ plans/README.md | 3 + 2 files changed, 376 insertions(+) create mode 100644 plans/016-oxc-stack.md diff --git a/plans/016-oxc-stack.md b/plans/016-oxc-stack.md new file mode 100644 index 0000000..37aa614 --- /dev/null +++ b/plans/016-oxc-stack.md @@ -0,0 +1,373 @@ +# Plan 016: Switch JS/TS linting + formatting to the oxc stack (oxlint + oxfmt) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 28819ec..HEAD -- lua/plugins/web-linter.lua lua/plugins/lspconfig.lua` +> If either file changed since this plan was written, compare the "Current +> state" excerpts against the live code before proceeding; on a mismatch, +> treat it as a STOP condition. + +## Status + +- **Priority**: P1 (fixes a live regression) +- **Effort**: S +- **Risk**: LOW (tool swap; both oxc tools verified real in nvim-lint, conform, AND mason) +- **Depends on**: none +- **Category**: bug / dx +- **Planned at**: commit `28819ec`, 2026-06-19 +- **Supersedes**: plan 003 (which introduced the regression by renaming `biomejs` → `biome`) + +## Why this matters + +### The regression (P1 — must fix) +Plan 003 (commit `b021096`) renamed the nvim-lint linter from `biomejs` to +`biome` on the *assumption* that the linter name matched the Mason package +name. That assumption was **wrong**: nvim-lint ships exactly one Biome +linter, registered as `biomejs` (file `lua/lint/linters/biomejs.lua`, verified +in nvim-lint source). `biome` is the *Mason package* name (the binary) — a +different namespace. The result is a live error on every JS/TS buffer read: + +``` +lua/lint.lua:82: Linter with name `biome` not available +``` + +This was the MED-confidence risk I flagged in plan 003 and proceeded past +without verifying the linter name against nvim-lint's source — exactly the +verification discipline I'd applied elsewhere this session. My mistake. + +### The resolution: switch to oxc (user's choice) +Rather than revert to `biomejs`, the maintainer chose to switch the whole +JS/TS toolchain to the **oxc** stack (oxlint for linting, oxfmt for +formatting). This both fixes the regression AND upgrades to faster, +ESLint-compatible tooling. + +### Verified real (all three namespaces checked, no assumptions) + +| Tool | nvim-lint name | conform name | Mason package | Status | +|---|---|---|---|---| +| **oxlint** (linter) | `oxlint` ✓ (`lua/lint/linters/oxlint.lua`) | — | `oxlint` ✓ (mason-registry) | Stable, widely used | +| **oxfmt** (formatter) | — | `oxfmt` ✓ (`lua/conform/formatters/oxfmt.lua`) | `oxfmt` ✓ (mason-registry `@0.55.0`) | **Pre-1.0** — see caveat | + +**oxfmt caveat (honest):** `oxfmt` is pre-1.0 (npm `0.x`). Its formatting +output may change between minor bumps, which can reformat an entire repo on +upgrade. The maintainer has accepted this tradeoff for a personal config. If +that becomes painful, reverting the formatter to biome is a one-line change +(documented in Maintenance). + +oxfmt's config-file detection (from conform source): `.oxfmtrc.json`, +`.oxfmtrc.jsonc`, `oxfmt.config.ts`, `vite.config.{ts,js}`. + +## Current state + +### `lua/plugins/web-linter.lua` (the regression — full file) + +```lua +local linters = { + -- web + 'biome', +} + +return { + { + 'mfussenegger/nvim-lint', + ft = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact' }, + opts = { + -- Events to trigger linter + events = { "BufWritePost", "BufReadPost", "InsertLeave" }, + linters_by_ft = { + javascript = { 'biome' }, + javascriptreact = { 'biome' }, + typescript = { 'biome' }, + typescriptreact = { 'biome' }, + }, + }, + config = function(_, opts) + local mason_registry = require('mason-registry') + for _, linter in ipairs(linters) do + if not mason_registry.is_installed(linter) then + vim.cmd('MasonInstall ' .. linter) + end + end + + local lint = require('lint') + lint.linters_by_ft = opts.linters_by_ft + + vim.api.nvim_create_autocmd( + opts.events, + { + callback = function() + lint.try_lint() + end + } + ) + end, + dependencies = { + { 'williamboman/mason.nvim' } + } + }, +} +``` + +(The `opts.events` wiring from plan 003 is CORRECT — keep it. Only the linter +name and the Mason install list change.) + +### `lua/plugins/lspconfig.lua` conform block (lines ~196–207) + +```lua + conform.setup({ + formatters_by_ft = { + lua = { "stylua" }, + javascript = { "biome-check" }, + javascriptreact = { "biome-check" }, + typescript = { "biome" }, + typescriptreact = { "biome-check" }, + java = { "google-java-format" }, + }, + format_on_save = { + timeout_ms = 500, + lsp_format = "fallback", + }, + }) +``` + +Note: `typescript = { "biome" }` is inconsistent with the others (`biome-check`) +— this is deferred finding TECH-7. The oxc swap makes TECH-7 moot (all four +filetypes get the same formatter). + +### `lua/plugins/lspconfig.lua` formatter install list (line ~46) + +```lua +local mason_formatters = { + ensure_installed = { "biome", "stylua" }, +} +``` + +This drives `MasonInstallAll`. With the formatter swap, `biome` → `oxfmt`. +`stylua` (Lua) and `google-java-format` (Java, conform-only, not in this list) +are unchanged. + +### `lua/plugins/lspconfig.lua` `mason_lsp_mapping` (lines ~28–40) + +Contains `stylua = "stylua"` but NOT `biome` — biome was never in the LSP +mapping (it's a formatter/linter, not an LSP server). **Do not add oxfmt or +oxlint here either** — this table maps LSP server names to Mason packages; +formatters/linters are separate. (This is the namespace distinction plan 003 +got wrong on the linter side; be precise here.) + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| No `biome`/`biomejs` left in web-linter | `grep -nE 'biome' lua/plugins/web-linter.lua` | no matches | +| oxlint present in web-linter | `grep -n 'oxlint' lua/plugins/web-linter.lua` | ≥4 matches (install list + 4 by_ft) | +| No `biome` left in conform formatters | `grep -nE 'biome' lua/plugins/lspconfig.lua` | only `stylua = "stylua"`-adjacent if any; the 4 biome entries gone | +| oxfmt present in conform | `grep -n 'oxfmt' lua/plugins/lspconfig.lua` | 4 matches + 1 in ensure_installed | +| mason_formatters updated | `grep -n 'ensure_installed' lua/plugins/lspconfig.lua` | shows `{ "oxfmt", "stylua" }` | +| Files parse | see Done criteria | exit 0 each | + +## Scope + +**In scope** (the only files you should modify): +- `lua/plugins/web-linter.lua` — rename `biome` → `oxlint` (4 by_ft entries + the `linters` install list). +- `lua/plugins/lspconfig.lua` — replace the 4 biome conform formatters with `oxfmt`; update `mason_formatters.ensure_installed`. + +**Out of scope** (do NOT touch): +- `lua = { "stylua" }` and `java = { "google-java-format" }` conform entries — + unrelated languages, keep them. +- `mason_lsp_mapping` (LSP server → Mason package map) — oxlint/oxfmt are + NOT LSP servers; they don't belong here. +- `mason_options.ensure_installed` (LSP servers list) — unchanged. +- `nvim-lint` plugin spec structure (ft, opts.events, autocmd, deps) — keep + plan 003's wiring intact; only the names change. +- The `fM` conform-format keymap — unchanged. + +## Git workflow + +- Branch: `advisor/016-oxc-stack` +- Single commit. Message style (conventional commits): + `fix: switch JS/TS lint+format to oxc stack (oxlint + oxfmt), fixes biome regression`. + +## Steps + +### Step 1: Switch the linter to oxlint in `web-linter.lua` + +Change `biome` → `oxlint` in **two places**: the `linters` install list (line 3) +and all four `linters_by_ft` entries (lines 14–17). + +The top of the file becomes: +```lua +local linters = { + -- web + 'oxlint', +} +``` + +The `linters_by_ft` block becomes: +```lua + linters_by_ft = { + javascript = { 'oxlint' }, + javascriptreact = { 'oxlint' }, + typescript = { 'oxlint' }, + typescriptreact = { 'oxlint' }, + }, +``` + +Leave `opts.events`, the config function, the autocmd, and dependencies +unchanged (plan 003's wiring was correct; only the names were wrong). + +**Verify**: +- `grep -nE 'biome' lua/plugins/web-linter.lua` → no matches. +- `grep -c 'oxlint' lua/plugins/web-linter.lua` → 5 (1 install list + 4 by_ft). + +### Step 2: Switch the formatter to oxfmt in `lspconfig.lua` conform block + +Replace the four biome entries in `formatters_by_ft` with `oxfmt`. This also +resolves the TECH-7 inconsistency (all four web filetypes now use the same +formatter). + +Before: +```lua + formatters_by_ft = { + lua = { "stylua" }, + javascript = { "biome-check" }, + javascriptreact = { "biome-check" }, + typescript = { "biome" }, + typescriptreact = { "biome-check" }, + java = { "google-java-format" }, + }, +``` + +After: +```lua + formatters_by_ft = { + lua = { "stylua" }, + javascript = { "oxfmt" }, + javascriptreact = { "oxfmt" }, + typescript = { "oxfmt" }, + typescriptreact = { "oxfmt" }, + java = { "google-java-format" }, + }, +``` + +**Verify**: +- `grep -nE 'biome' lua/plugins/lspconfig.lua` in the conform block → no + `biome-check`/`biome` formatter entries (only `stylua`/`oxfmt`/`google-java-format` remain). +- `grep -c 'oxfmt' lua/plugins/lspconfig.lua` → 5 (4 formatters + 1 in ensure_installed, set next). + +### Step 3: Update the Mason formatter install list + +In `lua/plugins/lspconfig.lua` line ~46, replace `"biome"` with `"oxfmt"`: + +Before: +```lua +local mason_formatters = { + ensure_installed = { "biome", "stylua" }, +} +``` + +After: +```lua +local mason_formatters = { + ensure_installed = { "oxfmt", "stylua" }, +} +``` + +**Verify**: `grep -n 'ensure_installed = { "oxfmt"' lua/plugins/lspconfig.lua` → one match. + +### Step 4: Parse-check and commit + +**Verify**: +```bash +for f in lua/plugins/web-linter.lua lua/plugins/lspconfig.lua; do + nvim --headless -u NONE +"lua local fn,err=loadfile('$f'); assert(fn,err)" +qa && echo "OK $f" || echo "FAIL $f" +done +``` +Both print `OK`. + +- Stage both files. +- Commit: `fix: switch JS/TS lint+format to oxc stack (oxlint + oxfmt), fixes biome regression`. + +**Verify**: `git show --stat HEAD` → exactly two files changed. + +## Test plan + +- **Static (required)**: all grep checks + parse loop above. +- **Linter registration (recommended, requires plugins)**: load the config, + run `:lua print(vim.inspect(vim.tbl_keys(require('lint').linters)))` — + `oxlint` should be present. Open a `.ts` file with a lint error; confirm + diagnostics appear with `source = "oxlint"` and NO "Linter not available" + error (that's the regression proof — gone). +- **Formatter (recommended, requires plugins)**: run `:MasonInstallAll` (or + `:MasonInstall oxlint oxfmt`); open a `.ts` file, run `:ConformInfo` — + `oxfmt` should be the selected formatter. Trigger format (`fM>` or + write — `format_on_save`); confirm it formats without error. +- **Mason orthogonality**: `:MasonInstallAll` installs both `oxlint` and + `oxfmt` (via `mason_formatters.ensure_installed` and the web-linter's own + install loop). Neither tool should be in `mason_lsp_mapping`. + +## Done criteria + +ALL must hold: + +- [ ] `grep -nE 'biome' lua/plugins/web-linter.lua` returns no matches. +- [ ] `grep -c 'oxlint' lua/plugins/web-linter.lua` returns 5. +- [ ] The conform `formatters_by_ft` block has `oxfmt` for all four web + filetypes (no `biome`/`biome-check` entries). +- [ ] `grep -n 'ensure_installed = { "oxfmt"' lua/plugins/lspconfig.lua` returns one match. +- [ ] `lua = { "stylua" }` and `java = { "google-java-format" }` are unchanged. +- [ ] `mason_lsp_mapping` does NOT contain `oxlint` or `oxfmt` (they aren't LSP servers). +- [ ] Both files parse (Step 4 loop prints `OK` each). +- [ ] `git show --stat HEAD` shows only the two in-scope files. +- [ ] `plans/README.md` status row for 016 updated, plan 003 marked superseded, + and TECH-7 marked moot (resolved by the uniform oxfmt swap). + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **`oxlint` is not a registered nvim-lint linter** in your environment. This + was verified against nvim-lint source (`lua/lint/linters/oxlint.lua` exists + at the lazy-lock commit). If a re-check (`:lua print(require('lint').linters.oxlint ~= nil)`) + shows nil, report — the linter file may have been renamed in a newer nvim-lint. + Do not fall back to `biome` (that's the regression); report and let the + operator decide. +- **`oxfmt` is not a registered conform formatter.** Verified in conform source + (`lua/conform/formatters/oxfmt.lua`). If a re-check shows it's gone, report. +- **Mason rejects `oxlint` or `oxfmt` as a package name.** Verified in + `mason-org/mason-registry` (both exist). If `:MasonInstall oxlint oxfmt` + errors, report the error — do not guess alternate names. +- You're tempted to add `oxlint`/`oxfmt` to `mason_lsp_mapping`. Don't — that + table is LSP-server→package only. This is the exact namespace confusion that + caused the original regression. +- Any file at the cited locations doesn't match its excerpt (drift). Report. + +## Maintenance notes + +- **Why this is correct where plan 003 was wrong:** plan 003 conflated the + Mason package name (`biome`) with the nvim-lint linter name (`biomejs`). For + oxc the two namespaces happen to align (`oxlint`/`oxfmt` in both), but the + discipline matters: each namespace was checked independently against its + own source (nvim-lint source for linter names, conform source for formatter + names, mason-registry for package names). +- **oxfmt is pre-1.0.** A future `oxfmt` minor bump may reformat codebases. + If that churn becomes unwanted, revert Step 2's conform entries to + `biome-check`/`biome` (and Step 3's install list back to `biome`) — biome + is stable post-1.0. The linter (oxlint) is independent and unaffected. +- **TECH-7 (the `typescript = { "biome" }` vs `biome-check` inconsistency) + is now moot** — all four web filetypes uniformly use `oxfmt`. Record it + resolved-by-016 in the index. +- **Reviewer focus**: confirm (a) no `biome`/`biomejs` anywhere in the two + files, (b) `oxlint` x5 in web-linter, `oxfmt` x5 in lspconfig, (c) the LSP + mapping table is untouched, (d) `opts.events`/autocmd structure from 003 + preserved. +- **Lesson (on the record)**: plan 003's regression is the negative example + that proves the verification discipline. Every other plan this session that + checked the actual source/behavior (007 digraphs, 010 `package.path`, 013 + wildcard-merge, 015 `before_init`) avoided shipping a bug; plan 003 skipped + the linter-name check and shipped one. Plans 016+ must verify tool names in + EACH namespace they touch. diff --git a/plans/README.md b/plans/README.md index 5e36516..00dbcb7 100644 --- a/plans/README.md +++ b/plans/README.md @@ -30,6 +30,7 @@ deferred — see "Deferred" at the bottom. | 013 | Restructure LSP config into `lsp/.lua` + `after/ftplugin/` (borrow from paketo) | P2 | M | — | DONE (branch `advisor/013-lsp` @ `2afa5c7`, verdict APPROVE) | | 014 | Tier diagnostics (signs/underline/current-line virtual_text; supersedes 012's virtual_lines) | P3 | S | — | DONE (branch `advisor/014-diag` @ `a5fddf1`, verdict APPROVE) | | 015 | Resolve ts_ls Vue plugin path per-attach via `before_init` (fix N2) | P2 | S | — | DONE (branch `advisor/015-vue` @ `c28a188`, verdict APPROVE) | +| 016 | Switch JS/TS lint+format to oxc stack (oxlint + oxfmt); fixes biome regression | P1 | S | — | DONE (branch `advisor/016-oxc` @ `daac270`, verdict APPROVE; supersedes 003) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). @@ -92,6 +93,8 @@ Recorded so they are not lost and not blindly re-audited next time: `typescript = { "biome" }` vs `biome-check` elsewhere): MED confidence it's unintended. Related to but separate from plan 003 (different file, formatter vs linter). + **RESOLVED by plan 016** — the oxc swap makes all four web filetypes use + `oxfmt` uniformly, so the biome/biome-check inconsistency is moot. - **TECH-8 — several `vim.keymap.set` calls omit `desc`**: PARTIALLY planned as 009. Vetting corrected the record: `lspconfig.lua:177` (rust `ca`) and `:260` (`fM`) ALREADY have `desc` — they were false positives. From 3934b3221df555fe142f14f6f9447ed529f514a1 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 21:51:01 +0700 Subject: [PATCH 22/26] feat: add oxlint to MasonInstallAll --- lazy-lock.json | 42 +++++++++++++++++++-------------------- lua/plugins/lspconfig.lua | 6 ++++++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lazy-lock.json b/lazy-lock.json index 048f01b..2087d84 100644 --- a/lazy-lock.json +++ b/lazy-lock.json @@ -1,10 +1,10 @@ { - "99": { "branch": "master", "commit": "4d229141546290746c82ac90f5afc2786865b5f3" }, - "amp.nvim": { "branch": "main", "commit": "3b9ad5ef0328de1b35cc9bfa723a37db5daf9434" }, + "99": { "branch": "master", "commit": "c17422457027c913c76c75a921fca1e623d2678e" }, + "amp.nvim": { "branch": "main", "commit": "b851d97d8e8782e58343608d8de7d9eb3a88090f" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, "blink.compat": { "branch": "main", "commit": "1454f14a8d855a578ceeba77c62538fa1459a67c" }, "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, - "conform.nvim": { "branch": "master", "commit": "dca1a190aa85f9065979ef35802fb77131911106" }, + "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" }, "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, "gruvbox.nvim": { "branch": "main", "commit": "154eb5ff5b96d0641307113fa385eaf0d36d9796" }, @@ -12,28 +12,28 @@ "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, "lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" }, "luvit-meta": { "branch": "main", "commit": "cc9b2d412d2fbd30b94a70cfc214c2a3be27a0a2" }, - "mason.nvim": { "branch": "main", "commit": "e54f5bf5f12c560da31c17eee5b3e1bd369f3ff9" }, - "mini.ai": { "branch": "main", "commit": "a783ce6e1a5b3bc90519f20b51f4a5f6702734af" }, - "mini.basics": { "branch": "main", "commit": "9ad4ffe62474f5f82c88d3b745f47581c51ba592" }, - "mini.comment": { "branch": "main", "commit": "fc87ba6554f182161d9a4bab5017c575571f000f" }, - "mini.diff": { "branch": "main", "commit": "117c301374ab8546891e2b34f63885ea83527432" }, - "mini.files": { "branch": "main", "commit": "f9f0ccbef0fd24eaddc8675accca83fa1f310c3f" }, - "mini.icons": { "branch": "main", "commit": "9c7b1b90b15bdd69c52f6e31889dbc9987c30ec4" }, - "mini.notify": { "branch": "main", "commit": "ef35a3ec68399398f097d0c1736c343a45cb7406" }, - "mini.nvim": { "branch": "main", "commit": "17c448b0f3f29c0857a3436fc64e1d7cb9267ec3" }, - "mini.pairs": { "branch": "main", "commit": "30cf2f01c4aaa2033db67376b9924fa2442c05d6" }, - "mini.starter": { "branch": "main", "commit": "35b018a035794e341ac01cb2091bbd71b3f823d0" }, - "mini.statusline": { "branch": "main", "commit": "e9e5c147385e5e0310ab79162dd08d0465e96d83" }, - "mini.trailspace": { "branch": "main", "commit": "ae2fd422564c6e781caf6545355ca6051e20fa26" }, - "nvim-jdtls": { "branch": "master", "commit": "77ccaeb422f8c81b647605da5ddb4a7f725cda90" }, - "nvim-lint": { "branch": "master", "commit": "665525810630701b84181e4d9eefd24b49845b29" }, - "nvim-lspconfig": { "branch": "master", "commit": "75e49cfa588a89ca667d767c0afef3ceac205faa" }, + "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, + "mini.ai": { "branch": "main", "commit": "d73c36349aa7b0bab5f77ad71701a1d42211a1df" }, + "mini.basics": { "branch": "main", "commit": "b5be271ebe862ca0570e450976799554e585d16e" }, + "mini.comment": { "branch": "main", "commit": "4677392f091e8b5c18d4b535130220a6d1da4aca" }, + "mini.diff": { "branch": "main", "commit": "0743d26bd858ebe32efcf5c86a91a422a000f273" }, + "mini.files": { "branch": "main", "commit": "a5689dae6b732955e33eec225b798d6815063179" }, + "mini.icons": { "branch": "main", "commit": "e56797f90192d81f1fda02e662fc3e8e3d775027" }, + "mini.notify": { "branch": "main", "commit": "7d3832e369853eaf1a2d25dede9db34ae5a809e9" }, + "mini.nvim": { "branch": "main", "commit": "fe1aa97c8c7408f3def6336f7082237e8cf67833" }, + "mini.pairs": { "branch": "main", "commit": "4a014143fcb4e9df26198ccb3ecff3b9e77a048c" }, + "mini.starter": { "branch": "main", "commit": "0575c96206d63fd98d7f786df78dc225bf847d95" }, + "mini.statusline": { "branch": "main", "commit": "b5547f44560dae3ccd81f914256fa6f705837022" }, + "mini.trailspace": { "branch": "main", "commit": "22653218f1aedc9bf306c8b4e8ec2c8a575f6fae" }, + "nvim-jdtls": { "branch": "master", "commit": "6e9d953f0b82bccdb834cfde0e893f3119c22592" }, + "nvim-lint": { "branch": "master", "commit": "01c9842c089069ab497430159312b2c8868a4590" }, + "nvim-lspconfig": { "branch": "master", "commit": "bfcc0171a43f22afa61d927ffe9fcb6cb85dc99e" }, "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, - "nvim-web-devicons": { "branch": "master", "commit": "2795c26c916bb3c57dde308b82be51971fa92747" }, + "nvim-web-devicons": { "branch": "master", "commit": "dfbfaa967a6f7ec50789bead7ef87e336c1fa63c" }, "rustaceanvim": { "branch": "master", "commit": "d50597d482a6f44ddfc54d1af2f69f052053b4de" }, "sidekick.nvim": { "branch": "main", "commit": "208e1c5b8170c01fd1d07df0139322a76479b235" }, "smear-cursor.nvim": { "branch": "main", "commit": "9e9378d6ee34bb3782e0e8c63d9ec8ca618b479b" }, - "snacks.nvim": { "branch": "main", "commit": "ad9ede6a9cddf16cedbd31b8932d6dcdee9b716e" }, + "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" }, "supermaven-nvim": { "branch": "main", "commit": "07d20fce48a5629686aefb0a7cd4b25e33947d50" }, "toggleterm.nvim": { "branch": "main", "commit": "50ea089fc548917cc3cc16b46a8211833b9e3c7c" }, "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index 52edbd9..e51a64b 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -42,6 +42,10 @@ local mason_lsp_mapping = { ruby_lsp = "ruby-lsp", } +local mason_linters = { + ensure_installed = { "oxlint" }, +} + local mason_formatters = { ensure_installed = { "oxfmt", "stylua" }, } @@ -110,6 +114,8 @@ return { .. table.concat(mason_formatters.ensure_installed, " ") .. " " .. table.concat(mason_servers, " ") + .. " " + .. table.concat(mason_linters.ensure_installed, " ") ) end, {}) end, From 4e01b02e28dc87e1e44e4c683f223dd40cb353e2 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 22:00:30 +0700 Subject: [PATCH 23/26] feat: add Python support via astral stack (ruff + ty) --- lsp/ruff.lua | 16 ++++++++++++++++ lua/plugins/lspconfig.lua | 15 +++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 lsp/ruff.lua diff --git a/lsp/ruff.lua b/lsp/ruff.lua new file mode 100644 index 0000000..a73c13d --- /dev/null +++ b/lsp/ruff.lua @@ -0,0 +1,16 @@ +-- ruff (Astral's Python linter) LSP config. +-- Base config (cmd, filetypes, root_markers) comes from nvim-lspconfig's +-- shipped lsp/ruff.lua — this file only overrides what differs. +-- Shared on_attach + capabilities come from the wildcard default in +-- lua/plugins/lspconfig.lua. +-- +-- Disable ruff LSP's organizeImports so it doesn't conflict with conform's +-- ruff_organize_imports formatter (the single source of truth for import +-- sorting on save). ruff still provides lint diagnostics + code actions. +return { + init_options = { + settings = { + organizeImports = false, + }, + }, +} diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index e51a64b..d0cbefe 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -4,7 +4,7 @@ local treesitter_options = { "javascript", "lua", "markdown", - -- "python", + "python", "rust", -- "svelte", "typescript", @@ -21,10 +21,9 @@ local mason_options = { ensure_installed = { "lua_ls", "ts_ls", - -- "pyright", - -- "ruff", + "ruff", + "ty", "rust_analyzer", - -- "svelte", "gopls", "ruby_lsp", }, @@ -33,11 +32,10 @@ local mason_options = { local mason_lsp_mapping = { gopls = "gopls", lua_ls = "lua-language-server", - -- pyright = "pyright", - -- ruff = "ruff", + ruff = "ruff", + ty = "ty", rust_analyzer = "rust-analyzer", stylua = "stylua", - -- svelte = "svelte-language-server", ts_ls = "typescript-language-server", ruby_lsp = "ruby-lsp", } @@ -85,7 +83,7 @@ return { -- Enable the loop-managed servers. rust_analyzer is intentionally -- omitted: rustaceanvim manages it via vim.g.rustaceanvim. -- (Verified: vim.lsp.enable accepts a list.) - vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }) + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp", "ruff", "ty" }) end, dependencies = { { "saghen/blink.cmp" }, @@ -207,6 +205,7 @@ return { typescript = { "oxfmt" }, typescriptreact = { "oxfmt" }, java = { "google-java-format" }, + python = { "ruff_organize_imports", "ruff_format" }, }, format_on_save = { timeout_ms = 500, From 93770d873f78dc23e1e6b0a1ebbb559887994e5a Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 22:00:43 +0700 Subject: [PATCH 24/26] docs: add plan 017 (Python astral stack) --- plans/017-python-astral-stack.md | 433 +++++++++++++++++++++++++++++++ plans/README.md | 1 + 2 files changed, 434 insertions(+) create mode 100644 plans/017-python-astral-stack.md diff --git a/plans/017-python-astral-stack.md b/plans/017-python-astral-stack.md new file mode 100644 index 0000000..0ae3050 --- /dev/null +++ b/plans/017-python-astral-stack.md @@ -0,0 +1,433 @@ +# Plan 017: Add Python support via the astral.sh stack (ruff + ty) + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 3934b32..HEAD -- lua/plugins/lspconfig.lua lua/plugins/web-linter.lua` +> If either file changed since this plan was written, compare the "Current +> state" excerpts against the live code before proceeding; on a mismatch, +> treat it as a STOP condition. + +## Status + +- **Priority**: P2 (new language support) +- **Effort**: S +- **Risk**: LOW (additive; uses lspconfig-provided defaults + one override) +- **Depends on**: none (works on top of plan 013's native `lsp/` pattern and + your post-pull `mason_linters`/`mason_formatters` split) +- **Category**: feature +- **Planned at**: commit `3934b32`, 2026-06-20 + +## Why this matters + +The repo currently has **no Python support** — `python` is commented out of +treesitter, `pyright`/`ruff` are commented out of the Mason lists, and no +Python LSP is enabled. This plan adds the **astral.sh stack** end-to-end: + +- **`ty`** (Astral's type checker) as an LSP — fast types + hover + diagnostics +- **`ruff`** (Astral's linter) as an LSP — linting + code actions + diagnostics +- **`ruff_format` + `ruff_organize_imports`** in conform — format-on-save +- **`python`** in treesitter — syntax highlighting + +This matches what `baseddxyz/paketo` (the same author's other config) already +does (`mise.toml` ships `ty`; `plugin/40_plugins.lua` enables `ty` + `ruff`), +and the existing commented-out `pyright`/`ruff` lines in this repo signal the +same intent — now resolved with the more cohesive Astral-native choice. + +### Verified (all three namespaces checked against real sources — no assumptions) + +| Tool | lspconfig server | conform name | Mason package | +|---|---|---|---| +| ty | `ty` (`cmd={'ty','server'}`, `filetypes={'python'}`, root_markers include `ty.toml`/`pyproject.toml`) ✓ | — | `ty` (`pkg:pypi/ty@0.0.51`) ✓ | +| ruff | `ruff` (`cmd={'ruff','server'}`, `filetypes={'python'}`, root_markers `pyproject.toml`/`ruff.toml`/`.ruff.toml`) ✓ | (also `ruff_format`/`ruff_organize_imports`) | `ruff` (`pkg:pypi/ruff`) ✓ | +| ruff format | — | `ruff_format` (runs `ruff format`) ✓ | (same `ruff` binary) | +| ruff imports | — | `ruff_organize_imports` (runs `ruff check --fix --select=I001`) ✓ | (same `ruff` binary) | + +**`ty` is alpha** (`0.0.x` versioning — Astral's newest tool). It type-checks +fast and the LSP works, but expect rough edges and behavior changes between +versions. For a personal config that's fine and Astral-consistent; reverting +to pyright later is trivial (swap `"ty"` → `"pyright"` in the enable list). + +### Why no redundant `lsp/ruff.lua`/`lsp/ty.lua` files (corrected from plan 013's pattern) + +Plan 013 created `lsp/{lua_ls,gopls,ruby_lsp}.lua` with `return {}` — verified +later to be **redundant no-ops** because nvim-lspconfig already ships native +`lsp/.lua` configs (deep-merged by Neovim's loader; lspconfig provides +`cmd`/`filetypes`/`root_markers` for free). This plan follows the corrected +discipline: **only create `lsp/.lua` when there's something real to +override.** + +- `lsp/ruff.lua` — created here, with a real override: disable ruff LSP's + `organizeImports` so it doesn't fight conform's `ruff_organize_imports` on + save (the same reason the old commented pyright had `disableOrganizeImports`). +- `lsp/ty.lua` — **not created.** Its lspconfig defaults are correct; nothing + to override. + +### How ty + ruff coexist + +Both attach to `.py` files. ruff gives lint diagnostics + code actions; ty +gives type diagnostics + types. They complement each other — `:LspInfo` will +show two clients per Python buffer, which is expected and correct (paketo +runs the same combo). + +## Current state + +### Mason install lists (`lua/plugins/lspconfig.lua`, post pull `3934b32`) + +```lua +local mason_options = { + ensure_installed = { + "lua_ls", + "ts_ls", + -- "pyright", + -- "ruff", + "rust_analyzer", + -- "svelte", + "gopls", + "ruby_lsp", + }, +} + +local mason_lsp_mapping = { + gopls = "gopls", + lua_ls = "lua-language-server", + -- pyright = "pyright", + -- ruff = "ruff", + rust_analyzer = "rust-analyzer", + stylua = "stylua", + -- svelte = "svelte-language-server", + ts_ls = "typescript-language-server", + ruby_lsp = "ruby-lsp", +} + +local mason_linters = { + ensure_installed = { "oxlint" }, +} +local mason_formatters = { + ensure_installed = { "oxfmt", "stylua" }, +} +``` + +### Enable list + wildcard (`lua/plugins/lspconfig.lua`, line ~88) + +```lua + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }) +``` + +### Conform formatters (`lua/plugins/lspconfig.lua`, ~line 199, post plan 016) + +```lua + formatters_by_ft = { + lua = { "stylua" }, + javascript = { "oxfmt" }, + javascriptreact = { "oxfmt" }, + typescript = { "oxfmt" }, + typescriptreact = { "oxfmt" }, + java = { "google-java-format" }, + }, +``` + +### Treesitter (`lua/plugins/lspconfig.lua`, top of file) + +```lua +local treesitter_options = { + ensure_installed = { + "bash", + "javascript", + "lua", + "markdown", + -- "python", + "rust", + -- "svelte", + "typescript", + "go", + "ruby", + "java", + }, +``` + +### `MasonInstallAll` wiring (the three-list pattern you established in `3934b32`) + +The command concatenates `mason_formatters.ensure_installed`, then +`mason_servers` (from `mason_lsp_mapping`), then `mason_linters.ensure_installed`. +So adding to `mason_options.ensure_installed` + `mason_lsp_mapping` is what +gets a new LSP installed; adding to `mason_linters` gets a new linter installed. + +### Repo conventions to match + +- Three-list Mason pattern: `mason_options` (LSP), `mason_linters`, `mason_formatters`. +- `lsp/.lua` only when overriding (corrected discipline from plan 013's review). +- Tabs for indentation. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---------|---------|---------------------| +| ruff in mason_options | `grep -n '"ruff"' lua/plugins/lspconfig.lua` | one match in mason_options | +| ty in mason_options | `grep -n '"ty"' lua/plugins/lspconfig.lua` | one match in mason_options | +| enable list has both | `grep -n 'vim.lsp.enable({' lua/plugins/lspconfig.lua` | includes `"ruff", "ty"` | +| ruff override file exists | `test -f lsp/ruff.lua && echo OK` | OK | +| NO lsp/ty.lua (not redundant) | `test ! -e lsp/ty.lua && echo OK` | OK | +| conform has python entry | `grep -n 'python' lua/plugins/lspconfig.lua` | `python = { "ruff_organize_imports", "ruff_format" }` | +| treesitter has python | `grep -n '"python"' lua/plugins/lspconfig.lua` | one match (uncommented) | +| Files parse | see Done criteria loop | exit 0 each | + +## Scope + +**In scope** (the only files you should create or modify): +- `lua/plugins/lspconfig.lua` — enable `ruff`+`ty`; add them to `mason_options` + `mason_lsp_mapping`; add `python` to treesitter; add `python` to conform. +- `lsp/ruff.lua` (new) — the one real override (disable organize-imports). + +**Out of scope** (do NOT touch): +- `lua/plugins/web-linter.lua` — JS/TS linting; unrelated to Python. +- `lua/plugins/java.lua`, rustaceanvim spec — other languages. +- The wildcard defaults block — the shared `on_attach`/`capabilities` from + plan 013 apply to ruff/ty automatically (they're enabled servers). +- Do NOT create `lsp/ty.lua` — redundant; lspconfig provides correct defaults. +- Do NOT wire nvim-lint for Python — ruff LSP already provides diagnostics; + doubling up causes duplicate diagnostics (the lesson from the biome regression). + +## Git workflow + +- Branch: `advisor/017-python-astral` +- Single commit. Message style (conventional commits): + `feat: add Python support via astral stack (ruff + ty)`. + +## Steps + +### Step 1: Enable ruff + ty in the enable list + +In `lua/plugins/lspconfig.lua` (~line 88), add `ruff` and `ty` to the +`vim.lsp.enable({...})` list: + +Before: +```lua + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp" }) +``` + +After: +```lua + vim.lsp.enable({ "lua_ls", "ts_ls", "gopls", "ruby_lsp", "ruff", "ty" }) +``` + +**Verify**: `grep -n 'vim.lsp.enable({' lua/plugins/lspconfig.lua` → the list +ends with `..., "ruff", "ty" }`. + +### Step 2: Add ruff + ty to the Mason install lists + +In `mason_options.ensure_installed`, replace the commented `-- "pyright",` and +`-- "ruff",` block. Since you're switching to the astral stack (no pyright), +uncomment `"ruff"` and add `"ty"`. Remove the `pyright` line (the astral stack +uses ty for types). Result: + +```lua +local mason_options = { + ensure_installed = { + "lua_ls", + "ts_ls", + "ruff", + "ty", + "rust_analyzer", + "gopls", + "ruby_lsp", + }, +} +``` + +(Also remove the now-stale `-- "svelte",` if it bothers you, or leave it. +Drop the commented `pyright`.) + +In `mason_lsp_mapping`, replace the commented pyright/ruff lines with the +real mappings: + +```lua +local mason_lsp_mapping = { + gopls = "gopls", + lua_ls = "lua-language-server", + ruff = "ruff", + ty = "ty", + rust_analyzer = "rust-analyzer", + stylua = "stylua", + ts_ls = "typescript-language-server", + ruby_lsp = "ruby-lsp", +} +``` + +(The Mason package name == lspconfig server name for both — verified. `ruff` +installs the `ruff` binary that serves the ruff LSP AND the conform +formatters; `ty` installs the `ty` binary.) + +**Verify**: +- `grep -n '"ruff"' lua/plugins/lspconfig.lua` → ≥2 matches (mason_options + mason_lsp_mapping). +- `grep -n '"ty"' lua/plugins/lspconfig.lua` → ≥2 matches (same). + +### Step 3: Create `lsp/ruff.lua` (the one real override) + +Create `lsp/ruff.lua` (repo root). Its single purpose: disable ruff LSP's +import-organizing so conform's `ruff_organize_imports` (Step 5) is the single +source of truth for import sorting on save (otherwise both try to do it and +you get conflicts/duplicate work). + +```lua +-- ruff (Astral's Python linter) LSP config. +-- Base config (cmd, filetypes, root_markers) comes from nvim-lspconfig's +-- shipped lsp/ruff.lua — this file only overrides what differs. +-- Shared on_attach + capabilities come from the wildcard default in +-- lua/plugins/lspconfig.lua. +-- +-- Disable ruff LSP's organizeImports so it doesn't conflict with conform's +-- ruff_organize_imports formatter (the single source of truth for import +-- sorting on save). ruff still provides lint diagnostics + code actions. +return { + init_options = { + settings = { + organizeImports = false, + }, + }, +} +``` + +This merges on top of lspconfig's `lsp/ruff.lua` (verified merge behavior — +the same deep-extend mechanism that confirmed plan 013's `{}` files were +harmless no-ops). + +**Verify**: `test -f lsp/ruff.lua && grep -q 'organizeImports' lsp/ruff.lua && echo OK` → OK. + +### Step 4: Add `python` to treesitter + +In `treesitter_options.ensure_installed`, uncomment the `"python"` line: + +Before: +```lua + "markdown", + -- "python", + "rust", +``` + +After: +```lua + "markdown", + "python", + "rust", +``` + +**Verify**: `grep -n '"python"' lua/plugins/lspconfig.lua` → one match (no `--`). + +### Step 5: Add `python` to conform formatters + +In the conform `formatters_by_ft` block, add a `python` entry. Astral's +documented order is **organize imports first, then format**: + +```lua + formatters_by_ft = { + lua = { "stylua" }, + javascript = { "oxfmt" }, + javascriptreact = { "oxfmt" }, + typescript = { "oxfmt" }, + typescriptreact = { "oxfmt" }, + java = { "google-java-format" }, + python = { "ruff_organize_imports", "ruff_format" }, + }, +``` + +**Verify**: `grep -n 'python = {' lua/plugins/lspconfig.lua` → +`python = { "ruff_organize_imports", "ruff_format" },`. + +### Step 6: Parse-check and commit + +**Verify**: +```bash +for f in lua/plugins/lspconfig.lua lsp/ruff.lua; do + nvim --headless -u NONE +"lua local fn,err=loadfile('$f'); assert(fn,err)" +qa && echo "OK $f" || echo "FAIL $f" +done +``` +Both print `OK`. + +- Stage `lua/plugins/lspconfig.lua` and `lsp/ruff.lua`. +- Commit: `feat: add Python support via astral stack (ruff + ty)`. + +**Verify**: `git show --stat HEAD` → exactly two files (1 modified, 1 new). + +## Test plan + +- **Static (required)**: all grep checks + parse loop. +- **Install (recommended)**: run `:MasonInstallAll` — it should install both + `ruff` and `ty` (via the three-list wiring you set up). Confirm with + `:Mason` that both appear as installed. +- **LSP attach (recommended)**: open a `.py` file with some type/lint issues; + `:LspInfo` should show BOTH `ruff` and `ty` attached. Hover (`K`) on a + function should show type info from `ty`. Lint diagnostics should be + sourced from `ruff`. +- **Format-on-save (recommended)**: with `format_on_save` on, saving a `.py` + file should run `ruff_organize_imports` then `ruff_format` (verify with + `:ConformInfo`). Imports get sorted, code gets formatted. +- **Type checking (recommended)**: a deliberate type error (e.g. + `x: int = "string"`) should produce a diagnostic from `ty`. + +## Done criteria + +ALL must hold: + +- [ ] `vim.lsp.enable({...})` list ends with `..., "ruff", "ty" }`. +- [ ] `mason_options.ensure_installed` contains `"ruff"` and `"ty"` (uncommented); `pyright` is gone. +- [ ] `mason_lsp_mapping` contains `ruff = "ruff"` and `ty = "ty"` (no `pyright`). +- [ ] `test -f lsp/ruff.lua` succeeds and it contains `organizeImports = false`. +- [ ] `test ! -e lsp/ty.lua` succeeds (NOT created — redundant). +- [ ] `treesitter_options.ensure_installed` has `"python"` uncommented. +- [ ] conform `formatters_by_ft` has + `python = { "ruff_organize_imports", "ruff_format" },`. +- [ ] Both files parse (Step 6 loop prints `OK`). +- [ ] `git show --stat HEAD` shows only `lua/plugins/lspconfig.lua` (modified) and `lsp/ruff.lua` (new). +- [ ] `lua/plugins/web-linter.lua` is NOT modified (Python is separate from JS/TS linting). +- [ ] `plans/README.md` status row for 017 updated. + +## STOP conditions + +Stop and report back (do not improvise) if: + +- **`ruff` or `ty` is not a valid `vim.lsp.enable` target** in your + environment. Both are verified as lspconfig-shipped native `lsp/.lua` + configs. If `:LspInfo` or `:checkhealth lspconfig` doesn't recognize them, + report — your lspconfig may be too old (the post-pull lazy-lock pins a recent + `nvim-lspconfig`; the lockfile bump in `3934b32` matters here). +- **`ruff_format` or `ruff_organize_imports` is not a registered conform + formatter.** Verified in conform source. If `:ConformInfo` doesn't list them, + report — conform version may differ. +- **Mason rejects `ruff` or `ty` as a package.** Verified in mason-registry. + If `:MasonInstall ruff ty` errors, report the error. +- **You're tempted to create `lsp/ty.lua`.** Don't — its defaults are correct. + Only create it if you find a real setting to override, and report first. +- **You're tempted to add `ruff` to `mason_linters` (the oxlint list).** Don't — + `ruff` here is an LSP server (linter via LSP), not a standalone nvim-lint + linter. It goes in `mason_options` + `mason_lsp_mapping`. This is the + namespace discipline from plan 016. +- Any file at the cited locations doesn't match its excerpt (drift). Report. + +## Maintenance notes + +- **Why `lsp/ruff.lua` exists but `lsp/ty.lua` doesn't:** ruff needs the + `organizeImports = false` override to avoid fighting conform; ty has nothing + to override. This is the corrected discipline from plan 013's review (don't + create redundant `{}` files — let lspconfig's shipped defaults provide the + base). +- **ruff LSP serves linting; do NOT also wire nvim-lint for Python.** Double-wiring + causes duplicate diagnostics. nvim-lint stays JS/TS-only (oxlint). +- **ty is alpha.** If its rough edges bother you, swap `"ty"` → `"pyright"` + in the enable list + `mason_options`/`mason_lsp_mapping` (and add a + `lsp/pyright.lua` with `disableOrganizeImports = true` + the + `python.analysis.ignore = { "*" }` pattern you previously had commented out). + One-line conceptual change. +- **The two-LSP-per-buffer pattern (ruff + ty) is intentional and matches + paketo.** Don't try to collapse to one — they do different jobs (lint vs types). +- **Reviewer focus**: confirm (a) enable list has both, (b) mason_options + + mason_lsp_mapping have both with correct names, (c) `lsp/ruff.lua` has ONLY + the organizeImports override (not a full cmd/filetypes repeat), (d) no + `lsp/ty.lua`, (e) web-linter.lua untouched. +- **Lesson**: this plan applies the corrected plan-013 discipline (no + redundant `{}` files) and the plan-016 namespace discipline (ruff is an LSP + here, so it goes in mason_options/mason_lsp_mapping, not mason_linters). + Both lessons came from earlier mistakes this session. diff --git a/plans/README.md b/plans/README.md index 00dbcb7..6c68ff3 100644 --- a/plans/README.md +++ b/plans/README.md @@ -31,6 +31,7 @@ deferred — see "Deferred" at the bottom. | 014 | Tier diagnostics (signs/underline/current-line virtual_text; supersedes 012's virtual_lines) | P3 | S | — | DONE (branch `advisor/014-diag` @ `a5fddf1`, verdict APPROVE) | | 015 | Resolve ts_ls Vue plugin path per-attach via `before_init` (fix N2) | P2 | S | — | DONE (branch `advisor/015-vue` @ `c28a188`, verdict APPROVE) | | 016 | Switch JS/TS lint+format to oxc stack (oxlint + oxfmt); fixes biome regression | P1 | S | — | DONE (branch `advisor/016-oxc` @ `daac270`, verdict APPROVE; supersedes 003) | +| 017 | Add Python support via astral stack (ruff + ty) | P2 | S | — | DONE (branch `advisor/017-python` @ `4e01b02`, verdict APPROVE) | Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale). From 75349f0e739fadb2f5227b77c155fc4dbbce1249 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sat, 20 Jun 2026 22:07:04 +0700 Subject: [PATCH 25/26] chore: remove leftover commented svelte treesitter entry --- lua/plugins/lspconfig.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/plugins/lspconfig.lua b/lua/plugins/lspconfig.lua index d0cbefe..2508902 100644 --- a/lua/plugins/lspconfig.lua +++ b/lua/plugins/lspconfig.lua @@ -6,7 +6,6 @@ local treesitter_options = { "markdown", "python", "rust", - -- "svelte", "typescript", "go", "ruby", From 83215fa8c028a828185684c495a0633e73bb01b7 Mon Sep 17 00:00:00 2001 From: qapquiz Date: Sun, 21 Jun 2026 15:24:54 +0700 Subject: [PATCH 26/26] fix: set conceallevel globally instead of on transient startup buffer Fixes BUG-5 (deferred). vim.opt_local.conceallevel at startup only affected the empty startup buffer; vim.opt sets a global default inherited by all windows. --- lua/vim-options.lua | 2 +- plans/README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/vim-options.lua b/lua/vim-options.lua index 4cedc5e..70a4ec6 100644 --- a/lua/vim-options.lua +++ b/lua/vim-options.lua @@ -49,4 +49,4 @@ vim.diagnostic.config({ update_in_insert = false, }) -vim.opt_local.conceallevel = 1 +vim.opt.conceallevel = 1 diff --git a/plans/README.md b/plans/README.md index 6c68ff3..41c0302 100644 --- a/plans/README.md +++ b/plans/README.md @@ -87,6 +87,7 @@ Recorded so they are not lost and not blindly re-audited next time: only affects the transient startup buffer; intent was a global default. Low impact, trivial fix (`vim.opt.conceallevel = 1`). Fold into any future `vim-options.lua` edit. + **RESOLVED** — switched to `vim.opt.conceallevel = 1` in `lua/vim-options.lua:52`. - **TECH-6 — ~558 lines (33%) of commented-out "alternative palette" code** (`ai.lua`: 257, `colorscheme.lua`: 145): best done *after* the maintainer decides the AI-stack and theme direction, otherwise it just regrows.