(nvim) nvim v0.12 migration

This commit is contained in:
juancwu 2026-04-13 00:30:14 +00:00
commit 7b2eb8c525
51 changed files with 1629 additions and 1627 deletions

29
nvim/lsp/bashls.lua Normal file
View file

@ -0,0 +1,29 @@
---@brief
---
--- https://github.com/bash-lsp/bash-language-server
---
--- `bash-language-server` can be installed via `npm`:
--- ```sh
--- npm i -g bash-language-server
--- ```
---
--- Language server for bash, written using tree sitter in typescript.
---@type vim.lsp.Config
return {
cmd = { "bash-language-server", "start" },
settings = {
bashIde = {
-- Glob pattern for finding and parsing shell script files in the workspace.
-- Used by the background analysis features across files.
-- Prevent recursive scanning which will cause issues when opening a file
-- directly in the home directory (e.g. ~/foo.sh).
--
-- Default upstream pattern is "**/*@(.sh|.inc|.bash|.command)".
globPattern = vim.env.GLOB_PATTERN or "*@(.sh|.inc|.bash|.command)",
},
},
filetypes = { "bash", "sh" },
root_markers = { ".git" },
}

View file

@ -0,0 +1,51 @@
---@brief
---
--- https://github.com/vunguyentuan/vscode-css-variables/tree/master/packages/css-variables-language-server
---
--- CSS variables autocompletion and go-to-definition
---
--- `css-variables-language-server` can be installed via `npm`:
---
--- ```sh
--- npm i -g css-variables-language-server
--- ```
---@type vim.lsp.Config
return {
cmd = { "css-variables-language-server", "--stdio" },
filetypes = { "css", "scss", "less" },
-- Taken from lsp/ts_ls.lua to handle simple projects and monorepos.
root_dir = function(bufnr, on_dir)
local root_markers = { "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock" }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has("nvim-0.11.3") == 1 and { root_markers, { ".git" } }
or vim.list_extend(root_markers, { ".git" })
-- We fallback to the current working directory if no project root is found
local project_root = vim.fs.root(bufnr, root_markers) or vim.fn.getcwd()
on_dir(project_root)
end,
-- Same as inlined defaults that don't seem to work without hardcoding them in the lua config
-- https://github.com/vunguyentuan/vscode-css-variables/blob/763a564df763f17aceb5f3d6070e0b444a2f47ff/packages/css-variables-language-server/src/CSSVariableManager.ts#L31-L50
settings = {
cssVariables = {
lookupFiles = { "**/*.less", "**/*.scss", "**/*.sass", "**/*.css" },
blacklistFolders = {
"**/.cache",
"**/.DS_Store",
"**/.git",
"**/.hg",
"**/.next",
"**/.svn",
"**/bower_components",
"**/CVS",
"**/dist",
"**/node_modules",
"**/tests",
"**/tmp",
},
},
},
}

15
nvim/lsp/cssls.lua Normal file
View file

@ -0,0 +1,15 @@
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true
---@type vim.lsp.Config
return {
cmd = { "vscode-css-language-server", "--stdio" },
filetypes = { "css", "scss", "less" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
settings = {
css = { validate = true },
scss = { validate = true },
less = { validate = true },
},
}

99
nvim/lsp/gopls.lua Normal file
View file

@ -0,0 +1,99 @@
---@brief
---
--- https://github.com/golang/tools/tree/master/gopls
---
--- Google's lsp server for golang.
--- @class go_dir_custom_args
---
--- @field envvar_id string
---
--- @field custom_subdir string?
local mod_cache = nil
local std_lib = nil
---@param custom_args go_dir_custom_args
---@param on_complete fun(dir: string | nil)
local function identify_go_dir(custom_args, on_complete)
local cmd = { "go", "env", custom_args.envvar_id }
vim.system(cmd, { text = true }, function(output)
local res = vim.trim(output.stdout or "")
if output.code == 0 and res ~= "" then
if custom_args.custom_subdir and custom_args.custom_subdir ~= "" then
res = res .. custom_args.custom_subdir
end
on_complete(res)
else
vim.schedule(function()
vim.notify(
("[gopls] identify " .. custom_args.envvar_id .. " dir cmd failed with code %d: %s\n%s"):format(
output.code,
vim.inspect(cmd),
output.stderr
)
)
end)
on_complete(nil)
end
end)
end
---@return string?
local function get_std_lib_dir()
if std_lib and std_lib ~= "" then
return std_lib
end
identify_go_dir({ envvar_id = "GOROOT", custom_subdir = "/src" }, function(dir)
if dir then
std_lib = dir
end
end)
return std_lib
end
---@return string?
local function get_mod_cache_dir()
if mod_cache and mod_cache ~= "" then
return mod_cache
end
identify_go_dir({ envvar_id = "GOMODCACHE" }, function(dir)
if dir then
mod_cache = dir
end
end)
return mod_cache
end
---@param fname string
---@return string?
local function get_root_dir(fname)
if mod_cache and fname:sub(1, #mod_cache) == mod_cache then
local clients = vim.lsp.get_clients({ name = "gopls" })
if #clients > 0 then
return clients[#clients].config.root_dir
end
end
if std_lib and fname:sub(1, #std_lib) == std_lib then
local clients = vim.lsp.get_clients({ name = "gopls" })
if #clients > 0 then
return clients[#clients].config.root_dir
end
end
return vim.fs.root(fname, "go.work") or vim.fs.root(fname, "go.mod") or vim.fs.root(fname, ".git")
end
---@type vim.lsp.Config
return {
cmd = { "gopls" },
filetypes = { "go", "gomod", "gowork", "gotmpl" },
root_dir = function(bufnr, on_dir)
local fname = vim.api.nvim_buf_get_name(bufnr)
get_mod_cache_dir()
get_std_lib_dir()
-- see: https://github.com/neovim/nvim-lspconfig/issues/804
on_dir(get_root_dir(fname))
end,
}

16
nvim/lsp/html.lua Normal file
View file

@ -0,0 +1,16 @@
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true
---@type vim.lsp.Config
return {
cmd = { "vscode-html-language-server", "--stdio" },
filetypes = { "html", "templ" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
init_options = {
provideFormatter = true,
embeddedLanguages = { css = true, javascript = true },
configurationSection = { "html", "css", "javascript" },
},
settings = {},
}

33
nvim/lsp/lua_ls.lua Normal file
View file

@ -0,0 +1,33 @@
---@type vim.lsp.Config
return {
cmd = { "lua-language-server" },
filetypes = { "lua" },
root_markers = {
".luarc.json",
".luarc.jsonc",
".luacheckrc",
".stylua.toml",
"stylua.toml",
"selene.toml",
"selene.yml",
".git",
},
settings = {
Lua = {
runtime = {
version = "Lua 5.4",
},
completion = {
enable = true,
},
diagnostics = {
enable = true,
globals = { "vim" },
},
workspace = {
library = { vim.env.VIMRUNTIME },
checkThirdParty = false,
},
},
},
}

195
nvim/lsp/rust-analyzer.lua Normal file
View file

@ -0,0 +1,195 @@
---@brief
---
--- https://github.com/rust-lang/rust-analyzer
---
--- rust-analyzer (aka rls 2.0), a language server for Rust
---
---
--- See [docs](https://rust-analyzer.github.io/book/configuration.html) for extra settings. The settings can be used like this:
--- ```lua
--- vim.lsp.config('rust_analyzer', {
--- settings = {
--- ['rust-analyzer'] = {
--- diagnostics = {
--- enable = false;
--- }
--- }
--- }
--- })
--- ```
---
--- Note: do not set `init_options` for this LS config, it will be automatically populated by the contents of settings["rust-analyzer"] per
--- https://github.com/rust-lang/rust-analyzer/blob/eb5da56d839ae0a9e9f50774fa3eb78eb0964550/docs/dev/lsp-extensions.md?plain=1#L26.
local function reload_workspace(bufnr)
local clients = vim.lsp.get_clients({ bufnr = bufnr, name = "rust_analyzer" })
for _, client in ipairs(clients) do
vim.notify("Reloading Cargo Workspace")
---@diagnostic disable-next-line:param-type-mismatch
client:request("rust-analyzer/reloadWorkspace", nil, function(err)
if err then
error(tostring(err))
end
vim.notify("Cargo workspace reloaded")
end, 0)
end
end
local function user_sysroot_src()
return vim.tbl_get(vim.lsp.config["rust_analyzer"], "settings", "rust-analyzer", "cargo", "sysrootSrc")
end
local function default_sysroot_src()
local sysroot = vim.tbl_get(vim.lsp.config["rust_analyzer"], "settings", "rust-analyzer", "cargo", "sysroot")
if not sysroot then
local rustc = os.getenv("RUSTC") or "rustc"
local result = vim.system({ rustc, "--print", "sysroot" }, { text = true }):wait()
local stdout = result.stdout
if result.code == 0 and stdout then
if string.sub(stdout, #stdout) == "\n" then
if #stdout > 1 then
sysroot = string.sub(stdout, 1, #stdout - 1)
else
sysroot = ""
end
else
sysroot = stdout
end
end
end
return sysroot and vim.fs.joinpath(sysroot, "lib/rustlib/src/rust/library") or nil
end
local function is_library(fname)
local user_home = vim.fs.normalize(vim.env.HOME)
local cargo_home = os.getenv("CARGO_HOME") or user_home .. "/.cargo"
local registry = cargo_home .. "/registry/src"
local git_registry = cargo_home .. "/git/checkouts"
local rustup_home = os.getenv("RUSTUP_HOME") or user_home .. "/.rustup"
local toolchains = rustup_home .. "/toolchains"
local sysroot_src = user_sysroot_src() or default_sysroot_src()
for _, item in ipairs({ toolchains, registry, git_registry, sysroot_src }) do
if item and vim.fs.relpath(item, fname) then
local clients = vim.lsp.get_clients({ name = "rust_analyzer" })
return #clients > 0 and clients[#clients].config.root_dir or nil
end
end
end
---@type vim.lsp.Config
return {
cmd = { "rust-analyzer" },
filetypes = { "rust" },
root_dir = function(bufnr, on_dir)
local fname = vim.api.nvim_buf_get_name(bufnr)
local reused_dir = is_library(fname)
if reused_dir then
on_dir(reused_dir)
return
end
local cargo_crate_dir = vim.fs.root(fname, { "Cargo.toml" })
local cargo_workspace_root
if cargo_crate_dir == nil then
on_dir(
vim.fs.root(fname, { "rust-project.json" })
or vim.fs.dirname(vim.fs.find(".git", { path = fname, upward = true })[1])
)
return
end
local cmd = {
"cargo",
"metadata",
"--no-deps",
"--format-version",
"1",
"--manifest-path",
cargo_crate_dir .. "/Cargo.toml",
}
vim.system(cmd, { text = true }, function(output)
if output.code == 0 then
if output.stdout then
local result = vim.json.decode(output.stdout)
if result["workspace_root"] then
cargo_workspace_root = vim.fs.normalize(result["workspace_root"])
end
end
on_dir(cargo_workspace_root or cargo_crate_dir)
else
vim.schedule(function()
vim.notify(
("[rust_analyzer] cmd failed with code %d: %s\n%s"):format(output.code, cmd, output.stderr)
)
end)
end
end)
end,
capabilities = {
experimental = {
serverStatusNotification = true,
commands = {
commands = {
"rust-analyzer.showReferences",
"rust-analyzer.runSingle",
"rust-analyzer.debugSingle",
},
},
},
},
---@type lspconfig.settings.rust_analyzer
settings = {
["rust-analyzer"] = {
lens = {
debug = { enable = true },
enable = true,
implementations = { enable = true },
references = {
adt = { enable = true },
enumVariant = { enable = true },
method = { enable = true },
trait = { enable = true },
},
run = { enable = true },
updateTest = { enable = true },
},
},
},
before_init = function(init_params, config)
-- See https://github.com/rust-lang/rust-analyzer/blob/eb5da56d839ae0a9e9f50774fa3eb78eb0964550/docs/dev/lsp-extensions.md?plain=1#L26
if config.settings and config.settings["rust-analyzer"] then
init_params.initializationOptions = config.settings["rust-analyzer"]
end
---@param command table{ title: string, command: string, arguments: any[] }
vim.lsp.commands["rust-analyzer.runSingle"] = function(command)
local r = command.arguments[1]
local cmd = { "cargo", unpack(r.args.cargoArgs) }
if r.args.executableArgs and #r.args.executableArgs > 0 then
vim.list_extend(cmd, { "--", unpack(r.args.executableArgs) })
end
local proc = vim.system(cmd, { cwd = r.args.cwd, env = r.args.environment })
local result = proc:wait()
if result.code == 0 then
vim.notify(result.stdout, vim.log.levels.INFO)
else
vim.notify(result.stderr, vim.log.levels.ERROR)
end
end
end,
on_attach = function(_, bufnr)
vim.api.nvim_buf_create_user_command(bufnr, "LspCargoReload", function()
reload_workspace(bufnr)
end, { desc = "Reload current cargo workspace" })
end,
}

36
nvim/lsp/tailwindcss.lua Normal file
View file

@ -0,0 +1,36 @@
---@type vim.lsp.Config
return {
cmd = { "tailwindcss-language-server", "--stdio" },
filetypes = { "html", "css", "scss", "javascriptreact", "typescriptreact", "templ" },
root_markers = {
"tailwind.config.js",
"tailwind.config.cjs",
"tailwind.config.mjs",
"tailwind.config.ts",
"postcss.config.js",
"package.json",
".git",
},
init_options = {
userLanguages = {
templ = "html",
},
},
settings = {
tailwindCSS = {
validate = true,
lint = {
cssConflict = "warning",
invalidApply = "error",
invalidConfigPath = "error",
invalidScreen = "error",
invalidTailwindDirective = "error",
invalidVariant = "error",
recommendedVariantOrder = "warning",
},
includeLanguages = {
templ = "html",
},
},
},
}

6
nvim/lsp/templ.lua Normal file
View file

@ -0,0 +1,6 @@
---@type vim.lsp.Config
return {
cmd = { "templ", "lsp" },
filetypes = { "templ" },
root_markers = { "go.mod", "go.work", ".git" },
}

199
nvim/lsp/ts_ls.lua Normal file
View file

@ -0,0 +1,199 @@
---@brief
---
--- https://github.com/typescript-language-server/typescript-language-server
---
--- `ts_ls`, aka `typescript-language-server`, is a Language Server Protocol implementation for TypeScript wrapping `tsserver`. Note that `ts_ls` is not `tsserver`.
---
--- `typescript-language-server` depends on `typescript`. Both packages can be installed via `npm`:
--- ```sh
--- npm install -g typescript typescript-language-server
--- ```
---
--- To configure typescript language server, add a
--- [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) or
--- [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) to the root of your
--- project.
---
--- Here's an example that disables type checking in JavaScript files.
---
--- ```json
--- {
--- "compilerOptions": {
--- "module": "commonjs",
--- "target": "es6",
--- "checkJs": false
--- },
--- "exclude": [
--- "node_modules"
--- ]
--- }
--- ```
---
--- Use the `:LspTypescriptSourceAction` command to see "whole file" ("source") code-actions such as:
--- - organize imports
--- - remove unused code
---
--- Use the `:LspTypescriptGoToSourceDefinition` command to navigate to the source definition of a symbol (e.g., jump to the original implementation instead of type definitions).
---
--- ### Monorepo support
---
--- `ts_ls` supports monorepos by default. It will automatically find the `tsconfig.json` or `jsconfig.json` corresponding to the package you are working on.
--- This works without the need of spawning multiple instances of `ts_ls`, saving memory.
---
--- It is recommended to use the same version of TypeScript in all packages, and therefore have it available in your workspace root. The location of the TypeScript binary will be determined automatically, but only once.
---
--- Some care must be taken here to correctly infer whether a file is part of a Deno program, or a TS program that
--- expects to run in Node or Web Browsers. This supports having a Deno module using the denols LSP as a part of a
--- mostly-not-Deno monorepo. We do this by finding the nearest package manager lock file, and the nearest deno.json
--- or deno.jsonc.
---
--- Example:
---
--- ```
--- project-root
--- +-- node_modules/...
--- +-- package-lock.json
--- +-- package.json
--- +-- packages
--- +-- deno-module
--- | +-- deno.json
--- | +-- package.json <-- It's normal for Deno projects to have package.json files!
--- | +-- src
--- | +-- index.ts <-- this is a Deno file
--- +-- node-module
--- +-- package.json
--- +-- src
--- +-- index.ts <-- a non-Deno file (ie, should use ts_ls or tsgols)
--- ```
---
--- From the file being edited, we walk up to find the nearest package manager lockfile. This is PROJECT ROOT.
--- From the file being edited, find the nearest deno.json or deno.jsonc. This is DENO ROOT.
--- From the file being edited, find the nearest deno.lock. This is DENO LOCK ROOT
--- If DENO LOCK ROOT is found, and PROJECT ROOT is missing or shorter, then this is a deno file, and we abort.
--- If DENO ROOT is found, and it's longer than or equal to PROJECT ROOT, then this is a Deno file, and we abort.
--- Otherwise, attach at PROJECT ROOT, or the cwd if not found.
---@type vim.lsp.Config
return {
init_options = { hostInfo = "neovim" },
cmd = function(dispatchers, config)
local cmd = "typescript-language-server"
if (config or {}).root_dir then
local local_cmd = vim.fs.joinpath(config.root_dir, "node_modules/.bin", cmd)
if vim.fn.executable(local_cmd) == 1 then
cmd = local_cmd
end
end
return vim.lsp.rpc.start({ cmd, "--stdio" }, dispatchers)
end,
filetypes = {
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
},
root_dir = function(bufnr, on_dir)
-- The project root is where the LSP can be started from
-- As stated in the documentation above, this LSP supports monorepos and simple projects.
-- We select then from the project root, which is identified by the presence of a package
-- manager lock file.
local root_markers = { "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock" }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has("nvim-0.11.3") == 1 and { root_markers, { ".git" } }
or vim.list_extend(root_markers, { ".git" })
-- exclude deno
local deno_root = vim.fs.root(bufnr, { "deno.json", "deno.jsonc" })
local deno_lock_root = vim.fs.root(bufnr, { "deno.lock" })
local project_root = vim.fs.root(bufnr, root_markers)
if deno_lock_root and (not project_root or #deno_lock_root > #project_root) then
-- deno lock is closer than package manager lock, abort
return
end
if deno_root and (not project_root or #deno_root >= #project_root) then
-- deno config is closer than or equal to package manager lock, abort
return
end
-- project is standard TS, not deno
-- We fallback to the current working directory if no project root is found
on_dir(project_root or vim.fn.getcwd())
end,
handlers = {
-- handle rename request for certain code actions like extracting functions / types
["_typescript.rename"] = function(_, result, ctx)
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
vim.lsp.util.show_document({
uri = result.textDocument.uri,
range = {
start = result.position,
["end"] = result.position,
},
}, client.offset_encoding)
vim.lsp.buf.rename()
return vim.NIL
end,
},
commands = {
["editor.action.showReferences"] = function(command, ctx)
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
local file_uri, position, references = unpack(command.arguments)
local quickfix_items = vim.lsp.util.locations_to_items(references --[[@as any]], client.offset_encoding)
vim.fn.setqflist({}, " ", {
title = command.title,
items = quickfix_items,
context = {
command = command,
bufnr = ctx.bufnr,
},
})
vim.lsp.util.show_document({
uri = file_uri --[[@as string]],
range = {
start = position --[[@as lsp.Position]],
["end"] = position --[[@as lsp.Position]],
},
}, client.offset_encoding)
---@diagnostic enable: assign-type-mismatch
vim.cmd("botright copen")
end,
},
on_attach = function(client, bufnr)
-- ts_ls provides `source.*` code actions that apply to the whole file. These only appear in
-- `vim.lsp.buf.code_action()` if specified in `context.only`.
vim.api.nvim_buf_create_user_command(bufnr, "LspTypescriptSourceAction", function()
local source_actions = vim.tbl_filter(function(action)
return vim.startswith(action, "source.")
end, client.server_capabilities.codeActionProvider.codeActionKinds)
vim.lsp.buf.code_action({
context = {
only = source_actions,
diagnostics = {},
},
})
end, {})
-- Go to source definition command
vim.api.nvim_buf_create_user_command(bufnr, "LspTypescriptGoToSourceDefinition", function()
local win = vim.api.nvim_get_current_win()
local params = vim.lsp.util.make_position_params(win, client.offset_encoding)
client:exec_cmd({
command = "_typescript.goToSourceDefinition",
title = "Go to source definition",
arguments = { params.textDocument.uri, params.position },
}, { bufnr = bufnr }, function(err, result)
if err then
vim.notify("Go to source definition failed: " .. err.message, vim.log.levels.ERROR)
return
end
if not result or vim.tbl_isempty(result) then
vim.notify("No source definition found", vim.log.levels.INFO)
return
end
vim.lsp.util.show_document(result[1], client.offset_encoding, { focus = true })
end)
end, { desc = "Go to source definition" })
end,
}