Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
0392395
feat(tooling): add a shared discovery-first tool resolver
charliie-dev Jul 20, 2026
8316907
feat(settings): move dep lists to resolver-native names
charliie-dev Jul 20, 2026
4e933b3
feat(lsp): resolve language servers discovery-first
charliie-dev Jul 20, 2026
229ec3c
feat(conform): resolve formatters discovery-first
charliie-dev Jul 20, 2026
a72c766
feat(lint): resolve linters discovery-first
charliie-dev Jul 20, 2026
5642ed3
feat(dap): resolve debug adapters discovery-first
charliie-dev Jul 20, 2026
7bb2678
refactor(mason): drop the bootstrap ensure-install loop
charliie-dev Jul 20, 2026
9ed2a05
refactor(lsp): resolve the traefik schema conflict by file pattern
charliie-dev Jul 20, 2026
5cf756d
feat(tooling): hand off mid-session installs to pending configures
charliie-dev Jul 20, 2026
cf90b27
fix(lsp): make user override reads install-timing independent
charliie-dev Jul 20, 2026
cfeefa4
fix(lint): surface the real load error for a broken linter module
charliie-dev Jul 20, 2026
8a911df
fix(tooling): keep concrete failure reasons out of the dedup sink
charliie-dev Jul 20, 2026
c2fa84b
fix(lsp): consult vim.lsp.config as the server discovery truth source
charliie-dev Jul 20, 2026
b74e53d
fix(tooling): report invalid dep entries and unresolvable tools honestly
charliie-dev Jul 20, 2026
a75823a
fix(conform): stop reporting buffer-conditional overrides as typos
charliie-dev Jul 20, 2026
d67d14e
fix(lsp): scope the traefik schema prune to the claimed files
charliie-dev Jul 20, 2026
5e1c478
fix(dap): survive partial mason-nvim-dap mapping drift
charliie-dev Jul 20, 2026
252ebd5
refactor(tooling): sink the no-fall-through config contract into tools
charliie-dev Jul 20, 2026
df782e2
perf(tooling): move deferral ownership into the resolver
charliie-dev Jul 20, 2026
a20707b
perf(dap): keep mason off the :Dap tick for provisioned sessions
charliie-dev Jul 20, 2026
4a33b30
perf(lsp): defer registry classification off the first file open
charliie-dev Jul 20, 2026
88be21c
perf(dap): cache the negative debugpy import probe for the session
charliie-dev Jul 20, 2026
b543856
perf(tooling): memoize hot registry and mason-root lookups
charliie-dev Jul 20, 2026
4958f0f
fix(lint): propagate a failed post-install linter reload
charliie-dev Jul 20, 2026
ee9aafa
refactor(lint): remove the unreachable factory-wrapper machinery
charliie-dev Jul 20, 2026
535b8e9
refactor(dap): probe debugpy through the supported mason surface
charliie-dev Jul 20, 2026
8961821
refactor(conform): drop the dead format-on-save re-check
charliie-dev Jul 20, 2026
1f1d5b7
fix(settings): warn when a stale external_lsp_deps override survives
charliie-dev Jul 20, 2026
953b2be
docs(settings): correct the linter_deps resolution timing
charliie-dev Jul 20, 2026
798151c
docs(tooling): include the unresolved state in the probe annotation
charliie-dev Jul 20, 2026
0c0a436
fix(lsp): use the documented gh-dash schema url
charliie-dev Jul 20, 2026
17dfa3a
fix(dap): re-probe debugpy on a ttl with a finite retry budget
charliie-dev Jul 20, 2026
89201ab
fix(dap): restore enumerable opts for user overrides and name mapping…
charliie-dev Jul 20, 2026
4260acd
fix(settings): classify external_lsp_deps residue before advising
charliie-dev Jul 20, 2026
9f40f9e
fix(tooling): strip only real chunkname prefixes from error reasons
charliie-dev Jul 20, 2026
0c24473
refactor(tooling): carry the unresolved reason on the probe result
charliie-dev Jul 20, 2026
5a6aaf3
fix(conform): report a vanished get_formatter_config as unresolved
charliie-dev Jul 20, 2026
41c1dba
fix(lsp): bound the traefik prune's brace matcher and fail closed pas…
charliie-dev Jul 20, 2026
8cf62af
perf(lsp): resolve language servers per filetype
charliie-dev Jul 20, 2026
9f56985
docs(settings): describe the per-filetype lsp_deps timing
charliie-dev Jul 20, 2026
25bf650
fix(tooling): drop the bin-to-package index on registry updates
charliie-dev Jul 20, 2026
39936df
fix(lsp): refetch the mason mapping after a registry update
charliie-dev Jul 20, 2026
d87a337
refactor(tooling): consolidate resolver bookkeeping into single sources
charliie-dev Jul 20, 2026
f9d1114
refactor(dap): inline the single-caller client-config loader
charliie-dev Jul 20, 2026
3712ef3
fix(lint): add an idle-gated parity sweep for never-opened filetypes
charliie-dev Jul 21, 2026
e6f08f8
fix(dap): raise spawn-free at config time when mason can provision de…
charliie-dev Jul 21, 2026
0e7e391
fix(dap): guard the venv fallback against a nil cwd
charliie-dev Jul 21, 2026
84edddd
fix(lsp): guard get_mappings against mason-lspconfig api drift
charliie-dev Jul 21, 2026
e14f445
fix(tooling): flag each registry event subscription separately
charliie-dev Jul 21, 2026
84c5855
fix(dap): validate the delve remote-attach host shape
charliie-dev Jul 21, 2026
c9ac3d0
fix(tooling): let a set-up mason.settings root win over a cached $MASON
charliie-dev Jul 21, 2026
ea74de8
fix(tooling): re-check $PATH membership on every ensure_mason_on_path…
charliie-dev Jul 21, 2026
87ad594
refactor(tooling): share the lazy mason-registry thunk across resolvers
charliie-dev Jul 21, 2026
8daf5be
fix(tooling): normalize a drifted registry thunk result in phase 2
charliie-dev Jul 21, 2026
6242a03
refactor(lsp): classify lsp_deps through split_dep_names
charliie-dev Jul 21, 2026
aad58a4
refactor(lsp): extract the one cmd-to-binary rule
charliie-dev Jul 21, 2026
6c6fe32
refactor(tooling): share the pending-binaries probe across hand-off p…
charliie-dev Jul 21, 2026
fc00806
refactor(tooling): extract the string-or-nil sanitizer
charliie-dev Jul 21, 2026
9656317
refactor(tooling): drop the normalize_names wrapper
charliie-dev Jul 21, 2026
3642567
docs(tooling): label mason_root as the tested root-priority contract
charliie-dev Jul 21, 2026
6601542
refactor(dap): return the override verdict instead of the winning module
charliie-dev Jul 21, 2026
bff4a12
docs(lsp): label the harness-facing exports
charliie-dev Jul 21, 2026
2fa8a0d
docs(tooling): de-rot and reattach the resolver docstrings
charliie-dev Jul 21, 2026
e04e565
docs(lsp): de-rot the replay, sweep, and shuck comments
charliie-dev Jul 21, 2026
fe9d466
docs(lint): de-rot the override and sweep comments
charliie-dev Jul 21, 2026
31f20ec
docs(conform): de-rot the prettier override and drop restating comments
charliie-dev Jul 21, 2026
e181a23
docs(dap): correct the loader and adapter comments
charliie-dev Jul 21, 2026
26e0618
docs(settings): de-rot dep-list comments and fix the palette type
charliie-dev Jul 21, 2026
e92954c
chore(tool): drop the commented-out remote-nvim spec
charliie-dev Jul 21, 2026
6df2354
docs(settings): suppress missing-fields on the partial palette override
charliie-dev Jul 21, 2026
c53bd6e
fix(dap): bound the debugpy import probe with a timeout
charliie-dev Jul 21, 2026
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
82 changes: 62 additions & 20 deletions lua/core/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,15 @@ 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, resolved discovery-first at runtime: binary on $PATH is
-- used as-is; else Mason installs it when it ships a package; else an aggregated warning
-- asks you to provision it. 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"] = {
"bashls",
"clangd",
-- "dartls", -- Dart LSP (ships with the Dart SDK)
"dockerls",
"gh_actions_ls",
-- "gitlab_ci_ls",
Expand All @@ -111,7 +103,10 @@ settings["lsp_deps"] = {
"lua_ls",
"marksman",
"neocmake",
"nil_ls", -- Nix LSP; prefer the $PATH binary (Nix), else Mason installs it (package `nil`)
"nixd", -- Nix LSP (Rust); no Mason package, comes from Nix ($PATH)
"ruff",
"shuck", -- shell linter/formatter/LSP (Rust); no Mason package, installed via mise
"systemd_lsp",
"terraformls",
"tflint",
Expand All @@ -120,39 +115,55 @@ settings["lsp_deps"] = {
"zuban",
}

-- Formatters to install during bootstrap (Mason package names).
-- These are managed by Mason and used by conform.nvim.
-- Formatters to resolve when conform.nvim lazy-loads (first BufWritePre /
-- :Format). conform formatter names, resolved discovery-first like lsp_deps.
---@type string[]
settings["formatter_deps"] = {
"beautysh",
"clang-format",
"cmakelang",
"cmake_format",
"fixjson",
"gofumpt",
"goimports",
"mdsf",
"nixfmt", -- Nix formatter; prefer the $PATH binary (Nix)
"prettier",
"superhtml",
"shellharden",
"statix", -- Nix linter, its `fix` mode doubles as a conform formatter; from Nix ($PATH)
"stylua",
}

-- Linters to install during bootstrap (Mason package names).
-- These are managed by Mason and used by nvim-lint.
-- Linters to resolve discovery-first (nvim-lint linter names). A name mapped
-- to a filetype resolves on that filetype's FIRST matching event after
-- nvim-lint lazy-loads (the resolve-only FileType autocmd or a lint event) —
-- nothing is installed or warned about before such a buffer opens; unmapped
-- names (typos, manual-only linters) get an immediate deferred pass instead.
---@type string[]
settings["linter_deps"] = {
"actionlint",
"deadnix", -- Nix dead-code linter; prefer the $PATH binary (Nix)
"hadolint",
"markdownlint-cli2",
"oxlint",
-- "rumdl", -- markdownlint Rust rewrite; waiting for rule coverage to mature
"golangci-lint",
"golangcilint",
"selene",
"shellcheck",
"shuck", -- shell linter for yaml.github `run:` blocks; no Mason package, installed via mise
"statix", -- Nix linter; prefer the $PATH binary (Nix)
"systemdlint",
"zsh", -- `zsh -n` syntax check via the system shell itself
}

-- Debug Adapter Protocol (DAP) clients to install and configure during bootstrap.
-- Deadline (ms) for background Mason work before the aggregated missing-tool warning
-- flushes anyway. Gates each tracked install (its own window) AND the registry refresh
-- wait; late completions still recover. Non-positive values use the default.
---@type number
settings["tool_install_timeout"] = 300000

-- DAP adapters to enable (mason-nvim-dap adapter names), resolved
-- discovery-first when nvim-dap lazy-loads (first :Dap* command).
-- Supported DAPs: https://github.com/jay-babu/mason-nvim-dap.nvim/blob/main/lua/mason-nvim-dap/mappings/source.lua
---@type string[]
settings["dap_deps"] = {
Expand Down Expand Up @@ -242,4 +253,35 @@ settings["dashboard_image"] = {
[[⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⠿⢿⠿⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀]],
}

return require("modules.utils").extend_config(settings, "user.settings")
local merged = require("modules.utils").extend_config(settings, "user.settings")

-- Migration guard: the discovery-first refactor removed this key; a stale
-- user/settings.lua would merge it in and feed nothing — its servers would
-- vanish without a word. (Scheduled: the notifier plugin isn't loaded this
-- early; the default notify still lands in :messages.)
if merged.external_lsp_deps ~= nil then
-- The removed setting was a MAP of server name -> executable name: tell
-- the user to move the KEYS and list them (moving a value like "nil"
-- instead of the key "nil_ls" would land an invalid lsp_deps entry).
local keys = {}
if type(merged.external_lsp_deps) == "table" then
for server in pairs(merged.external_lsp_deps) do
keys[#keys + 1] = tostring(server)
end
table.sort(keys)
end
local names = #keys > 0 and (" (" .. table.concat(keys, ", ") .. ")") or ""
vim.schedule(function()
vim.notify(
"`external_lsp_deps` was removed: non-Mason servers are now discovered\n"
.. "from $PATH. Move its KEYS"
.. names
.. " — the server names, not the\n"
.. "executable values — into `lsp_deps` in user/settings.lua.",
vim.log.levels.WARN,
{ title = "core.settings" }
)
end)
end

return merged
79 changes: 65 additions & 14 deletions lua/modules/configs/completion/conform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ return function()
return false
end

---The gates a buffer must pass before an automatic format, grouped into one
---named predicate for the format_on_save callback's readability. The
---format_on_save SETTING is not re-checked here: its gate lives at the
---single place the callback is installed (format_on_save = enabled and …).
---@param bufnr integer
---@return boolean
local function autoformat_allowed(bufnr)
return block_list[vim.bo[bufnr].filetype] ~= true
and not is_disabled_workspace(bufnr)
and not vim.g.disable_autoformat
and not vim.b[bufnr].disable_autoformat
end

---Format only git-modified lines using gitsigns hunks + conform range format
---@param bufnr integer
---@return boolean @true if modifications were formatted
Expand Down Expand Up @@ -87,6 +100,8 @@ return function()
return true
end

local tools = require("modules.utils.tools")

require("modules.utils").load_plugin("conform", {
default_format_opts = {
timeout_ms = format_timeout,
Expand Down Expand Up @@ -132,27 +147,18 @@ return function()
args = { "fix", "--stdin" },
stdin = true,
},
-- prettier: stdin mode does not work under bun's node shim,
-- so use --write (file-based) mode instead.
-- prettier: stdin is broken under bun's node shim, so use --write on
-- conform's temp copy (`stdin = false` points $FILENAME at a
-- `.conform.$RANDOM.*` copy; the real file is never touched).
prettier = {
command = "prettier",
args = { "--write", "$FILENAME" },
stdin = false,
},
},
format_on_save = format_on_save_enabled and function(bufnr)
-- Check disabled filetypes
if block_list[vim.bo[bufnr].filetype] == true then
return
end

-- Check disabled workspaces
if is_disabled_workspace(bufnr) then
return
end

-- Check global toggle
if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
-- Disabled filetypes, disabled workspaces, and the global/buffer toggles
if not autoformat_allowed(bufnr) then
return
end

Expand All @@ -168,6 +174,51 @@ return function()
end or false,
})

-- Resolve `formatter_deps` (conform formatter names) discovery-first against
-- conform's own registry, so a missing formatter is installed / reported.
-- The probe only drives install/warn — nothing on the save path reads it —
-- so `defer` moves the resolve off the BufWritePre tick that lazy-loaded
-- conform; the resolver itself keeps the same-tick guarantee that Mason's
-- bin dir is on $PATH before the replayed save's spawns.
tools.resolve_runtime_tools("conform.nvim", settings.formatter_deps, function(name)
-- get_formatter_config is conform's @private API; if dropped, degrade to
-- "resolves itself" rather than misreporting every formatter as unknown.
local conform = require("conform")
if type(conform.get_formatter_config) ~= "function" then
return { binary = nil }
end
-- get_formatter_config runs a function-form override directly, so pcall keeps a
-- throwing override (a broken config) from being misread as an unknown name.
local ok, config, err = pcall(conform.get_formatter_config, name)
if not ok then
return { broken = tostring(config) }
end
if config then
-- A function-form command resolves per buffer at format time (e.g. the
-- builtin from_node_modules): treat it as self-resolving rather than
-- evaluating it for a representative binary — a node_modules command
-- shouldn't map to a Mason install anyway.
if type(config.command) == "function" then
return { binary = nil }
end
return { binary = config.command }
end
-- (nil, err) is a real formatter with a broken config; bare nil is an unknown name.
if type(err) == "string" then
return { broken = err }
end
-- A function-form override may legitimately return nil for the
-- probe-time buffer (this probe runs on a scheduled tick against
-- whatever buffer happens to be current): its existence proves the
-- name real, but nothing is verifiable — report it unresolved
-- (missing bucket, tailored reason) instead of a typo or a silent pass.
local overrides = conform.formatters
if type(overrides) == "table" and type(overrides[name]) == "function" then
return { unresolved = true }
end
return nil
end, nil, { defer = true })

-- User commands
vim.api.nvim_create_user_command("Format", function(args)
local range = nil
Expand Down
34 changes: 11 additions & 23 deletions lua/modules/configs/completion/lsp.lua
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
return function()
-- Handler/probe machinery first (no discovery yet): sets up the read
-- trigger the override pass below relies on.
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
-- Run `user.configs.lsp` with its vim.lsp.config registrations recorded: a
-- mid-session install registers after this point, and the replay keeps the
-- user's overrides on top regardless of timing.
-- `user.configs.lsp-servers.<name>` remains the richer per-server hook.
require("completion.mason-lspconfig").run_user_lsp_overrides()

pcall(require, "user.configs.lsp")
-- Discovery LAST (Mason-installed / on $PATH / installable / missing,
-- driven by `settings.lsp_deps`): user runtime registrations above must be
-- visible to the unknown/binary classification.
require("completion.mason-lspconfig").resolve_deps()

-- Start LSPs
pcall(vim.cmd.LspStart)
Expand Down
Loading
Loading