return { 'neovim/nvim-lspconfig', event = { 'BufReadPre', 'BufNewFile' }, dependencies = { { 'williamboman/mason.nvim', build = ':MasonUpdate', cmd = 'Mason', config = true, }, 'williamboman/mason-lspconfig.nvim', 'WhoIsSethDaniel/mason-tool-installer.nvim', -- lazydev replaces neodev { 'folke/lazydev.nvim', ft = 'lua', opts = { library = { { path = '${3rd}/luv/library', words = { 'vim%.uv' } } } }, }, { 'j-hui/fidget.nvim', event = 'LspAttach', opts = { notification = { override_vim_notify = true, }, }, config = function(_, opts) require('fidget').setup(opts) local filters = { 'ExperimentalWarning: SQLite', 'DeprecationWarning.*punycode', 'Cannot find request.*whilst attempting to cancel', 'AbortError: The operation was aborted', 'rate limit exceeded', 'Rate limited by server', 'Error while parsing file://', 'Error while formatting.*Shfmt', } vim.schedule(function() local fidget_notify = vim.notify ---@diagnostic disable-next-line: duplicate-set-field vim.notify = function(msg, level, notify_opts) if type(msg) == 'string' then for _, pattern in ipairs(filters) do if msg:match(pattern) then return end end end fidget_notify(msg, level, notify_opts) end end) end, }, { 'ray-x/lsp_signature.nvim', event = 'LspAttach' }, { 'b0o/schemastore.nvim', ft = { 'json', 'yaml' } }, }, config = function() local capabilities = vim.lsp.protocol.make_client_capabilities() local has_cmp, cmp_lsp = pcall(require, 'cmp_nvim_lsp') if has_cmp then capabilities = cmp_lsp.default_capabilities(capabilities) end -- Silence yamlls dynamicRegistration warning capabilities.workspace = vim.tbl_deep_extend('force', capabilities.workspace or {}, { didChangeConfiguration = { dynamicRegistration = false }, }) local function path_exists(p) return p and p ~= '' and vim.uv.fs_stat(p) ~= nil end local function mason_bin(name) local p = vim.fs.joinpath(vim.fn.stdpath('data'), 'mason', 'bin', name) return path_exists(p) and p or name end local function julia_bin() local home = vim.env.HOME or '' for _, p in ipairs({ vim.fn.exepath('julia'), vim.fs.joinpath(home, '.juliaup', 'bin', 'julia'), '/opt/homebrew/bin/julia', '/usr/local/bin/julia', }) do if path_exists(p) then return p end end return 'julia' end -- Augroup created once; buffer-scoped autocmds clean themselves up local lsp_aug = vim.api.nvim_create_augroup('lsp_attach', { clear = true }) vim.api.nvim_create_autocmd('LspAttach', { group = lsp_aug, callback = function(args) local bufnr = args.buf local client = vim.lsp.get_client_by_id(args.data.client_id) if not client then return end -- LSP signature require('lsp_signature').on_attach({ hint_enable = false, handler_opts = { border = 'rounded' }, }, bufnr) vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' -- Keymap helper local function map(keys, func, desc, modes) vim.keymap.set(modes or 'n', keys, func, { buffer = bufnr, desc = 'LSP: ' .. (desc or keys), }) end -- Lazy-load Telescope wrapper local function tb(name) return function(...) require('lazy').load({ plugins = { 'telescope.nvim' } }) return require('telescope.builtin')[name](...) end end -- Navigation map('gd', tb('lsp_definitions'), 'Goto Definition') map('gD', vim.lsp.buf.declaration, 'Goto Declaration') map('gR', tb('lsp_references'), 'Goto References') map('gI', tb('lsp_implementations'), 'Goto Implementation') map('gy', tb('lsp_type_definitions'), 'Goto Type Definition') map('ls', tb('lsp_document_symbols'), 'Document Symbols') map('lS', tb('lsp_dynamic_workspace_symbols'), 'Workspace Symbols') -- Diagnostics map('ld', vim.diagnostic.open_float, 'Diagnostic float') map(']e', function() vim.diagnostic.jump({ count = 1, float = true }) vim.cmd('normal! zz') end, 'Next diagnostic') map('[e', function() vim.diagnostic.jump({ count = -1, float = true }) vim.cmd('normal! zz') end, 'Prev diagnostic') map(']E', function() vim.diagnostic.jump({ count = 1, severity = vim.diagnostic.severity.ERROR, float = true }) vim.cmd('normal! zz') end, 'Next error') map('[E', function() vim.diagnostic.jump({ count = -1, severity = vim.diagnostic.severity.ERROR, float = true }) vim.cmd('normal! zz') end, 'Prev error') -- Code actions & refactoring map('lr', vim.lsp.buf.rename, 'Rename') map('la', vim.lsp.buf.code_action, 'Code Action', { 'n', 'v' }) -- Documentation map('K', vim.lsp.buf.hover, 'Hover documentation') map('lk', vim.lsp.buf.signature_help, 'Signature help') vim.keymap.set('i', '', vim.lsp.buf.signature_help, { buffer = bufnr, desc = 'LSP: Signature help (insert)', }) -- Document highlight — guarded per client, per buffer if client:supports_method('textDocument/documentHighlight', bufnr) then local visual_bg = vim.fn.synIDattr(vim.fn.hlID('Visual'), 'bg') or '#3e4452' vim.api.nvim_set_hl(0, 'LspReferenceText', { bg = visual_bg }) vim.api.nvim_set_hl(0, 'LspReferenceRead', { bg = visual_bg }) vim.api.nvim_set_hl(0, 'LspReferenceWrite', { bg = visual_bg }) -- Unique group per buffer so multiple clients don't clobber each other local hl_aug = vim.api.nvim_create_augroup('lsp_highlight_' .. bufnr, { clear = true }) vim.api.nvim_create_autocmd('CursorHold', { group = hl_aug, buffer = bufnr, callback = vim.lsp.buf.document_highlight, }) vim.api.nvim_create_autocmd('CursorMoved', { group = hl_aug, buffer = bufnr, callback = vim.lsp.buf.clear_references, }) vim.api.nvim_buf_create_user_command(bufnr, 'LspToggleHighlight', function() local enabled = vim.b.lsp_highlight_enabled if enabled then vim.api.nvim_clear_autocmds({ group = hl_aug, buffer = bufnr }) vim.lsp.buf.clear_references() else vim.api.nvim_create_autocmd( 'CursorHold', { group = hl_aug, buffer = bufnr, callback = vim.lsp.buf.document_highlight } ) vim.api.nvim_create_autocmd( 'CursorMoved', { group = hl_aug, buffer = bufnr, callback = vim.lsp.buf.clear_references } ) end vim.b.lsp_highlight_enabled = not enabled vim.notify('LSP highlights ' .. (enabled and 'disabled' or 'enabled')) end, {}) vim.b[bufnr].lsp_highlight_enabled = true end end, }) local format_enabled = { bashls = true, clangd = true, gopls = true, html = true, htmx = true, jsonls = true, lua_ls = true, marksman = true, pyright = true, ruff = true, rust_analyzer = true, taplo = true, texlab = true, yamlls = true, zls = true, } vim.api.nvim_create_autocmd('LspAttach', { group = lsp_aug, callback = function(args) local client = vim.lsp.get_client_by_id(args.data.client_id) if not client then return end if not format_enabled[client.name] then client.server_capabilities.documentFormattingProvider = false client.server_capabilities.documentRangeFormattingProvider = false end end, }) local julia_cmd = julia_bin() local servers = { bashls = { filetypes = { 'sh', 'bash', 'zsh' }, -- suppress all diagnostics from bashls (too noisy) handlers = { ['textDocument/publishDiagnostics'] = function() end }, }, html = { filetypes = { 'html', 'htmldjango' }, init_options = { configurationSection = { 'html', 'css', 'javascript' }, embeddedLanguages = { css = true, javascript = true }, }, }, htmx = { cmd = { 'htmx-lsp' }, filetypes = { 'html', 'htmx' }, }, gopls = { settings = { gopls = { gofumpt = true, staticcheck = true, completeUnimported = true, usePlaceholders = true, analyses = { unusedparams = true }, }, }, }, clangd = { cmd = { 'clangd', '--background-index', '--clang-tidy', '--header-insertion=iwyu', '--completion-style=detailed', '--header-insertion-decorators', '--query-driver=/usr/bin/clang,/usr/bin/clang++', '--enable-config', }, -- offsetEncoding is clangd-specific — set here, not on global capabilities capabilities = vim.tbl_deep_extend('force', capabilities, { offsetEncoding = { 'utf-8' }, }), filetypes = { 'c', 'cpp', 'objc', 'objcpp', 'h', 'hpp', 'hxx' }, settings = { formatting = true, inlayHints = { designators = true, enabled = true, parameterNames = true, deducedTypes = true }, }, }, marksman = { filetypes = { 'markdown' }, settings = { marksman = { extensions = { 'mdx' } } }, }, jsonls = { cmd = { 'vscode-json-language-server', '--stdio' }, settings = { json = { validate = { enable = true } } }, on_attach = function(client, _) -- attach schemastore schemas after server starts local ok, schema = pcall(require, 'schemastore') if ok then client.config.settings.json.schemas = schema.json.schemas() client.notify('workspace/didChangeConfiguration', { settings = client.config.settings }) end end, }, julials = { filetypes = { 'julia' }, cmd = { julia_cmd, '--project=' .. vim.fn.expand('~/.julia/environments/nvim-lsp'), '--startup-file=no', '--history-file=no', '-e', [[ using Logging, LanguageServer, SymbolServer global_logger(ConsoleLogger(stderr, Logging.Warn)) depot = get(ENV, "JULIA_DEPOT_PATH", "") server = LanguageServer.LanguageServerInstance( stdin, stdout, something(LanguageServer.find_project_path(pwd()), pwd()), depot, ) server.runlinter = true run(server) ]], }, settings = { julia = { lint = { run = true } } }, }, pyright = { settings = { python = { analysis = { autoSearchPaths = true, diagnosticMode = 'workspace', useLibraryCodeForTypes = true, typeCheckingMode = 'none', }, }, }, }, ruff = { filetypes = { 'python' }, on_attach = function(client) client.server_capabilities.hoverProvider = false end, }, rust_analyzer = { settings = { ['rust-analyzer'] = { imports = { granularity = { group = 'module' }, prefix = 'self' }, cargo = { buildScripts = { enable = true } }, procMacro = { enable = true }, checkOnSave = { command = 'clippy' }, }, }, }, taplo = { filetypes = { 'toml' } }, yamlls = { settings = { yaml = { schemaStore = { enable = true }, validate = true }, }, }, texlab = { filetypes = { 'tex', 'plaintex', 'bib', 'cls', 'sty' }, settings = { texlab = { build = { onSave = false }, auxDirectory = 'output', diagnostics = { ignoredPatterns = { '^Overfull \\\\hbox', '^Underfull \\\\hbox', '^Package.*Warning', }, }, }, }, }, lua_ls = { cmd = { mason_bin('lua-language-server') }, settings = { Lua = { workspace = { checkThirdParty = false }, telemetry = { enable = false }, diagnostics = { globals = { 'vim' } }, }, }, }, sqls = { filetypes = { 'sql', 'mysql', 'plsql', 'postgresql' }, settings = { sql = { connections = { { driver = 'sqlite3', dataSourceName = 'file::memory:?cache=shared' } } }, }, on_init = function(client) local root = client.config.root_dir or vim.fn.getcwd() local dbs = vim.list_extend(vim.fn.globpath(root, '*.db', false, true), vim.fn.globpath(root, '*.sqlite', false, true)) if #dbs > 0 then client.config.settings.sql.connections = vim.tbl_map(function(p) return { driver = 'sqlite3', dataSourceName = vim.fn.fnamemodify(p, ':p') } end, dbs) client.notify('workspace/didChangeConfiguration', { settings = client.config.settings }) end end, }, zls = { filetypes = { 'zig' } }, } require('mason-lspconfig').setup({ ensure_installed = vim.tbl_filter(function(n) return n ~= 'julials' end, vim.tbl_keys(servers)), automatic_enable = false, }) for name, config in pairs(servers) do vim.lsp.config( name, vim.tbl_deep_extend('force', { capabilities = capabilities, }, config) ) vim.lsp.enable(name) end vim.diagnostic.config({ underline = true, severity_sort = true, signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', [vim.diagnostic.severity.WARN] = 'W', [vim.diagnostic.severity.HINT] = 'H', [vim.diagnostic.severity.INFO] = 'I', }, numhl = { [vim.diagnostic.severity.WARN] = 'WarningMsg' }, }, virtual_text = { spacing = 2, prefix = '●' }, float = { source = 'if_many', border = 'rounded' }, }) require('mason-tool-installer').setup({ ensure_installed = { -- LSP servers (formatters/linters installed via mason-lspconfig separately) 'lua-language-server', 'bash-language-server', 'pyright', 'ruff', -- Formatters (from formatting.lua) 'stylua', 'yamlfmt', 'shfmt', 'sqlfluff', 'jq', 'prettierd', 'prettier', -- Linters (from linting.lua) 'mypy', 'golangci-lint', 'yamllint', 'markdownlint', 'shellcheck', 'luacheck', }, auto_update = false, run_on_start = true, start_delay = 3000, -- defer 3s after startup so it doesn't block anything }) end, }