feat: discovery-first tool resolution (make Mason optional) - #26
feat: discovery-first tool resolution (make Mason optional)#26charliie-dev wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a37d44f765
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Implements a shared “discovery-first” tool resolution layer across the config so executables found on $PATH are used immediately, Mason becomes an optional installer backend, and missing/unknown tools are aggregated into a single actionable warning per subsystem.
Changes:
- Add a new shared resolver (
modules.utils.tools) that drives discovery → optional Mason install → aggregated warnings + retry hooks. - Refactor LSP, DAP, conform formatters, and nvim-lint linters to use the shared resolver and to avoid loading Mason unless needed.
- Update settings and filetype/schema plumbing (e.g.,
yaml.githubdetection + schema/LS claims) to support the new resolution model.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vim.yml | Teach Selene about package.searchpath (LuaJIT extension) used by the new resolver. |
| lua/modules/utils/tools.lua | New resolver core: executable discovery, optional Mason integration, install tracking, aggregated warnings, and :ToolsRetry. |
| lua/modules/utils/init.lua | Remove register_server helper in favor of the new LSP resolution/enable flow. |
| lua/modules/utils/dap.lua | Add shared attach endpoint validation helper and export it alongside existing helpers. |
| lua/modules/plugins/tool.lua | Make mason-nvim-dap lazy/optional instead of a hard dependency of nvim-dap. |
| lua/modules/configs/tool/dap/init.lua | Refactor DAP setup to use discovery-first resolution, lazy Mason mapping usage, and robust client-config loading. |
| lua/modules/configs/tool/dap/clients/python.lua | Switch python adapter to discovery-first debugpy resolution (PATH shim → import-probe cascade) + config validation. |
| lua/modules/configs/tool/dap/clients/lldb.lua | Resolve LLDB adapter binary discovery-first (lldb-dap/lldb-vscode). |
| lua/modules/configs/tool/dap/clients/delve.lua | Use dlv dap directly with remote-attach support and discovery-first provisioning signaling. |
| lua/modules/configs/tool/dap/clients/codelldb.lua | Self-validate codelldb availability at config time using the shared resolver. |
| lua/modules/configs/completion/servers/yamlls.lua | Expand YAML schema configuration and add a bounded prune to prevent Traefik v2/v3 schema conflicts; explicitly claim dotted filetypes. |
| lua/modules/configs/completion/servers/shuck.lua | Update comments/semantics to align shuck with discovery-first $PATH resolution. |
| lua/modules/configs/completion/servers/gh_actions_ls.lua | Add server override to explicitly claim yaml.github (and yaml) filetypes. |
| lua/modules/configs/completion/nvim-lint.lua | Resolve linter deps discovery-first against nvim-lint’s registry, add late reload handling, and add a parity sweep. |
| lua/modules/configs/completion/mason.lua | Remove ensure-install loop; keep Mason as UI + event hand-off to the resolver. |
| lua/modules/configs/completion/mason-lspconfig.lua | Major LSP refactor: recorded user overrides, discovery-first dep resolution, per-filetype batching, and sweep backstop. |
| lua/modules/configs/completion/lsp.lua | Reorder flow: setup machinery → apply user overrides → resolve deps discovery-first → start LSP. |
| lua/modules/configs/completion/conform.lua | Factor autoformat gating predicate and add discovery-first formatter dep resolution against conform’s registry. |
| lua/core/settings.lua | Consolidate LSP deps (remove external_lsp_deps), switch formatter/linter dep naming to subsystem names, add tool_install_timeout, and add migration warning. |
| ftdetect/github.lua | Add dotted yaml.github filetype detection for GitHub Actions workflows to key tooling off it. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cfe347f to
26d30cb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
lua/modules/utils/dap.lua:99
modules.utils.dapnow returns a proxy table whose__indexalways produces a double-thunk (meant forinput_*()helpers). This breaks callers that expect an eager value likeutils.get_env()(e.g. LLDB client config setsenv = utils.get_env()):get_env()will currently return a function instead of the env table. Exportget_envas a concrete function (likeattach_endpoint) so it bypasses the__indexthunking.
local export = setmetatable({}, {
-- Lazy nullary double-thunk: `program = utils.input_file_path()` hands
-- nvim-dap a zero-arg closure. Any key reached through this __index
-- DISCARDS call arguments — functions that take arguments must be
-- CONCRETE keys on the export (raw hits bypass __index).
__index = function(_, key)
return function()
return function()
return M[key]()
end
end
end,
})
export.attach_endpoint = M.attach_endpoint
return export
Add modules.utils.tools — the $PATH → Mason-install → aggregated-warning
resolver (M.resolve) shared by LSP, formatters, linters, and DAP, with Mason
as an optional backend. Includes the missing-tool collector (reason upgrades,
timeout-placeholder retraction, attempted-and-failed vs typo classification),
the install hand-off / :ToolsRetry paths (phase-1 $PATH failures stay
reachable), per-tick registry/mason-root memoization read live to avoid
latching, $MASON read live, and the shared parity-sweep delay. Removes the
now-unused utils.register_server.
Folds subsequent hardening and review fixes: an empty-string dep name is
classified invalid (not a blank dep); package.searchpath is guarded for
non-LuaJIT nvim builds (degrade to vim.loader.find); superseded resolver
sessions drop on config re-source (drop_sessions, per-consumer); static_binaries
specs skip phase 2's identical $PATH re-probe; the collector retracts a standing
missing-report once a late configure succeeds (coalesced corrective INFO when
the warning was already shown — a slow install no longer strands a stale
timeout warning); user-config load errors classify by module existence instead
of message sniffing; luadoc fun types are parenthesized so lua_ls parses the
spec contracts, and load_plugin merges over `opts or {}` per its documented
nil|table contract.
Resolve formatter_deps through the shared resolver against conform's own registry (deferred off the BufWritePre tick), classifying broken vs unknown vs per-buffer-unverifiable configs honestly. Function-form commands are evaluated at probe time so a node_modules copy passes and the bare-name fallback stays in the install/warn contract. autoformat_allowed groups the save-time gates. Folds later fixes: the superseded-session guard on config re-source, and a lazily-memoized disabled-dir matcher (an eager vim.regex hoist could abort config load on a user-supplied pattern).
Rewrite mason-lspconfig around the shared resolver + lsp.lua ordering (setup → user overrides → resolve). Per-filetype deferral is event-driven — no hardcoded eager set: a listed bucket resolves that ft, a dotted variant resolves its base bucket, an unlisted real-file ft drains the rest; enable() attaches by the module's real filetypes so deferring is correctness-safe. The user.configs.lsp override window records/replays per-server ops and closes so a captured proxy can't re-globalize. yamlls/gh_actions_ls claim yaml.github; yamlls prunes the traefik v2/v3 fileMatch conflict by pattern. Folds later rounds: the per-ft deferral state derives from the static bucket keys instead of three lockstep tables; the yamlls brace-prune engine is replaced by a literal basename filter with one shared glob_basename helper (deliberately not vim.fs.basename — its separator handling is platform-dependent); mason.nvim and mason-lspconfig.nvim leave nvim-lspconfig's dependencies so a provisioned session never loads Mason on the BufReadPre tick (a cmd-triggered spec restores :LspInstall/:LspUninstall and owns setup(); the phase-2 mapping fetch is lazy, resolve_batch passes the registry thunk, and an M._mapped_package test hook pins the mapping verdict); ServerInfo's four write-only cache fields demote to locals; resolver state is guarded against config re-source.
Resolve linter_deps through the shared resolver against nvim-lint's registry, batched per filetype off the lint events, with an idle-gated 120s parity sweep (generation-guarded) for never-opened filetypes and a fallback timer so an already-idle session isn't stranded. The plugin-loading buffer gets its initial lint. Late configures re-lint only the new linter; a broken-linter verdict is memoized so its load-time side effects run once. ftdetect/github.lua detects GitHub workflow files as yaml.github (consumed by the actionlint/shuck run: linters here, and claimed by the LSP servers). Folds the config re-source guard for its resolver sessions.
Resolve dap_deps through the shared resolver; adapters self-validate from $PATH (delve via dlv, codelldb, lldb, python via debugpy) with Mason loaded lazily off the :Dap tick. Client configs follow the validate-FIRST / raise-LAST contract so remote-attach adapters stay registered. debugpy resolves via a bounded probe cascade with a negative cache. Shared attach_endpoint validates remote host/port shape at config time (config.port and opts.default_port to the same 1-65535 contract). Folds later rounds: the debugpy import-probe negative cache collapses to one flat 10s TTL; :DapInstall/:DapUninstall are restored via a cmd-triggered mason-nvim-dap spec whose config module is the single setup() owner (require-path loads funnel through lazy's module loader, and the user-override slot moves to user.configs.mason-nvim-dap); attach_endpoint's argument guards raise clean error(..., 0) like every other raise in the helper; resolver state is guarded against config re-source.
Move lsp/formatter/linter/dap dep lists to resolver-native names with a tool_install_timeout knob, and guard the removed external_lsp_deps key with a migration warning. mason.lua drops the bootstrap ensure-install loop (the resolvers own installs) and attaches registry events for mid-session install hand-off; mason-nvim-dap becomes a standalone lazy spec so a provisioned session never pays Mason on the :Dap tick. vim.yml teaches selene package.searchpath. Folds later rounds: the external_lsp_deps migration guard relocates to core/migrations.lua (settings.lua stays declarative), and mason.lua's registry acquisition reuses tools.default_registry — the one copy of the guarded load.
The remainder of the zero-warning lua-language-server pass that no subsystem commit owns: suppress undefined-doc-name on blink.lua's three upstream @type names (real upstream types, just outside the workspace library), align snacks.lua's indent-filter doc param with the real (buf, win) signature, and narrow keymap.lua's types (@cast on the modes list; vim.deepcopy(opts or {}), behavior-equivalent for every input shape).
bfb3a69 to
8aa0730
Compare
A "validates" local config failing with a raise_verbatim config-layer error was final-marked in phase 2 while its session.pending entry stayed set: the session leaked until restart, and every :ToolsRetry or Mason install event re-ran the broken configure and re-emitted the warning. This contradicted phase 1's own policy (configure_available never parks config-layer errors). Clear session.pending[name] in the config_error sub-case only; provisioning failures keep their legitimate retry paths. Session drop rides finish()'s existing drop_session_if_done sweep, the same pattern as the mark_unknown branches. Found by /code-review (CONFIRMED); plan and diff both passed adversarial review.
The per-dir cache stored both the compiled vim.regex and the normalized string, but the string's only reader is the rare format_notify warning, where the loop's `dir` is in scope — derive it there with vim.fs.normalize(dir) instead and drop the wrapper table. The lazy-compile contract is unchanged: vim.regex still throws before the cache write, so only successful compiles are cached. Round-8 review finding (PLAUSIBLE); plan and diff both passed adversarial review.
configure_available evaluated the identical `spec.has_local_config and spec.has_local_config(name)` guard in both the mutually-exclusive "resolves" and "validates" branches; hoist it into one guard and let the mode pick the branch. Call count is unchanged (the old first branch short-circuited on the mode compare before the lookup), and both branch bodies and comments carry over verbatim. missing_collector's add() compressed its reason-upgrade rule into a four-clause compound; split it into two guard clauses (usable-reason check, then first-real-reason-wins) with the finality latch staying ahead of the early returns. Both are behavior-preserving (De Morgan-exact for add(); truth-table-verified for configure_available). Round-8 review findings (PLAUSIBLE); plan gate passed round 2 after re-mapping the regression pins to real-resolver collector scenarios (t2/t3/t9/t11), fix gate passed round 1; all 7 pinned scenarios green pre- and post-edit.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lua/modules/configs/completion/nvim-lint.lua:444
vim.api.nvim_create_autocmdexpectsopts.groupto be an augroup id. Using the string "NvimLint" can throw at runtime and prevent this one-shot FileType catch-up from being registered.
vim.api.nvim_create_autocmd("FileType", {
group = "NvimLint",
buffer = load_buf,
once = true,
callback = function()
Name the shape resolve() actually implements (Template Method with steps injected as a spec table) and document the three places it departs from that shape, so a reviewer reading the skeleton is not left inferring the contract from behaviour. The header note lists the deviations with their re-evaluation triggers: variant flags (local_config_mode, static_binaries, defer_phase2) branching inside the skeleton instead of being steps, local_config_mode leaking which retry gates a name can reach, and Mason's API being called straight from the skeleton rather than from behind a step. Each site carries a one-line marker pointing back at the note rather than repeating the argument. Comments only; no behaviour change.
Discovery-first tool resolution: for LSP servers, formatters, linters, and DAP
adapters, a tool on
$PATHis used as-is; else Mason installs it when it ships apackage; else an aggregated warning asks you to provision it. Mason becomes an
optional backend rather than a hard dependency.
History aggregated into six thematic commits (resolver core, LSP, conform, lint,
DAP, settings). Final tree is byte-identical to the fully-reviewed work from the
prior branch — this supersedes #21, which GitHub locked as merged after an
accidental merge+reset (the merge was reverted on
main).Verification: stylua + selene clean; the discovery-first behavior was validated
by an extensive headless test fleet and four rounds of Copilot review (all
findings addressed) on the prior branch state.