Files
nixos-dotfiles/config/nvim/lua/lsp-nix.lua
T

214 lines
8.1 KiB
Lua

local augroup = vim.api.nvim_create_augroup("UserConfig", { clear = false })
-- Diagnostic icons shown in the sign column and virtual text
local diagnostic_signs = {
Error = " ",
Warn = " ",
Hint = "",
Info = "",
}
-- Global diagnostic display options
vim.diagnostic.config({
virtual_text = { prefix = "●", spacing = 4 }, -- inline diagnostic text to the right
signs = {
text = {
[vim.diagnostic.severity.ERROR] = diagnostic_signs.Error,
[vim.diagnostic.severity.WARN] = diagnostic_signs.Warn,
[vim.diagnostic.severity.INFO] = diagnostic_signs.Info,
[vim.diagnostic.severity.HINT] = diagnostic_signs.Hint,
},
},
underline = true,
update_in_insert = false, -- do not update diagnostics while typing (reduces noise)
severity_sort = true, -- show errors before warnings in lists
float = {
border = "rounded",
source = "always", -- always show which LSP server produced the diagnostic
header = "",
prefix = "",
focusable = false,
style = "minimal",
},
})
-- Patch vim.lsp.util.open_floating_preview so every LSP floating window
-- (hover, signature help, etc.) gets a rounded border without having to
-- configure it per-server.
do
local orig = vim.lsp.util.open_floating_preview
function vim.lsp.util.open_floating_preview(contents, syntax, opts, ...)
opts = opts or {}
opts.border = opts.border or "rounded"
return orig(contents, syntax, opts, ...)
end
end
-- lsp_on_attach is called whenever an LSP server attaches to a buffer.
-- It sets up buffer-local keymaps that are only active when an LSP is present.
local function lsp_on_attach(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if not client then
return
end
local bufnr = ev.buf
local function map(key, fn, desc)
vim.keymap.set("n", key, fn, { noremap = true, silent = true, buffer = bufnr, desc = desc })
end
-- Navigation
map("<leader>gd", vim.lsp.buf.definition, "Go to definition")
map("<leader>gS", function() vim.cmd("vsplit") vim.lsp.buf.definition() end, "Go to definition (vsplit)")
-- Code actions and refactoring
map("<leader>ca", vim.lsp.buf.code_action, "Code action")
map("<leader>rn", vim.lsp.buf.rename, "Rename symbol")
-- Diagnostics
map("<leader>D", function() vim.diagnostic.open_float({ scope = "line" }) end, "Line diagnostics")
map("<leader>d", function() vim.diagnostic.open_float({ scope = "cursor" }) end, "Cursor diagnostic")
map("<leader>nd", function() vim.diagnostic.jump({ count = 1 }) end, "Next diagnostic")
map("<leader>pd", function() vim.diagnostic.jump({ count = -1 }) end, "Prev diagnostic")
-- Hover documentation
map("K", vim.lsp.buf.hover, "Hover docs")
-- fzf-lua LSP pickers
map("<leader>fd", function() require("fzf-lua").lsp_definitions({ jump_to_single_result = true }) end, "Definitions")
map("<leader>fr", function() require("fzf-lua").lsp_references() end, "References")
map("<leader>ft", function() require("fzf-lua").lsp_typedefs() end, "Type definitions")
map("<leader>fs", function() require("fzf-lua").lsp_document_symbols() end, "Document symbols")
map("<leader>fw", function() require("fzf-lua").lsp_workspace_symbols() end, "Workspace symbols")
map("<leader>fi", function() require("fzf-lua").lsp_implementations() end, "Implementations")
-- Organise imports then format (only registered when the server supports codeAction)
if client:supports_method("textDocument/codeAction", bufnr) then
map("<leader>oi", function()
vim.lsp.buf.code_action({
context = { only = { "source.organizeImports" }, diagnostics = {} },
apply = true,
bufnr = bufnr,
})
-- Small delay lets the import action settle before formatting
vim.defer_fn(function()
vim.lsp.buf.format({ bufnr = bufnr })
end, 50)
end, "Organize imports")
end
end
vim.api.nvim_create_autocmd("LspAttach", { group = augroup, callback = lsp_on_attach })
-- Global diagnostic keymaps (available without an LSP attached)
vim.keymap.set("n", "<leader>q", function() vim.diagnostic.setloclist({ open = true }) end, { desc = "Open diagnostic list" })
-- blink.cmp: completion engine wired to LuaSnip for snippet expansion
require("blink.cmp").setup({
enabled = function()
return not vim.tbl_contains({ "markdown", "text" }, vim.bo.filetype)
end,
keymap = {
preset = "none", -- start from a blank slate; all bindings are explicit
["<C-Space>"] = { "show", "hide" }, -- toggle the completion menu
["<CR>"] = { "accept", "fallback" }, -- confirm selection
["<C-j>"] = { "select_next", "fallback" },
["<C-k>"] = { "select_prev", "fallback" },
["<Tab>"] = { "snippet_forward", "fallback" }, -- jump to next snippet placeholder
["<S-Tab>"] = { "snippet_backward", "fallback" }, -- jump to previous placeholder
},
appearance = { nerd_font_variant = "mono" },
completion = { menu = { auto_show = true } }, -- show the menu automatically (no manual trigger needed)
sources = { default = { "lsp", "path", "buffer", "snippets" } },
snippets = {
-- Delegate snippet expansion to LuaSnip
expand = function(snippet)
require("luasnip").lsp_expand(snippet)
end,
},
fuzzy = {
implementation = "prefer_rust", -- use the faster Rust fuzzy-matching backend when available
prebuilt_binaries = { download = true }, -- automatically download the pre-built binary
},
})
-- Inject blink.cmp capabilities into every LSP server so they can offer
-- completions that respect snippet and LSP-specific completion item fields
vim.lsp.config["*"] = {
capabilities = require("blink.cmp").get_lsp_capabilities(),
}
-- Server configurations
vim.lsp.config("lua_ls", {
settings = {
Lua = {
diagnostics = { globals = { "vim" } }, -- teach lua_ls that `vim` is a known global
telemetry = { enable = false },
},
},
})
vim.lsp.config("pyright", {}) -- Python type checker and language server
vim.lsp.config("bashls", {}) -- Bash language server
vim.lsp.config("nil_ls", {}) -- Nix language server
vim.lsp.config("vtsls", {}) -- TypeScript / JavaScript language server (VS Code's TS server)
vim.lsp.config("gopls", {}) -- Go language server
vim.lsp.config("omnisharp", {}) -- C# language server
-- EFM (efm-langserver): a generic LSP adapter that runs command-line linters
-- and formatters and exposes them as LSP capabilities.
do
local luacheck = require("efmls-configs.linters.luacheck")
local stylua = require("efmls-configs.formatters.stylua")
local flake8 = require("efmls-configs.linters.flake8")
local black = require("efmls-configs.formatters.black")
local prettier_d = require("efmls-configs.formatters.prettier_d")
local eslint_d = require("efmls-configs.linters.eslint_d")
local fixjson = require("efmls-configs.formatters.fixjson")
local shellcheck = require("efmls-configs.linters.shellcheck")
local shfmt = require("efmls-configs.formatters.shfmt")
local go_revive = require("efmls-configs.linters.go_revive")
local gofumpt = require("efmls-configs.formatters.gofumpt")
local csharpier = { formatCommand = "dotnet csharpier --write-stdout", formatStdin = true }
vim.lsp.config("efm", {
filetypes = {
"cs", "css", "go", "html",
"javascript", "javascriptreact",
"json", "jsonc",
"lua", "markdown", "python", "sh",
"typescript", "typescriptreact",
},
init_options = { documentFormatting = true },
settings = {
languages = {
cs = { csharpier },
go = { gofumpt, go_revive },
css = { prettier_d },
html = { prettier_d },
javascript = { eslint_d, prettier_d },
javascriptreact = { eslint_d, prettier_d },
json = { eslint_d, fixjson },
jsonc = { eslint_d, fixjson },
lua = { luacheck, stylua },
markdown = { prettier_d },
python = { flake8, black },
sh = { shellcheck, shfmt },
typescript = { eslint_d, prettier_d },
typescriptreact = { eslint_d, prettier_d },
},
},
})
end
-- Start all configured LSP servers
vim.lsp.enable({
"lua_ls",
"pyright",
"bashls",
"nil_ls",
"vtsls",
"gopls",
"omnisharp",
"efm",
})