Skip to content

feat: discovery-first tool resolution (make Mason optional) - #26

Open
charliie-dev wants to merge 11 commits into
mainfrom
feat/discovery-first-tooling
Open

feat: discovery-first tool resolution (make Mason optional)#26
charliie-dev wants to merge 11 commits into
mainfrom
feat/discovery-first-tooling

Conversation

@charliie-dev

Copy link
Copy Markdown
Owner

Discovery-first tool resolution: for LSP servers, formatters, linters, and DAP
adapters, a tool on $PATH is used as-is; else Mason installs it when it ships a
package; 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread lua/modules/utils/tools.lua

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.github detection + 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.

Comment thread lua/modules/utils/tools.lua
Comment thread lua/modules/configs/tool/dap/clients/python.lua

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread lua/modules/utils/tools.lua

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread lua/modules/configs/completion/nvim-lint.lua
Comment thread lua/modules/configs/completion/nvim-lint.lua

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.dap now returns a proxy table whose __index always produces a double-thunk (meant for input_*() helpers). This breaks callers that expect an eager value like utils.get_env() (e.g. LLDB client config sets env = utils.get_env()): get_env() will currently return a function instead of the env table. Export get_env as a concrete function (like attach_endpoint) so it bypasses the __index thunking.
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comment thread lua/modules/utils/dap.lua Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

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).
@charliie-dev
charliie-dev force-pushed the feat/discovery-first-tooling branch from bfb3a69 to 8aa0730 Compare July 23, 2026 16:24
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_autocmd expects opts.group to 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()

Comment thread lua/modules/configs/completion/nvim-lint.lua

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants