aboutsummaryrefslogtreecommitdiff
path: root/common/nvim/lua/plugins/lsp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'common/nvim/lua/plugins/lsp.lua')
-rwxr-xr-xcommon/nvim/lua/plugins/lsp.lua674
1 files changed, 674 insertions, 0 deletions
diff --git a/common/nvim/lua/plugins/lsp.lua b/common/nvim/lua/plugins/lsp.lua
new file mode 100755
index 0000000..5ed1152
--- /dev/null
+++ b/common/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