feat: discovery-first tool resolution (make Mason optional)#21
feat: discovery-first tool resolution (make Mason optional)#21charliie-dev wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements a discovery-first tool resolution model so that tools already available on $PATH (system-provided or Mason-provided) are used directly, while Mason becomes an optional installer backend rather than a hard runtime gate. This supports environments like NixOS/FreeBSD where Mason-installed binaries may be unavailable or undesirable.
Changes:
- Add shared helpers for
$PATHprobing, Masonspec.binextraction, and aggregated “missing tools” warnings. - Rework LSP and DAP setup to be driven by desired dependency lists with discovery-first resolution and deferred installs.
- Update Mason bootstrap for formatters/linters to only install when the tool isn’t already on
$PATH, aggregating missing-tool warnings.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lua/modules/utils/tools.lua | New helper module for executable discovery + aggregated missing-tool warnings. |
| lua/modules/configs/tool/dap/init.lua | Switch DAP setup to discovery-first resolution instead of mason-nvim-dap’s installed-only handler gating. |
| lua/modules/configs/completion/servers/shuck.lua | Update server comment to reflect the new discovery-first resolution model. |
| lua/modules/configs/completion/mason.lua | Only install formatter/linter packages when not already available on $PATH; aggregate missing warnings. |
| lua/modules/configs/completion/mason-lspconfig.lua | Drive LSP setup via discovery-first resolution + deferred installs, using mason-lspconfig mappings. |
| lua/modules/configs/completion/lsp.lua | Remove external_lsp_deps wiring; rely on centralized discovery-first LSP setup. |
| lua/core/settings.lua | Collapse external/system LSP list into unified lsp_deps list with discovery-first semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lua/modules/configs/completion/mason-lspconfig.lua:79
mason_lsp_handleralwaysrequires the repo preset module (completion.servers.<name>) even when a user override exists and is a function. Some preset modules do non-trivial work at load (e.g.vim.fn.expandinlua_ls.lua), so this adds avoidable overhead and can surface preset-load errors that the user override wouldn’t otherwise hit. Load the preset lazily only when needed (no user override, or when merging table-based overrides).
local modules = server_modules(lsp_name)
local ok, custom_handler = pcall(require, modules[1])
local default_ok, default_handler = pcall(require, modules[2])
-- Use preset if there is no user definition
if not ok then
ok, custom_handler = default_ok, default_handler
end
Shared resolution loop for LSP/DAP/formatter/linter deps: probe $PATH and Mason's bin dir first, fall back to a Mason install, and aggregate every unresolved tool into one warning with a configurable deadline (settings.tool_install_timeout). Teach selene about package.searchpath, which module_path uses.
Fold external_lsp_deps into a single lsp_deps list resolved via the shared loop: probe manual specs and lspconfig cmds for binaries, install via Mason when a package exists, rewrite off-$PATH cmds before enabling, and route typos to the unknown-name bucket. Registration stays synchronous; only installs defer.
conform.nvim and nvim-lint resolve their own deps against their runtime registries (the ground truth), reverse-looking-up Mason packages by binary; mason.lua's ensure_installed loop is gone. nvim-lint's probe defers off the first BufReadPost and re-lints after late installs; conform stays synchronous so the off-$PATH rewrite lands before the first format-on-save.
Adapters resolve via the shared loop, with mason-nvim-dap's mappings guarded as private internals and an adapter_meta ground-truth fallback (delve -> dlv, python -> debugpy-adapter). Client configs self-validate and resolve their binaries lazily on the spawn path, so remote attach works without them.
The built-in v2 entry's fileMatch overlaps the v3 schema added via extra, and schemastore's replace can't rename v2 -> v3.
Feasibility POC — RFC ayamir/nvimdots#1293
Implements the "separate tool discovery from tool installation" idea from this comment: Mason becomes one installer backend, not a hard requirement. On FreeBSD/NixOS/etc. — where Mason's prebuilt binaries don't run — system-provided tools are used directly, with no manual symlinking and no startup notification spam.
No new toggle, no OS detection. Behaviour is driven purely by availability.
Resolution model (per tool)
Applied to LSP, DAP, formatters, and linters through one shared loop (
modules/utils/tools.lua). Everything resolvable without an install registers synchronously (lazy-load trigger replay still works); only installs defer.Changes
modules/utils/tools.lua(new)$PATH+ Mason-bin-dir probe, install-then-configure tracking, bin→package reverse lookup, per-subsystem warning aggregator (one message, not N) with a configurable deadline (settings.tool_install_timeout)mason-lspconfig.lua,lsp.lualsp_deps;external_lsp_depsmerged intolsp_deps; function-cmdservers (e.g.jsonls) probed via the Mason package's declared bins; off-$PATHcmds rewritten to absolute paths beforevim.lsp.enable()(covers MasonPATH = "skip")conform.lua,nvim-lint.lua,mason.luamason.lua'sensure_installedloop removed (UI-only now)dap/init.lua,dap/clients/*dap_deps; mason-nvim-dap's private mappings guarded, with anadapter_metaground-truth fallback (delve→dlv,python→debugpy-adapter); client configs self-validate and resolve binaries lazily on the spawn path, so remote attach works without local binariessettings.lualsp_deps: absorbsnil_ls,nixd, andshuck— one list, every entry is wanted. Package-less servers (nixd,shuck) are enabled from their$PATHbinary; when absent they join the aggregated warning (the fix is provisioning via home-manager/mise, not Mason)external_lsp_deps: removedformatter_deps/linter_deps: entries are now the subsystem's tool names, matchinglsp_depssemantics (cmakelang→cmake_format,golangci-lint→golangcilint); the Mason package name is derived, not configuredtool_install_timeout(new): deadline before the aggregated warning is flushed despite unsettled installsVerification
stylua+selene+nix flake check, green.lua_lsvia the new resolver, no errors.Known POC limitations
$PATHdetection leans on the Mason package's declared binaries (nvim-dap has no uniform command registry); adapters outsideadapter_meta/mason-nvim-dap mappings need a client config.