diff options
Diffstat (limited to 'common/config/nvim/lua/plugins/lsp.lua')
| -rwxr-xr-x | common/config/nvim/lua/plugins/lsp.lua | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/common/config/nvim/lua/plugins/lsp.lua b/common/config/nvim/lua/plugins/lsp.lua new file mode 100755 index 0000000..5ed1152 --- /dev/null +++ b/common/config/nvim/lua/plugins/lsp.lua @@ -0,0 +1,674 @@ +local M = {} + +-- Safe require helper +local function safe_require(name) + local ok, mod = pcall(require, name) + return ok and mod or nil +end + +-- Autocmd groups for managing event listeners +local augroup_format = vim.api.nvim_create_augroup("LspFormattingOnSave", { clear = true }) +local augroup_diag_float = vim.api.nvim_create_augroup("ShowLineDiagnostics", { clear = true }) +local augroup_diag_load = vim.api.nvim_create_augroup("OpenDiagnosticsOnLoad", { clear = true }) +local augroup_highlight = vim.api.nvim_create_augroup("LspDocumentHighlight", { clear = true }) + +-- Border for floating windows +local border = { + { "┌", "FloatBorder" }, { "─", "FloatBorder" }, { "┐", "FloatBorder" }, + { "│", "FloatBorder" }, { "┘", "FloatBorder" }, { "─", "FloatBorder" }, + { "└", "FloatBorder" }, { "│", "FloatBorder" } +} + +-- Initialize LSP modules +local function init_modules() + -- Silently try to load each module + M.lspconfig = safe_require("lspconfig") + M.mason = safe_require("mason") + M.mason_lspconfig = safe_require("mason-lspconfig") + M.mason_tool_installer = safe_require("mason-tool-installer") + M.null_ls = safe_require("null-ls") + + if M.null_ls then + M.builtins = M.null_ls.builtins + end + + return true +end + +-- Check Neovim version compatibility and feature availability +local function has_feature(feature) + if feature == "diagnostic_api" then + return vim.fn.has("nvim-0.6") == 1 + elseif feature == "native_lsp_config" then + -- Check for both vim.lsp.enable AND vim.lsp.config + return vim.fn.has("nvim-0.11") == 1 and vim.lsp.enable ~= nil + elseif feature == "lsp_get_client_by_id" then + return vim.fn.has("nvim-0.10") == 1 + elseif feature == "cmp_nvim_lsp" then + return pcall(require, "cmp_nvim_lsp") + elseif feature == "virtual_text_disabled_by_default" then + return vim.fn.has("nvim-0.11") == 1 + elseif feature == "deprecated_lsp_handlers" then + -- vim.lsp.handlers.hover and signature_help deprecated in 0.12, removed in 0.13 + return vim.fn.has("nvim-0.12") == 0 + elseif feature == "new_lsp_config_api" then + -- New LSP config API available from 0.12+ + return vim.fn.has("nvim-0.12") == 1 and vim.lsp.config ~= nil + end + return false +end + +-- Backwards compatible capabilities setup +local function setup_capabilities() + local capabilities + + if has_feature("cmp_nvim_lsp") then + capabilities = require('cmp_nvim_lsp').default_capabilities() + elseif vim.lsp.protocol and vim.lsp.protocol.make_client_capabilities then + capabilities = vim.lsp.protocol.make_client_capabilities() + else + capabilities = {} + end + + -- Add snippet support if available + if capabilities.textDocument then + capabilities.textDocument.completion = capabilities.textDocument.completion or {} + capabilities.textDocument.completion.completionItem = + capabilities.textDocument.completion.completionItem or {} + capabilities.textDocument.completion.completionItem.snippetSupport = true + end + + -- Set offset encoding for newer versions (0.11+ supports utf-8 and utf-32) + if vim.fn.has("nvim-0.11") == 1 then + capabilities.offsetEncoding = { "utf-8", "utf-32", "utf-16" } + elseif vim.fn.has("nvim-0.9") == 1 then + capabilities.offsetEncoding = { "utf-8", "utf-16" } + end + + return capabilities +end + +-- Default LSP keymaps (fallback if external not available) +local function setup_fallback_keymaps(bufnr) + -- Only set up minimal fallbacks, prefer external setup + local opts = { buffer = bufnr, silent = true, noremap = true } + vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts) + vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts) + vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts) + vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts) +end + +-- Create LSP directory and config files for native LSP +local function setup_native_lsp_configs() + local config_path = vim.fn.stdpath("config") + local lsp_dir = config_path .. "/lsp" + + -- Create lsp directory if it doesn't exist + vim.fn.mkdir(lsp_dir, "p") + + -- LSP server configurations for native config + local server_configs = { + lua_ls = { + cmd = { "lua-language-server" }, + filetypes = { "lua" }, + root_markers = { ".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml" }, + settings = { + Lua = { + diagnostics = { + globals = { "vim", "use", "_G", "packer_plugins", "P" }, + disable = { + "undefined-global", + "lowercase-global", + "unused-local", + "unused-vararg", + "trailing-space" + }, + }, + workspace = { + library = { + vim.env.VIMRUNTIME, + "${3rd}/luv/library", + "${3rd}/busted/library", + }, + checkThirdParty = false, + }, + telemetry = { + enable = false, + }, + }, + }, + }, + + pyright = { + cmd = { "pyright-langserver", "--stdio" }, + filetypes = { "python" }, + root_markers = { "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json" }, + settings = { + python = { + formatting = { + provider = "none" + } + } + } + }, + + ts_ls = { + cmd = { "typescript-language-server", "--stdio" }, + filetypes = { "javascript", "javascriptreact", "javascript.jsx", "typescript", "typescriptreact", "typescript.tsx" }, + root_markers = { "tsconfig.json", "jsconfig.json", "package.json" }, + init_options = { + disableAutomaticTypeAcquisition = true + }, + }, + + rust_analyzer = { + cmd = { "rust-analyzer" }, + filetypes = { "rust" }, + root_markers = { "Cargo.toml", "rust-project.json" }, + }, + + clangd = { + cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu" }, + filetypes = { "c", "cpp", "objc", "objcpp", "cuda", "proto" }, + root_markers = { ".clangd", ".clang-tidy", ".clang-format", "compile_commands.json", "compile_flags.txt", "configure.ac" }, + }, + + gopls = { + cmd = { "gopls" }, + filetypes = { "go", "gomod", "gowork", "gotmpl" }, + root_markers = { "go.work", "go.mod" }, + settings = { + gopls = { + gofumpt = true, + codelenses = { + gc_details = false, + generate = true, + regenerate_cgo = true, + run_govulncheck = true, + test = true, + tidy = true, + upgrade_dependency = true, + vendor = true, + }, + hints = { + assignVariableTypes = true, + compositeLiteralFields = true, + compositeLiteralTypes = true, + constantValues = true, + functionTypeParameters = true, + parameterNames = true, + rangeVariableTypes = true, + }, + analyses = { + fieldalignment = true, + nilness = true, + unusedparams = true, + unusedwrite = true, + useany = true, + }, + usePlaceholders = true, + completeUnimported = true, + staticcheck = true, + directoryFilters = { "-.git", "-.vscode", "-.idea", "-.vscode-test", "-node_modules" }, + semanticTokens = true, + }, + }, + }, + + -- Add more basic configs + bashls = { + cmd = { "bash-language-server", "start" }, + filetypes = { "sh", "bash" }, + }, + + --html = { + -- cmd = { "vscode-html-language-server", "--stdio" }, + -- filetypes = { "html" }, + --}, + + --cssls = { + -- cmd = { "vscode-css-language-server", "--stdio" }, + -- filetypes = { "css", "scss", "less" }, + --}, + + --jsonls = { + -- cmd = { "vscode-json-language-server", "--stdio" }, + -- filetypes = { "json", "jsonc" }, + --}, + + yamlls = { + cmd = { "yaml-language-server", "--stdio" }, + filetypes = { "yaml", "yml" }, + }, + } + + -- Write config files to lsp directory + for server_name, config in pairs(server_configs) do + local file_path = lsp_dir .. "/" .. server_name .. ".lua" + local file_content = "return " .. vim.inspect(config) + + -- Only write if file doesn't exist to avoid overwriting user customizations + if vim.fn.filereadable(file_path) == 0 then + local file = io.open(file_path, "w") + if file then + file:write(file_content) + file:close() + vim.notify("Created LSP config: " .. file_path, vim.log.levels.DEBUG) + end + end + end + + return vim.tbl_keys(server_configs) +end + +-- Set up LSP on_attach function +local function create_on_attach() + return function(client, bufnr) + -- Your existing keymap setup function from keys.lua + if _G.setup_lsp_keymaps then + _G.setup_lsp_keymaps(bufnr) + else + setup_fallback_keymaps(bufnr) + end + + -- Disable LSP formatting in favor of null-ls (if null-ls is available) + if M.null_ls then + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + end + + -- Disable specific LSP capabilities to avoid conflicts + if client.name == "ruff" then + -- Disable ruff hover in favor of Pyright + client.server_capabilities.hoverProvider = false + elseif client.name == "ts_ls" then + -- Disable ts_ls formatting in favor of prettier via null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + elseif client.name == "pyright" and M.null_ls then + -- Disable pyright formatting in favor of black/isort via null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + end + + -- Set log level (backwards compatible) + if vim.lsp.set_log_level then + vim.lsp.set_log_level("warn") + end + + -- Document highlight on cursor hold + if client.server_capabilities and client.server_capabilities.documentHighlightProvider then + vim.api.nvim_create_autocmd("CursorHold", { + group = augroup_highlight, + buffer = bufnr, + callback = function() + if vim.lsp.buf.document_highlight then + vim.lsp.buf.document_highlight() + end + end, + }) + vim.api.nvim_create_autocmd("CursorMoved", { + group = augroup_highlight, + buffer = bufnr, + callback = function() + if vim.lsp.buf.clear_references then + vim.lsp.buf.clear_references() + end + end, + }) + end + end +end + +-- Set up basic LSP configuration +function M.setup() + -- Initialize all required modules + init_modules() + + -- Enable virtual_text diagnostics by default for 0.11+ (since it's disabled by default) + if has_feature("virtual_text_disabled_by_default") then + vim.diagnostic.config({ virtual_text = true }) + end + + -- Set up Mason if available (useful for tool management) + if M.mason then + M.mason.setup({ + ui = { + border = 'rounded', + icons = { + package_installed = '✓', + package_pending = '➜', + package_uninstalled = '✗' + } + } + }) + end + + -- Set up mason-tool-installer if available + if M.mason_tool_installer then + M.mason_tool_installer.setup({ + ensure_installed = { + -- Language servers + "lua-language-server", "pyright", "typescript-language-server", "rust-analyzer", + "clangd", "bash-language-server", "yaml-language-server", + -- Formatters + "stylua", "clang-format", "prettier", "shfmt", "black", "isort", "goimports", + "sql-formatter", "shellharden", + -- Linters/Diagnostics + "eslint_d", "selene", "flake8", "dotenv-linter", "phpcs", + -- Utilities + "jq" + }, + auto_update = false, + run_on_start = true, + start_delay = 3000, + }) + end + + -- Set up null-ls if available + if M.null_ls and M.builtins then + local sources = { + M.builtins.diagnostics.selene.with({ + condition = function(utils) + return utils.root_has_file({"selene.toml"}) + end, + }), + M.builtins.diagnostics.dotenv_linter, + M.builtins.diagnostics.tidy, + M.builtins.diagnostics.phpcs.with({ + condition = function(utils) + return utils.root_has_file({"phpcs.xml", "phpcs.xml.dist", ".phpcs.xml", ".phpcs.xml.dist"}) + end, + }), + + -- Formatters (prioritized over LSP formatting) + M.builtins.formatting.stylua.with({ + extra_args = { "--quote-style", "AutoPreferSingle", "--indent-width", "2", "--column-width", "160" }, + condition = function(utils) + return utils.root_has_file({"stylua.toml", ".stylua.toml"}) + end, + }), + M.builtins.formatting.prettier.with({ + extra_args = { "--single-quote", "--tab-width", "4", "--print-width", "100" }, + filetypes = { "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "css", "scss", "less", "html", "json", "jsonc", "yaml", "markdown", "graphql", "handlebars" }, + prefer_local = "node_modules/.bin", + }), + M.builtins.formatting.black.with({ + extra_args = { "--fast" }, + prefer_local = ".venv/bin", + }), + M.builtins.formatting.isort.with({ + extra_args = { "--profile", "black" }, + prefer_local = ".venv/bin", + }), + M.builtins.formatting.goimports, + M.builtins.formatting.clang_format.with({ + extra_args = { "--style", "{BasedOnStyle: Google, IndentWidth: 4}" } + }), + M.builtins.formatting.shfmt.with({ + extra_args = { "-i", "2", "-ci" } + }), + M.builtins.formatting.shellharden, + M.builtins.formatting.sql_formatter, + M.builtins.formatting.dart_format, + + -- Code actions + M.builtins.code_actions.gitsigns, + M.builtins.code_actions.gitrebase, + } + + M.null_ls.setup({ + sources = sources, + update_in_insert = false, + on_attach = function(client, bufnr) + -- Disable LSP formatting in favor of null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + + local function lsp_supports_method(client, method) + if client.supports_method then + return client:supports_method(method) + elseif client.server_capabilities then + local capability_map = { + ["textDocument/formatting"] = "documentFormattingProvider", + ["textDocument/rangeFormatting"] = "documentRangeFormattingProvider", + ["textDocument/hover"] = "hoverProvider", + ["textDocument/signatureHelp"] = "signatureHelpProvider", + ["textDocument/documentHighlight"] = "documentHighlightProvider", + } + local cap = capability_map[method] + return cap and client.server_capabilities[cap] + end + return false + end + + if lsp_supports_method(client, "textDocument/formatting") then + vim.api.nvim_create_autocmd("BufWritePre", { + group = augroup_format, + buffer = bufnr, + callback = function() + if vim.fn.has("nvim-0.8") == 1 then + vim.lsp.buf.format({ + async = false, + bufnr = bufnr, + filter = function(formatting_client) + return formatting_client.name == "null-ls" + end, + }) + else + vim.lsp.buf.formatting_sync() + end + end, + }) + end + end, + }) + end + + -- Set up LSP capabilities + local capabilities = setup_capabilities() + local on_attach = create_on_attach() + + -- Set up LSP handlers with version compatibility (avoid deprecated APIs) + if has_feature("deprecated_lsp_handlers") then + -- Use old handler setup for versions before 0.12 + if vim.lsp.handlers then + vim.lsp.handlers['textDocument/hover'] = vim.lsp.with( + vim.lsp.handlers.hover, { border = 'rounded' } + ) + + vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with( + vim.lsp.handlers.signature_help, { border = 'rounded' } + ) + end + else + -- Use new handler setup for 0.12+ (when old handlers are deprecated/removed) + if vim.lsp.handlers then + vim.lsp.handlers['textDocument/hover'] = vim.lsp.with( + vim.lsp.handlers['textDocument/hover'], { border = 'rounded' } + ) + + vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with( + vim.lsp.handlers['textDocument/signatureHelp'], { border = 'rounded' } + ) + end + end + + -- Choose configuration method based on Neovim version and available features + if has_feature("native_lsp_config") then + -- Set up native LSP configuration + local servers = setup_native_lsp_configs() + + -- Set default on_attach and capabilities for all LSP servers + vim.lsp.config('*', { + on_attach = on_attach, + capabilities = capabilities, + }) + + -- Enable the LSP servers + vim.lsp.enable(servers) + + elseif M.mason_lspconfig and M.lspconfig then + -- Set up mason-lspconfig if available + if M.mason_lspconfig then + M.mason_lspconfig.setup({ + ensure_installed = { + "lua_ls", "pyright", "ts_ls", "rust_analyzer", "clangd", "gopls", + "bashls", "html", "cssls", "jsonls", "yamlls" + }, + automatic_installation = true, + }) + end + + -- Use traditional lspconfig with mason + local enabled_servers = {} + + local server_configs = { + lua_ls = { + settings = { + Lua = { + diagnostics = { + globals = { "vim", "use", "_G", "packer_plugins", "P" }, + }, + workspace = { + library = { + vim.env.VIMRUNTIME, + "${3rd}/luv/library", + "${3rd}/busted/library", + }, + checkThirdParty = false, + }, + telemetry = { enable = false }, + }, + }, + }, + pyright = { + settings = { + python = { + formatting = { provider = "none" } + } + } + }, + ts_ls = { + init_options = { + disableAutomaticTypeAcquisition = true + }, + }, + clangd = { + cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu" }, + }, + gopls = { + settings = { + gopls = { + gofumpt = true, + usePlaceholders = true, + completeUnimported = true, + staticcheck = true, + }, + }, + }, + } + + M.mason_lspconfig.setup_handlers({ + function(server_name) + if not enabled_servers[server_name] then + local config = server_configs[server_name] or {} + config.on_attach = on_attach + config.capabilities = capabilities + M.lspconfig[server_name].setup(config) + enabled_servers[server_name] = true + end + end, + }) + + elseif M.lspconfig then + -- Fallback: Set up servers manually if mason-lspconfig is not available + local servers = { 'lua_ls', 'pyright', 'ts_ls', 'rust_analyzer', 'clangd', 'gopls', 'bashls', 'html', 'cssls', 'jsonls', 'yamlls' } + local enabled_servers = {} + + for _, server in ipairs(servers) do + if not enabled_servers[server] and M.lspconfig[server] then + local config = { + on_attach = on_attach, + capabilities = capabilities, + } + M.lspconfig[server].setup(config) + enabled_servers[server] = true + end + end + end + + return true +end + +-- Global toggle for diagnostics (backwards compatible) +vim.g.diagnostics_visible = true +function _G.toggle_diagnostics() + if has_feature("diagnostic_api") then + if vim.g.diagnostics_visible then + vim.g.diagnostics_visible = false + vim.diagnostic.disable() + else + vim.g.diagnostics_visible = true + vim.diagnostic.enable() + end + else + -- Fallback for older versions + if vim.g.diagnostics_visible then + vim.g.diagnostics_visible = false + vim.lsp.handlers["textDocument/publishDiagnostics"] = function() end + else + vim.g.diagnostics_visible = true + vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, {} + ) + end + end +end + +-- Create Mason command if Mason is available +if M.mason then + vim.api.nvim_create_user_command("Mason", function() + require("mason.ui").open() + end, {}) +end + +-- Automatically show diagnostics in a float window for the current line +if has_feature("diagnostic_api") then + vim.api.nvim_create_autocmd("CursorHold", { + group = augroup_diag_float, + pattern = "*", + callback = function() + local opts = { + focusable = false, + close_events = { "BufLeave", "CursorMoved", "InsertEnter", "FocusLost" }, + border = border, + source = "always", + prefix = " ", + scope = "cursor", + } + vim.diagnostic.open_float(nil, opts) + end, + }) + + -- Autocmd to open the diagnostic window when a file with errors is opened + vim.api.nvim_create_autocmd({ "LspAttach", "BufReadPost" }, { + group = augroup_diag_load, + callback = function() + local has_errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR }) > 0 + if has_errors then + vim.diagnostic.setqflist({ + open = true, + title = "Diagnostics", + }) + end + end, + }) +end + +-- Create Toggle Diagnostic command +vim.api.nvim_create_user_command("ToggleDiagnostics", _G.toggle_diagnostics, { + desc = "Toggle global diagnostics visibility" +}) + +return M |
