nvim/lua/custom/plugins/lsp-config.lua
Jeremie Fraeys 4742e3a251
Some checks are pending
Check Lua Formatting in MyRepo / Stylua Check (push) Waiting to run
refactor: reorganize config and simplify README
2026-02-08 14:48:48 -05:00

522 lines
15 KiB
Lua
Executable file

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('<leader>lT', telescope_builtin('lsp_type_definitions'), '[T]ype Definition')
map('<leader>ls', telescope_builtin('lsp_document_symbols'), '[D]ocument [S]ymbols')
map('<leader>lS', telescope_builtin('lsp_dynamic_workspace_symbols'), '[W]orkspace [S]ymbols')
-- Diagnostics
map('<leader>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('<leader>lr', vim.lsp.buf.rename, 'Rename')
map('<leader>la', vim.lsp.buf.code_action, 'Code Action', { 'n', 'v' })
-- Documentation
map('K', vim.lsp.buf.hover, 'Hover Documentation')
map('<C-k>', 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,
}