return { 'neovim/nvim-lspconfig', event = { 'BufReadPre', 'BufNewFile' }, dependencies = { -- Mason core { 'williamboman/mason.nvim', build = ':MasonUpdate', cmd = 'Mason', config = true }, 'williamboman/mason-lspconfig.nvim', 'WhoIsSethDaniel/mason-tool-installer.nvim', -- Lua dev { 'folke/neodev.nvim', ft = 'lua', config = true }, -- LSP UI & UX { 'j-hui/fidget.nvim', opts = {}, event = 'LspAttach' }, { 'ray-x/lsp_signature.nvim', event = 'LspAttach' }, -- Python venv selector { 'linux-cultist/venv-selector.nvim', ft = 'python', opts = { auto_activate = true }, config = true, }, -- JSON Schema support { 'b0o/schemastore.nvim', ft = { 'json', 'yaml' } }, }, config = function() require('neodev').setup() -- Enhanced capabilities for LSP servers local capabilities = vim.lsp.protocol.make_client_capabilities() local has_cmp, cmp_nvim_lsp = pcall(require, 'cmp_nvim_lsp') if has_cmp then capabilities = cmp_nvim_lsp.default_capabilities(capabilities) end capabilities.offsetEncoding = { 'utf-8' } -- Disable dynamicRegistration to silence yamlls warning capabilities.workspace = capabilities.workspace or {} capabilities.workspace.didChangeConfiguration = capabilities.workspace.didChangeConfiguration or {} capabilities.workspace.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) if path_exists(p) then return p end return name end local function julia_bin() local home = vim.env.HOME or '' local candidates = { vim.fn.exepath('julia'), vim.fs.joinpath(home, '.juliaup', 'bin', 'julia'), '/opt/homebrew/bin/julia', '/usr/local/bin/julia', } for _, p in ipairs(candidates) do if path_exists(p) then return p end end return 'julia' end -- Servers that should provide formatting local format_enabled_servers = { 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, } -- Default on_attach function local on_attach = function(client, bufnr) -- Enable formatting only for supported servers if not format_enabled_servers[client.name] then client.server_capabilities.documentFormattingProvider = false client.server_capabilities.documentRangeFormattingProvider = false end vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' -- LSP signature help require('lsp_signature').on_attach({ hint_enable = false, handler_opts = { border = 'rounded' }, }, bufnr) -- Keymap helper local function map(keys, func, desc, modes) modes = modes or 'n' vim.keymap.set(modes, keys, func, { buffer = bufnr, desc = desc and 'LSP: ' .. desc or nil, }) end -- Lazy-load Telescope for LSP local function telescope_builtin(name) return function(...) require('lazy').load({ plugins = { 'telescope.nvim' } }) return require('telescope.builtin')[name](...) end end -- Navigation map('gd', telescope_builtin('lsp_definitions'), '[G]oto [D]efinition') map('gD', vim.lsp.buf.declaration, '[G]oto [D]eclaration') map('gr', telescope_builtin('lsp_references'), '[G]oto [R]eferences') map('gI', telescope_builtin('lsp_implementations'), '[G]oto [I]mplementation') map('lT', telescope_builtin('lsp_type_definitions'), '[T]ype Definition') map('ls', telescope_builtin('lsp_document_symbols'), '[D]ocument [S]ymbols') map('lS', telescope_builtin('lsp_dynamic_workspace_symbols'), '[W]orkspace [S]ymbols') -- Diagnostics map('ld', vim.diagnostic.open_float, 'Show line [E]rrors') map('[d', vim.diagnostic.get_prev, 'Previous Diagnostic') map(']d', vim.diagnostic.get_next, 'Next Diagnostic') -- 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('', vim.lsp.buf.signature_help, 'Signature Documentation') -- Document highlight if client.server_capabilities.documentHighlightProvider then local highlight_group = vim.api.nvim_create_augroup('lsp_document_highlight', { clear = true }) 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 }) vim.o.updatetime = math.max(vim.o.updatetime, 500) local function toggle_lsp_highlight(enable) if enable then vim.api.nvim_create_autocmd('CursorHold', { group = highlight_group, buffer = bufnr, callback = function() if client and client.server_capabilities.documentHighlightProvider then vim.lsp.buf.document_highlight() end end, }) vim.api.nvim_create_autocmd('CursorMoved', { group = highlight_group, buffer = bufnr, callback = vim.lsp.buf.clear_references, }) else vim.api.nvim_clear_autocmds({ group = highlight_group, buffer = bufnr }) vim.lsp.buf.clear_references() end end vim.api.nvim_buf_create_user_command(bufnr, 'LspToggleHighlight', function() local enabled = vim.b.lsp_highlight_enabled or false toggle_lsp_highlight(not enabled) vim.b.lsp_highlight_enabled = not enabled print('LSP document highlights ' .. (enabled and 'disabled' or 'enabled')) end, {}) toggle_lsp_highlight(true) end end -- Default LSP configuration local default_config = { capabilities = capabilities, on_attach = on_attach, autostart = true, } local julia_cmd = julia_bin() local julia_ls_project = vim.fn.expand('~/.julia/environments/nvim-lsp') -- Server-specific configurations local servers = { bashls = { filetypes = { 'sh', 'bash', 'zsh' }, }, 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', }, settings = { formatting = true, inlayHints = { designators = true, enabled = true, parameterNames = true, deducedTypes = true, }, }, filetypes = { 'c', 'cpp', 'objc', 'objcpp', 'h', 'hpp', 'hxx' }, }, marksman = { filetypes = { 'markdown' }, settings = { marksman = { extensions = { 'mdx' }, }, }, }, jsonls = { cmd = { 'vscode-json-language-server', '--stdio' }, settings = { json = { validate = { enable = true }, }, }, }, julials = { filetypes = { 'julia' }, cmd = { julia_cmd, '--project=' .. julia_ls_project, '--startup-file=no', '--history-file=no', '-e', [[ using Logging using LanguageServer using SymbolServer global_logger(ConsoleLogger(stderr, Logging.Warn)) function project_path() try return LanguageServer.find_project_path(pwd()) catch return pwd() end end depot_path = get(ENV, "JULIA_DEPOT_PATH", "") server = LanguageServer.LanguageServerInstance( stdin, stdout, something(project_path(), pwd()), depot_path, ) server.runlinter = true run(server) ]], }, settings = { julia = { lint = { run = true, }, }, }, on_attach = function(client, bufnr) on_attach(client, bufnr) -- Julia must never format via LSP client.server_capabilities.documentFormattingProvider = false client.server_capabilities.documentRangeFormattingProvider = false end, }, pyright = { settings = { python = { analysis = { autoSearchPaths = true, diagnosticMode = 'workspace', useLibraryCodeForTypes = true, typeCheckingMode = 'none', reportGeneralTypeIssues = false, }, }, }, }, ruff = { filetypes = { 'python' }, on_init = function() vim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('lsp_attach_disable_ruff_hover', { clear = true }), callback = function(args) local client = vim.lsp.get_client_by_id(args.data.client_id) if client and client.name == 'ruff' then -- Disable Ruff hover client.server_capabilities.hoverProvider = false end end, desc = 'LSP: Disable hover capability from Ruff', }) 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, }, diagnostics = { ignoredPatterns = { '^Overfull \\\\hbox', '^Underfull \\\\hbox', '^Package.*Warning', }, }, auxDirectory = 'output', }, }, }, 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_dir = client.config.root_dir or vim.fn.getcwd() local db_files = vim.fn.globpath(root_dir, '*.db', false, true) vim.list_extend(db_files, vim.fn.globpath(root_dir, '*.sqlite', false, true)) if #db_files > 0 then local connections = {} for _, path in ipairs(db_files) do table.insert(connections, { driver = 'sqlite3', dataSourceName = vim.fn.fnamemodify(path, ':p'), }) end client.config.settings.sql.connections = connections client.notify('workspace/didChangeConfiguration', { settings = client.config.settings }) end end, }, zls = { filetypes = { 'zig' }, }, } -- Use Mason to ensure servers are installed local ensure_installed = vim.tbl_keys(servers) ensure_installed = vim.tbl_filter(function(name) return name ~= 'julials' end, ensure_installed) require('mason-lspconfig').setup({ ensure_installed = ensure_installed, automatic_enable = false, }) local function get_lspconfig_defaults(server_name) local ok, cfg = pcall(require, 'lspconfig.configs.' .. server_name) if ok and cfg and cfg.default_config then local defaults = vim.deepcopy(cfg.default_config) if type(defaults.root_dir) == 'function' then local orig_root_dir = defaults.root_dir defaults.root_dir = function(bufnr, on_dir) local fname = bufnr if type(bufnr) == 'number' then fname = vim.api.nvim_buf_get_name(bufnr) if fname == '' or type(fname) ~= 'string' or fname:match('^%w+://') then fname = vim.fn.getcwd() end end local root = orig_root_dir(fname) if type(on_dir) == 'function' then on_dir(root) return end return root end end return defaults end return {} end -- Setup and enable LSP servers for server_name, config in pairs(servers) do if config then local lspconfig_defaults = get_lspconfig_defaults(server_name) -- Merge default config with server-specific config local merged_config = vim.tbl_deep_extend('force', lspconfig_defaults, default_config, config) -- Configure and enable the server vim.lsp.config(server_name, merged_config) vim.lsp.enable(server_name) end end -- Diagnostics configuration 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', }, linehl = { [vim.diagnostic.severity.ERROR] = 'ErrorMsg', }, numhl = { [vim.diagnostic.severity.WARN] = 'WarningMsg', }, }, virtual_text = { spacing = 2, prefix = '●' }, float = { source = 'if_many', border = 'rounded' }, }) end, }