Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions lua/core/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,12 @@ settings["external_browser"] = "chrome-cli open"
---@type boolean
settings["lsp_inlayhints"] = false

-- LSPs installed outside Mason (e.g. via system package manager).
-- These will be configured but not installed by Mason.
-- Key: lspconfig server name, Value: executable name to check availability.
---@type table<string, string>
settings["external_lsp_deps"] = {
nixd = "nixd",
nil_ls = "nil",
shuck = "shuck", -- shell linter/formatter/LSP (Rust); installed via mise, not Mason
-- dartls = "dart",
}

-- LSPs to install during bootstrap.
-- Language servers to enable. One flat list — no need to decide up front whether
-- a server is "Mason-managed" or "system-provided". At runtime each entry is
-- resolved discovery-first: if its binary is already on $PATH (system / Nix /
-- Mason) it is used as-is; otherwise Mason installs it when it ships a package;
-- otherwise the user is asked to install it manually. See `modules.utils.tools`
-- and `completion/mason-lspconfig.lua`.
-- Full list: https://github.com/neovim/nvim-lspconfig/tree/master/lsp
---@type string[]
settings["lsp_deps"] = {
Expand All @@ -111,7 +105,10 @@ settings["lsp_deps"] = {
"lua_ls",
"marksman",
"neocmake",
"nil_ls", -- Nix LSP; Nix-provided on $PATH (no Mason mapping, else flagged for manual install)
"nixd", -- Nix LSP; Nix-provided on $PATH (no Mason mapping, else flagged for manual install)
"ruff",
"shuck", -- shell linter/formatter/LSP (Rust); installed via mise, not Mason
"systemd_lsp",
"terraformls",
"tflint",
Expand Down
26 changes: 3 additions & 23 deletions lua/modules/configs/completion/lsp.lua
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
return function()
-- Server resolution (Mason-installed / on $PATH / installable / missing) is
-- handled centrally and discovery-first in `mason-lspconfig.setup`, driven by
-- the single `settings.lsp_deps` list.
require("completion.mason-lspconfig").setup()

local opts = {
capabilities = require("modules.utils").get_lsp_capabilities(),
}
-- Configure LSPs that are not managed by Mason but are available in `nvim-lspconfig`.
-- Servers are defined in `settings.external_lsp_deps` as { server_name = "executable" }.
for lsp_name, exe in pairs(require("core.settings").external_lsp_deps) do
if vim.fn.executable(exe) == 1 then
local ok, _opts = pcall(require, "user.configs.lsp-servers." .. lsp_name)
if not ok then
local default_ok, default_opts = pcall(require, "completion.servers." .. lsp_name)
if default_ok then
_opts = default_opts
end
end
if type(_opts) == "table" then
local final_opts = vim.tbl_deep_extend("keep", _opts, opts)
require("modules.utils").register_server(lsp_name, final_opts)
else
require("modules.utils").register_server(lsp_name, opts)
end
end
end

pcall(require, "user.configs.lsp")

-- Start LSPs
Expand Down
115 changes: 85 additions & 30 deletions lua/modules/configs/completion/mason-lspconfig.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ local M = {}

M.setup = function()
local lsp_deps = require("core.settings").lsp_deps
local mason_registry = require("mason-registry")
local mason_lspconfig = require("mason-lspconfig")

require("modules.utils").load_plugin("mason-lspconfig", {
ensure_installed = lsp_deps,
-- Skip auto enable because we are loading language servers lazily
automatic_enable = false,
})
-- Mason is an optional installer backend: guard its requires so a Mason-less
-- setup still resolves servers from $PATH instead of hard-erroring here.
local has_registry, mason_registry = pcall(require, "mason-registry")
local has_mlsp, mason_lspconfig = pcall(require, "mason-lspconfig")
local mason_ok = has_registry and has_mlsp
local tools = require("modules.utils.tools")
Comment thread
charliie-dev marked this conversation as resolved.

vim.diagnostic.config({
signs = true,
Expand Down Expand Up @@ -81,37 +79,94 @@ please REMOVE your LSP configuration (rust_analyzer.lua) from the `servers` dire
end
end

---A simplified mimic of <mason-lspconfig 1.x>'s `setup_handlers` callback.
---Invoked for each Mason package (name or `Package` object) to configure its language server.
---@param pkg string|{name: string} Either the package name (string) or a Package object
local function setup_lsp_for_package(pkg)
-- First try to grab the builtin mappings
local mappings = mason_lspconfig.get_mappings().package_to_lspconfig
-- If empty or nil, build it by hand
if not mappings or vim.tbl_isempty(mappings) then
mappings = {}
-- Map lspconfig server name -> Mason package name (only when Mason is present).
-- Used to tell whether a server has a Mason package at all, and to drive
-- installs. Build by hand if the builtin map is unavailable.
local lspconfig_to_package = {}
if mason_ok then
local mappings = mason_lspconfig.get_mappings()
lspconfig_to_package = (mappings and mappings.lspconfig_to_package) or {}
if vim.tbl_isempty(lspconfig_to_package) then
for _, spec in ipairs(mason_registry.get_all_package_specs()) do
local lspconfig = vim.tbl_get(spec, "neovim", "lspconfig")
if lspconfig then
mappings[spec.name] = lspconfig
local server = vim.tbl_get(spec, "neovim", "lspconfig")
if server then
lspconfig_to_package[server] = spec.name
end
end
end

-- Figure out the package name and lookup
local name = type(pkg) == "string" and pkg or pkg.name
local srv = mappings[name]
if not srv then
return
end
-- Load mason-lspconfig for the lspconfig integration only. Installs are
-- driven explicitly below (discovery-first) so they degrade gracefully where
-- Mason can't help (BSD/NixOS/...), instead of being gated on the installed set.
require("modules.utils").load_plugin("mason-lspconfig", {
ensure_installed = {},
-- Skip auto enable because we are loading language servers lazily
automatic_enable = false,
})
end

-- Invoke the handler
mason_lsp_handler(srv)
---Resolve a server's launch binary for the $PATH probe. Prefer an explicit
---`cmd` from the manual spec (user override, then repo default under
---`completion/servers/`), then fall back to nvim-lspconfig's default `cmd`.
---This way a server defined only via a local spec (with no nvim-lspconfig
---entry) is still discoverable on $PATH.
---@param name string
---@return string|nil
local function server_binary(name)
for _, module in ipairs({ "user.configs.lsp-servers." .. name, "completion.servers." .. name }) do
local ok, spec = pcall(require, module)
if ok and type(spec) == "table" and type(spec.cmd) == "table" then
return spec.cmd[1]
end
end
local ok, config = pcall(function()
return vim.lsp.config[name]
end)
if ok and type(config) == "table" and type(config.cmd) == "table" then
return config.cmd[1]
end
return nil
end
Comment thread
charliie-dev marked this conversation as resolved.

for _, pkg in ipairs(mason_registry.get_installed_package_names()) do
setup_lsp_for_package(pkg)
-- Discovery-first resolution per desired server:
-- 1. Mason-installed OR binary already on $PATH -> configure now.
-- 2. Not available but Mason ships a package -> install, configure next launch.
-- 3. No Mason package and not on $PATH -> ask the user to install it.
local collector = tools.missing_collector("LSP")

for _, name in ipairs(lsp_deps) do
local pkg_name = mason_ok and lspconfig_to_package[name] or nil
local installed = pkg_name ~= nil and mason_registry.is_installed(pkg_name)
local binary = server_binary(name)
local on_path = binary ~= nil and vim.fn.executable(binary) == 1

if installed or on_path then
-- Guard the handler: a user/server spec that errors during setup would
-- otherwise abort the whole loop (skipping the remaining servers and
-- collector.done()). Degrade by marking just that server.
if not pcall(mason_lsp_handler, name) then
collector.mark(name)
end
elseif pkg_name ~= nil then
local ok, pkg = pcall(mason_registry.get_package, pkg_name)
if ok then
collector.track(pkg, name, function()
return pkg:is_installed() or (binary ~= nil and vim.fn.executable(binary) == 1)
end)
else
-- The server maps to a Mason package the registry doesn't have
-- (stale mapping / registry skew). The lookup that failed was for
-- `pkg_name`, so record that (annotated with the server) rather than
-- `name` — otherwise the warning wrongly implicates the user's
-- lsp_deps entry when the mapping/registry is at fault.
collector.mark_unknown(pkg_name == name and name or (pkg_name .. " (for " .. name .. ")"))
end
else
collector.mark(name)
end
end

collector.done()
end

return M
36 changes: 20 additions & 16 deletions lua/modules/configs/completion/mason.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,39 @@ M.setup = function()
},
})

-- Ensure formatters and linters are installed (only if mason loaded)
-- Ensure formatters and linters are available (only if mason loaded)
local ok, registry = pcall(require, "mason-registry")
if not ok then
return
end

local settings = require("core.settings")
local tools = require("modules.utils.tools")
local ensure_installed = vim.list_extend(vim.deepcopy(settings.formatter_deps), settings.linter_deps)

-- Discovery-first: only install what isn't already on $PATH, so systems that
-- provide their own tools (NixOS/BSD/...) aren't nagged every startup. What
-- Mason genuinely can't provide is collected into a single warning.
local collector = tools.missing_collector("Mason")

for _, pkg_name in ipairs(ensure_installed) do
local pkg_ok, pkg = pcall(registry.get_package, pkg_name)
if not pkg_ok then
vim.notify(
string.format("[Mason] Package '%s' not found in registry", pkg_name),
vim.log.levels.WARN,
{ title = "Mason" }
)
elseif not pkg:is_installed() then
pkg:install():once("closed", function()
if not pkg:is_installed() then
vim.notify(
string.format("[Mason] Failed to install '%s'", pkg_name),
vim.log.levels.WARN,
{ title = "Mason" }
)
end
end)
-- No such Mason package (typo / removed): Mason can't provide it and the
-- package name isn't a reliable executable to probe. Surface it as an
-- unknown name so the warning points at the config, not a manual install.
collector.mark_unknown(pkg_name)
else
Comment thread
charliie-dev marked this conversation as resolved.
local binaries = tools.package_binaries(pkg, pkg_name)
if not (pkg:is_installed() or tools.any_executable(binaries)) then
collector.track(pkg, pkg_name, function()
return pkg:is_installed() or tools.any_executable(binaries)
end)
end
end
end

collector.done()
end

return M
6 changes: 3 additions & 3 deletions lua/modules/configs/completion/servers/shuck.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
-- shuck: Rust shell linter/formatter/language server.
-- https://ewhauser.github.io/shuck/docs/lsp/
--
-- Installed via mise (`cargo:shuck-cli`), not Mason, so it is wired through
-- `settings.external_lsp_deps` rather than `lsp_deps`. shuck is not shipped in
-- nvim-lspconfig either, so cmd/filetypes/root_markers must be declared here.
-- Installed via mise (`cargo:shuck-cli`), not Mason. It has no Mason package, so
-- its `lsp_deps` entry is resolved discovery-first from $PATH. cmd/filetypes/
-- root_markers are declared here so it works regardless of nvim-lspconfig.
--
-- Provides live diagnostics, code actions (incl. `source.fixAll.shuck`),
-- suppression-code hover, and document/range formatting over LSP.
Expand Down
49 changes: 44 additions & 5 deletions lua/modules/configs/tool/dap/clients/delve.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@ return function()
local dap = require("dap")
local utils = require("modules.utils.dap")

if not require("mason-registry").is_installed("go-debug-adapter") then
-- go-debug-adapter is a node-based adapter that in practice ships via Mason.
-- Mason is optional here: guard the registry require so a Mason-less setup
-- doesn't hard-error on the require itself. When the adapter genuinely can't be
-- resolved this config raises a clear error on purpose (see below) so the
-- resolver's pcall marks `delve` missing and surfaces it in the aggregated
-- warning — it does not silently leave the adapter unset.
local has_registry, registry = pcall(require, "mason-registry")
if has_registry and not registry.is_installed("go-debug-adapter") then
-- get_package throws when the name isn't in the registry (registry skew).
-- Guard it so that turns into a clear, actionable error the resolver's pcall
-- can surface, rather than a generic "missing" with the install path skipped.
local ok, go_dbg = pcall(registry.get_package, "go-debug-adapter")
if not ok then
error("go-debug-adapter is not in the Mason registry (registry outdated?); run :MasonUpdate")
end

vim.notify(
"Automatically installing `go-debug-adapter` for go debugging",
vim.log.levels.INFO,
Comment thread
charliie-dev marked this conversation as resolved.
{ title = "nvim-dap" }
Comment thread
charliie-dev marked this conversation as resolved.
)

local go_dbg = require("mason-registry").get_package("go-debug-adapter")
go_dbg:install():once(
"closed",
vim.schedule_wrap(function()
Expand All @@ -20,14 +34,39 @@ return function()
end
end)
)
-- The install was just kicked off (user informed via the INFO notification
-- above) and its bundle won't exist until it finishes. Configure on the next
-- launch rather than erroring below and being flagged "missing" while it's
-- already installing.
return
end

-- Reached only when Mason is absent or go-debug-adapter is already installed.
-- Resolve the node bundle; if it isn't readable (Mason unavailable, or the
-- package reported installed but its bundle is missing/moved/corrupted), error
-- out instead of returning with `dap.adapters.go` unset. The resolver in
-- `tool/dap/init.lua` runs this under pcall, so throwing lets it mark `delve`
-- as missing rather than silently leaving Go debugging broken.
local mason_root = require("modules.utils.tools").mason_root()
local adapter_js = mason_root and (mason_root .. "/packages/go-debug-adapter/extension/dist/debugAdapter.js")
if not (adapter_js and vim.fn.filereadable(adapter_js) == 1) then
error(
"go-debug-adapter bundle not found"
.. (adapter_js and (" at " .. adapter_js) or " ($MASON is unset)")
.. "; install `go-debug-adapter` via Mason or provide its bundle on this path"
)
end
-- The bundle is a Node script, so it needs `node` on $PATH. Validate it up
-- front and error out if absent: otherwise the adapter configures fine (so the
-- resolver won't flag `delve`) but debugging fails at runtime. Throwing lets
-- `tool/dap/init.lua`'s pcall surface it via the aggregated missing warning.
if vim.fn.executable("node") == 0 then
error("go-debug-adapter requires `node` on $PATH, which was not found")
end
dap.adapters.go = {
type = "executable",
command = "node",
args = {
vim.env.MASON .. "/packages/go-debug-adapter" .. "/extension/dist/debugAdapter.js",
},
args = { adapter_js },
}
Comment thread
charliie-dev marked this conversation as resolved.
dap.configurations.go = {
{
Expand Down
Loading
Loading