2023-12-31 00:10:56 +02:00

432 lines
17 KiB
Lua

--- ### AstroNvim LSP Utils
--
-- LSP related utility functions to use within AstroNvim and user configurations.
--
-- This module can be loaded with `local lsp_utils = require("astronvim.utils.lsp")`
--
-- @module astronvim.utils.lsp
-- @see astronvim.utils
-- @copyright 2022
-- @license GNU General Public License v3.0
local M = {}
local tbl_contains = vim.tbl_contains
local tbl_isempty = vim.tbl_isempty
local user_opts = astronvim.user_opts
local utils = require "astronvim.utils"
local conditional_func = utils.conditional_func
local is_available = utils.is_available
local extend_tbl = utils.extend_tbl
local server_config = "lsp.config."
local setup_handlers = user_opts("lsp.setup_handlers", {
function(server, opts) require("lspconfig")[server].setup(opts) end,
})
M.diagnostics = { [0] = {}, {}, {}, {} }
M.setup_diagnostics = function(signs)
local default_diagnostics = astronvim.user_opts("diagnostics", {
virtual_text = true,
signs = { active = signs },
update_in_insert = true,
underline = true,
severity_sort = true,
float = {
focused = false,
style = "minimal",
border = "rounded",
source = "always",
header = "",
prefix = "",
},
})
M.diagnostics = {
-- diagnostics off
[0] = extend_tbl(
default_diagnostics,
{ underline = false, virtual_text = false, signs = false, update_in_insert = false }
),
-- status only
extend_tbl(default_diagnostics, { virtual_text = false, signs = false }),
-- virtual text off, signs on
extend_tbl(default_diagnostics, { virtual_text = false }),
-- all diagnostics on
default_diagnostics,
}
vim.diagnostic.config(M.diagnostics[vim.g.diagnostics_mode])
end
M.formatting = user_opts("lsp.formatting", { format_on_save = { enabled = true }, disabled = {} })
if type(M.formatting.format_on_save) == "boolean" then
M.formatting.format_on_save = { enabled = M.formatting.format_on_save }
end
M.format_opts = vim.deepcopy(M.formatting)
M.format_opts.disabled = nil
M.format_opts.format_on_save = nil
M.format_opts.filter = function(client)
local filter = M.formatting.filter
local disabled = M.formatting.disabled or {}
-- check if client is fully disabled or filtered by function
return not (vim.tbl_contains(disabled, client.name) or (type(filter) == "function" and not filter(client)))
end
--- Helper function to set up a given server with the Neovim LSP client
---@param server string The name of the server to be setup
M.setup = function(server)
-- if server doesn't exist, set it up from user server definition
local config_avail, config = pcall(require, "lspconfig.server_configurations." .. server)
if not config_avail or not config.default_config then
local server_definition = user_opts(server_config .. server)
if server_definition.cmd then require("lspconfig.configs")[server] = { default_config = server_definition } end
end
local opts = M.config(server)
local setup_handler = setup_handlers[server] or setup_handlers[1]
if not vim.tbl_contains(astronvim.lsp.skip_setup, server) and setup_handler then setup_handler(server, opts) end
end
--- Helper function to check if any active LSP clients given a filter provide a specific capability
---@param capability string The server capability to check for (example: "documentFormattingProvider")
---@param filter vim.lsp.get_active_clients.filter|nil (table|nil) A table with
--- key-value pairs used to filter the returned clients.
--- The available keys are:
--- - id (number): Only return clients with the given id
--- - bufnr (number): Only return clients attached to this buffer
--- - name (string): Only return clients with the given name
---@return boolean # Whether or not any of the clients provide the capability
function M.has_capability(capability, filter)
for _, client in ipairs(vim.lsp.get_active_clients(filter)) do
if client.supports_method(capability) then return true end
end
return false
end
local function add_buffer_autocmd(augroup, bufnr, autocmds)
if not vim.tbl_islist(autocmds) then autocmds = { autocmds } end
local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
if not cmds_found or vim.tbl_isempty(cmds) then
vim.api.nvim_create_augroup(augroup, { clear = false })
for _, autocmd in ipairs(autocmds) do
local events = autocmd.events
autocmd.events = nil
autocmd.group = augroup
autocmd.buffer = bufnr
vim.api.nvim_create_autocmd(events, autocmd)
end
end
end
local function del_buffer_autocmd(augroup, bufnr)
local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
if cmds_found then vim.tbl_map(function(cmd) vim.api.nvim_del_autocmd(cmd.id) end, cmds) end
end
--- The `on_attach` function used by AstroNvim
---@param client table The LSP client details when attaching
---@param bufnr number The buffer that the LSP client is attaching to
M.on_attach = function(client, bufnr)
local lsp_mappings = require("astronvim.utils").empty_map_table()
lsp_mappings.n["<leader>ld"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
lsp_mappings.n["[d"] = { function() vim.diagnostic.goto_prev() end, desc = "Previous diagnostic" }
lsp_mappings.n["]d"] = { function() vim.diagnostic.goto_next() end, desc = "Next diagnostic" }
lsp_mappings.n["gl"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
if is_available "telescope.nvim" then
lsp_mappings.n["<leader>lD"] =
{ function() require("telescope.builtin").diagnostics() end, desc = "Search diagnostics" }
end
if is_available "mason-lspconfig.nvim" then
lsp_mappings.n["<leader>li"] = { "<cmd>LspInfo<cr>", desc = "LSP information" }
end
if is_available "null-ls.nvim" then
lsp_mappings.n["<leader>lI"] = { "<cmd>NullLsInfo<cr>", desc = "Null-ls information" }
end
if client.supports_method "textDocument/codeAction" then
lsp_mappings.n["<leader>la"] = {
function() vim.lsp.buf.code_action() end,
desc = "LSP code action",
}
lsp_mappings.v["<leader>la"] = lsp_mappings.n["<leader>la"]
end
if client.supports_method "textDocument/codeLens" then
add_buffer_autocmd("lsp_codelens_refresh", bufnr, {
events = { "InsertLeave", "BufEnter" },
desc = "Refresh codelens",
callback = function()
if not M.has_capability("textDocument/codeLens", { bufnr = bufnr }) then
del_buffer_autocmd("lsp_codelens_refresh", bufnr)
return
end
if vim.g.codelens_enabled then vim.lsp.codelens.refresh() end
end,
})
if vim.g.codelens_enabled then vim.lsp.codelens.refresh() end
lsp_mappings.n["<leader>ll"] = {
function() vim.lsp.codelens.refresh() end,
desc = "LSP CodeLens refresh",
}
lsp_mappings.n["<leader>lL"] = {
function() vim.lsp.codelens.run() end,
desc = "LSP CodeLens run",
}
end
if client.supports_method "textDocument/declaration" then
lsp_mappings.n["gD"] = {
function() vim.lsp.buf.declaration() end,
desc = "Declaration of current symbol",
}
end
if client.supports_method "textDocument/definition" then
lsp_mappings.n["gd"] = {
function() vim.lsp.buf.definition() end,
desc = "Show the definition of current symbol",
}
end
if client.supports_method "textDocument/formatting" and not tbl_contains(M.formatting.disabled, client.name) then
lsp_mappings.n["<leader>lf"] = {
function() vim.lsp.buf.format(M.format_opts) end,
desc = "Format buffer",
}
lsp_mappings.v["<leader>lf"] = lsp_mappings.n["<leader>lf"]
vim.api.nvim_buf_create_user_command(
bufnr,
"Format",
function() vim.lsp.buf.format(M.format_opts) end,
{ desc = "Format file with LSP" }
)
local autoformat = M.formatting.format_on_save
local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr })
if
autoformat.enabled
and (tbl_isempty(autoformat.allow_filetypes or {}) or tbl_contains(autoformat.allow_filetypes, filetype))
and (tbl_isempty(autoformat.ignore_filetypes or {}) or not tbl_contains(autoformat.ignore_filetypes, filetype))
then
add_buffer_autocmd("lsp_auto_format", bufnr, {
events = "BufWritePre",
desc = "autoformat on save",
callback = function()
if not M.has_capability("textDocument/formatting", { bufnr = bufnr }) then
del_buffer_autocmd("lsp_auto_format", bufnr)
return
end
local autoformat_enabled = vim.b.autoformat_enabled
if autoformat_enabled == nil then autoformat_enabled = vim.g.autoformat_enabled end
if autoformat_enabled and ((not autoformat.filter) or autoformat.filter(bufnr)) then
vim.lsp.buf.format(extend_tbl(M.format_opts, { bufnr = bufnr }))
end
end,
})
lsp_mappings.n["<leader>uf"] = {
function() require("astronvim.utils.ui").toggle_buffer_autoformat() end,
desc = "Toggle autoformatting (buffer)",
}
lsp_mappings.n["<leader>uF"] = {
function() require("astronvim.utils.ui").toggle_autoformat() end,
desc = "Toggle autoformatting (global)",
}
end
end
if client.supports_method "textDocument/documentHighlight" then
add_buffer_autocmd("lsp_document_highlight", bufnr, {
{
events = { "CursorHold", "CursorHoldI" },
desc = "highlight references when cursor holds",
callback = function()
if not M.has_capability("textDocument/documentHighlight", { bufnr = bufnr }) then
del_buffer_autocmd("lsp_document_highlight", bufnr)
return
end
vim.lsp.buf.document_highlight()
end,
},
{
events = { "CursorMoved", "CursorMovedI" },
desc = "clear references when cursor moves",
callback = function() vim.lsp.buf.clear_references() end,
},
})
end
if client.supports_method "textDocument/hover" then
-- TODO: Remove mapping after dropping support for Neovim v0.9, it's automatic
if vim.fn.has "nvim-0.10" == 0 then
lsp_mappings.n["K"] = {
function() vim.lsp.buf.hover() end,
desc = "Hover symbol details",
}
end
end
if client.supports_method "textDocument/implementation" then
lsp_mappings.n["gI"] = {
function() vim.lsp.buf.implementation() end,
desc = "Implementation of current symbol",
}
end
if client.supports_method "textDocument/inlayHint" then
if vim.b.inlay_hints_enabled == nil then vim.b.inlay_hints_enabled = vim.g.inlay_hints_enabled end
-- TODO: remove check after dropping support for Neovim v0.9
if vim.lsp.inlay_hint then
if vim.b.inlay_hints_enabled then vim.lsp.inlay_hint(bufnr, true) end
lsp_mappings.n["<leader>uH"] = {
function() require("astronvim.utils.ui").toggle_buffer_inlay_hints(bufnr) end,
desc = "Toggle LSP inlay hints (buffer)",
}
end
end
if client.supports_method "textDocument/references" then
lsp_mappings.n["gr"] = {
function() vim.lsp.buf.references() end,
desc = "References of current symbol",
}
lsp_mappings.n["<leader>lR"] = {
function() vim.lsp.buf.references() end,
desc = "Search references",
}
end
if client.supports_method "textDocument/rename" then
lsp_mappings.n["<leader>lr"] = {
function() vim.lsp.buf.rename() end,
desc = "Rename current symbol",
}
end
if client.supports_method "textDocument/signatureHelp" then
lsp_mappings.n["<leader>lh"] = {
function() vim.lsp.buf.signature_help() end,
desc = "Signature help",
}
end
if client.supports_method "textDocument/typeDefinition" then
lsp_mappings.n["gy"] = {
function() vim.lsp.buf.type_definition() end,
desc = "Definition of current type",
}
end
if client.supports_method "workspace/symbol" then
lsp_mappings.n["<leader>lG"] = { function() vim.lsp.buf.workspace_symbol() end, desc = "Search workspace symbols" }
end
if client.supports_method "textDocument/semanticTokens/full" and vim.lsp.semantic_tokens then
if vim.b.semantic_tokens_enabled == nil then vim.b.semantic_tokens_enabled = vim.g.semantic_tokens_enabled end
if not vim.g.semantic_tokens_enabled then vim.lsp.semantic_tokens["stop"](bufnr, client.id) end
lsp_mappings.n["<leader>uY"] = {
function() require("astronvim.utils.ui").toggle_buffer_semantic_tokens(bufnr) end,
desc = "Toggle LSP semantic highlight (buffer)",
}
end
if is_available "telescope.nvim" then -- setup telescope mappings if available
if lsp_mappings.n.gd then lsp_mappings.n.gd[1] = function() require("telescope.builtin").lsp_definitions() end end
if lsp_mappings.n.gI then
lsp_mappings.n.gI[1] = function() require("telescope.builtin").lsp_implementations() end
end
if lsp_mappings.n.gr then lsp_mappings.n.gr[1] = function() require("telescope.builtin").lsp_references() end end
if lsp_mappings.n["<leader>lR"] then
lsp_mappings.n["<leader>lR"][1] = function() require("telescope.builtin").lsp_references() end
end
if lsp_mappings.n.gy then
lsp_mappings.n.gy[1] = function() require("telescope.builtin").lsp_type_definitions() end
end
if lsp_mappings.n["<leader>lG"] then
lsp_mappings.n["<leader>lG"][1] = function()
vim.ui.input({ prompt = "Symbol Query: " }, function(query)
if query then require("telescope.builtin").lsp_workspace_symbols { query = query } end
end)
end
end
end
if not vim.tbl_isempty(lsp_mappings.v) then
lsp_mappings.v["<leader>l"] = { desc = utils.get_icon("ActiveLSP", 1, true) .. "LSP" }
end
utils.set_mappings(user_opts("lsp.mappings", lsp_mappings), { buffer = bufnr })
for id, _ in pairs(astronvim.lsp.progress) do -- clear lingering progress messages
if not next(vim.lsp.get_active_clients { id = tonumber(id:match "^%d+") }) then astronvim.lsp.progress[id] = nil end
end
local on_attach_override = user_opts("lsp.on_attach", nil, false)
conditional_func(on_attach_override, true, client, bufnr)
end
--- The default AstroNvim LSP capabilities
M.capabilities = vim.lsp.protocol.make_client_capabilities()
M.capabilities.textDocument.completion.completionItem.documentationFormat = { "markdown", "plaintext" }
M.capabilities.textDocument.completion.completionItem.snippetSupport = true
M.capabilities.textDocument.completion.completionItem.preselectSupport = true
M.capabilities.textDocument.completion.completionItem.insertReplaceSupport = true
M.capabilities.textDocument.completion.completionItem.labelDetailsSupport = true
M.capabilities.textDocument.completion.completionItem.deprecatedSupport = true
M.capabilities.textDocument.completion.completionItem.commitCharactersSupport = true
M.capabilities.textDocument.completion.completionItem.tagSupport = { valueSet = { 1 } }
M.capabilities.textDocument.completion.completionItem.resolveSupport =
{ properties = { "documentation", "detail", "additionalTextEdits" } }
M.capabilities.textDocument.foldingRange = { dynamicRegistration = false, lineFoldingOnly = true }
M.capabilities = user_opts("lsp.capabilities", M.capabilities)
M.flags = user_opts "lsp.flags"
--- Get the server configuration for a given language server to be provided to the server's `setup()` call
---@param server_name string The name of the server
---@return table # The table of LSP options used when setting up the given language server
function M.config(server_name)
local server = require("lspconfig")[server_name]
local lsp_opts = extend_tbl(
extend_tbl(server.document_config.default_config, server),
{ capabilities = M.capabilities, flags = M.flags }
)
if server_name == "jsonls" then -- by default add json schemas
local schemastore_avail, schemastore = pcall(require, "schemastore")
if schemastore_avail then
lsp_opts.settings = { json = { schemas = schemastore.json.schemas(), validate = { enable = true } } }
end
end
if server_name == "yamlls" then -- by default add yaml schemas
local schemastore_avail, schemastore = pcall(require, "schemastore")
if schemastore_avail then lsp_opts.settings = { yaml = { schemas = schemastore.yaml.schemas() } } end
end
if server_name == "lua_ls" then -- by default initialize neodev and disable third party checking
pcall(require, "neodev")
lsp_opts.before_init = function(param, config)
if vim.b.neodev_enabled then
for _, astronvim_config in ipairs(astronvim.supported_configs) do
if param.rootPath:match(astronvim_config) then
table.insert(config.settings.Lua.workspace.library, astronvim.install.home .. "/lua")
break
end
end
end
end
lsp_opts.settings = { Lua = { workspace = { checkThirdParty = false } } }
end
local opts = user_opts(server_config .. server_name, lsp_opts)
local old_on_attach = server.on_attach
local user_on_attach = opts.on_attach
opts.on_attach = function(client, bufnr)
conditional_func(old_on_attach, true, client, bufnr)
M.on_attach(client, bufnr)
conditional_func(user_on_attach, true, client, bufnr)
end
return opts
end
return M