Skip to content
Draft
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
12 changes: 12 additions & 0 deletions ftdetect/github.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- GitHub Actions workflow files get the dotted filetype so their dedicated
-- tooling keys on it: nvim-lint's `yaml.github` linters (actionlint + shuck)
-- and the LSP filetype claims in servers/yamlls.lua / servers/gh_actions_ls.lua.
-- Deliberately unanchored, same shape as ftdetect/gitlab.lua: an explicit `$`
-- (any variant) empirically stops vim.filetype.match from ever matching the
-- pattern on this nvim, so suffixed copies (ci.yml.bak) re-type too — accepted
-- parity with the gitlab precedent.
vim.filetype.add({
pattern = {
[".*/%.github/workflows/.*%.ya?ml"] = "yaml.github",
},
})
64 changes: 64 additions & 0 deletions lua/core/migrations.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- One-off migration guards for settings keys removed from the declarative
-- surface: core/settings.lua calls M.check(merged) once per startup after the
-- user merge. The imperative classification/notify logic lives here so
-- settings.lua stays a pure declarative source of truth; add future removed-key
-- guards as further branches in M.check (same pattern), not in settings.lua.
local M = {}

---Warn about residue of settings keys the refactors removed.
---@param merged table @The post-merge settings table (read, never written).
function M.check(merged)
-- 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.
if merged.external_lsp_deps ~= nil then
-- The removed setting was a MAP of server name -> executable name, but a
-- stale override can survive in any shape: classify before advising so the
-- guidance never presents numeric indices as keys, never drops entries a
-- half-migrated LIST residue still carries, and always names the final
-- step (deleting the dead key). Neither group suppresses the other.
local string_keys, list_items = {}, {}
if type(merged.external_lsp_deps) == "table" then
for k, v in pairs(merged.external_lsp_deps) do
if type(k) == "string" then
string_keys[#string_keys + 1] = k
elseif type(k) == "number" and type(v) == "string" then
list_items[#list_items + 1] = v
end
end
table.sort(string_keys)
table.sort(list_items)
end
local guidance
if #string_keys > 0 and #list_items > 0 then
guidance = "Move its KEYS ("
.. table.concat(string_keys, ", ")
.. ") AND its list entries ("
.. table.concat(list_items, ", ")
.. ")\n— all of them server names — into `lsp_deps`, then delete `external_lsp_deps`."
elseif #string_keys > 0 then
guidance = "Move its KEYS ("
.. table.concat(string_keys, ", ")
.. ") — the server names, not the\n"
.. "executable values — into `lsp_deps`, then delete `external_lsp_deps`."
elseif #list_items > 0 then
guidance = "It now holds a LIST ("
.. table.concat(list_items, ", ")
.. ") — those are already the\n"
.. "server names; move them into `lsp_deps` and delete `external_lsp_deps`."
else
guidance = "It is empty or not a map — delete `external_lsp_deps` from user/settings.lua."
end
-- (Scheduled: the notifier plugin isn't loaded this early; the default
-- notify still lands in :messages.)
vim.schedule(function()
vim.notify(
"`external_lsp_deps` was removed: non-Mason servers are now discovered\nfrom $PATH. " .. guidance,
vim.log.levels.WARN,
{ title = "core.settings" }
)
end)
end
end

return M
65 changes: 44 additions & 21 deletions lua/core/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ settings["disabled_plugins"] = {}
-- These settings will override the defaults during initialization.
-- Parameters will auto-complete as you type.
-- Example: { sky = "#04A5E5" }
---@type palette[]
---@type palette
---@diagnostic disable-next-line: missing-fields
settings["palette_overwrite"] = {}

-- Set the colorscheme here.
Expand All @@ -84,23 +85,19 @@ 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. Names whose filetypes lspconfig knows resolve on that
-- filetype's FIRST buffer (a late sweep classifies the rest once per session); names
-- with user overrides, repo modules that override filetypes, or no filetype data
-- resolve on the first file open. 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 +108,10 @@ settings["lsp_deps"] = {
"lua_ls",
"marksman",
"neocmake",
"nil_ls", -- Nix LSP; the Nix-provisioned $PATH binary is preferred
"nixd", -- Nix LSP (Rust); provisioned from Nix ($PATH)
"ruff",
"shuck", -- shell linter/formatter/LSP (Rust); installed via mise by choice ($PATH wins)
"systemd_lsp",
"terraformls",
"tflint",
Expand All @@ -120,39 +120,56 @@ 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; installed via mise by choice
"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. Missing or non-positive values fall back to
-- the resolver's DEFAULT_TOOL_INSTALL_TIMEOUT_MS in `modules/utils/tools.lua`.
---@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 or debug keymap).
-- 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 @@ -241,4 +258,10 @@ settings["dashboard_image"] = {
[[⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⠿⢿⠿⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀]],
}

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

-- Removed-key migration guards live in core/migrations.lua, keeping this file
-- declarative; they only read `merged` and notify.
require("core.migrations").check(merged)

return merged
3 changes: 3 additions & 0 deletions lua/modules/configs/completion/blink.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---@module 'blink.cmp'
---@diagnostic disable-next-line: undefined-doc-name
---@type blink.cmp.Config
local opts = {
-- 'default' for mappings similar to built-in completion
Expand All @@ -19,6 +20,7 @@ local opts = {
["<C-l>"] = { "scroll_documentation_down", "fallback" },
["<C-k>"] = { "show_signature", "hide_signature", "fallback" },
},
---@diagnostic disable-next-line: undefined-doc-name
---@type blink.cmp.CmdlineConfig
cmdline = {
enabled = true,
Expand Down Expand Up @@ -176,6 +178,7 @@ local opts = {
module = "blink-ripgrep",
name = "Ripgrep",
---@module "blink-ripgrep"
---@diagnostic disable-next-line: undefined-doc-name
---@type blink-ripgrep.Options
opts = {
prefix_min_len = 3,
Expand Down
111 changes: 96 additions & 15 deletions lua/modules/configs/completion/conform.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
return function()
local settings = require("core.settings")
local disabled_workspaces = settings.format_disabled_dirs
-- Compile-once cache per configured dir: entries are user input that may
-- throw at vim.regex compile time (settings.lua blesses vim-regex strings),
-- so compilation stays lazy — on the save path, exactly where it fails today —
-- and only SUCCESSFUL compiles are cached.
local disabled_dir_cache = {}
local function disabled_matcher(dir)
local regex = disabled_dir_cache[dir]
if not regex then
regex = vim.regex(vim.fs.normalize(dir))
disabled_dir_cache[dir] = regex
end
return regex
end
local format_on_save_enabled = settings.format_on_save
local format_notify = settings.format_notify
local format_modifications_only = settings.format_modifications_only
Expand All @@ -26,7 +39,7 @@ return function()
local function is_disabled_workspace(bufnr)
local filedir = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":h")
for _, dir in ipairs(disabled_workspaces) do
if vim.regex(vim.fs.normalize(dir)):match_str(filedir) ~= nil then
if disabled_matcher(dir):match_str(filedir) ~= nil then
if format_notify then
vim.notify(
string.format("[Conform] Formatting disabled for files under [%s].", vim.fs.normalize(dir)),
Expand All @@ -40,6 +53,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 @@ -95,6 +121,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 @@ -142,22 +170,10 @@ return function()
},
},
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
if not autoformat_allowed(bufnr) then
return
end

-- Check global toggle
if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
return
end

-- Format only modified lines if enabled
if format_modifications_only then
if format_modifications(bufnr) then
return
Expand All @@ -169,6 +185,71 @@ 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.
-- Superseded sessions from a previous run of this consumer must not be retried (re-source guard).
tools.drop_sessions("conform.nvim")
tools.resolve_runtime_tools("conform.nvim", settings.formatter_deps, function(name)
-- get_formatter_config is conform's @private API; if it vanishes, every
-- formatter is UNVERIFIABLE — report unresolved with the reason (missing
-- bucket, immediate flush) instead of silently classifying them all as
-- self-resolving, which would turn off installs and warnings wholesale.
local conform = require("conform")
if type(conform.get_formatter_config) ~= "function" then
return {
unresolved = true,
reason = "conform.get_formatter_config is unavailable (conform API drift?) — formatters cannot be verified",
}
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 (the builtin from_node_modules) resolves per
-- buffer at format time, so evaluate it against the probe-time buffer
-- the same way conform will: a project-local node_modules bin passes
-- the $PATH check as-is, and the bare-name FALLBACK ("prettier") keeps
-- the dep inside the install/warn contract — trusting the function
-- blindly would let a fresh machine with no copy anywhere silently
-- skip both the Mason install and the aggregated warning. A failed or
-- non-string evaluation degrades to self-resolving (no install/warn),
-- never to a typo report.
if type(config.command) == "function" then
local fname = vim.api.nvim_buf_get_name(0)
local cmd_ok, cmd = pcall(config.command, config, {
buf = vim.api.nvim_get_current_buf(),
filename = fname,
dirname = fname ~= "" and vim.fs.dirname(fname) or vim.fn.getcwd(),
})
return { binary = (cmd_ok and type(cmd) == "string" and cmd ~= "") and cmd or 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
-- The reason rides on the probe result: the phrasing is conform's,
-- not the shared resolver's (nvim-lint shares resolve_runtime_tools).
return { unresolved = true, reason = "config resolves per buffer and could not be verified at startup" }
end
return nil
end, nil, { defer = true })

-- User commands
vim.api.nvim_create_user_command("Format", function(args)
local range = nil
Expand Down Expand Up @@ -227,7 +308,7 @@ return function()
end
end, { nargs = 1, complete = "filetype" })

-- Auto stop shell LSPs for .env files (migrated from null-ls config).
-- Auto stop shell LSPs for .env files.
-- Both bashls and shuck attach to .env's `sh` filetype and only add noise there.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
Expand Down
Loading
Loading