lsp.lua

  1--- ### LSP utils.
  2--
  3--  DESCRIPTION:
  4--  Functions we use to configure the plugin `mason-lspconfig.nvim`.
  5--  You can specify your own lsp settings inside `M.apply_user_lsp_settings()`.
  6--
  7--  Most options we use in `M.apply_default_lsp_settings()`
  8--  can be tweaked on the file `../1-options.lua`.
  9--  Take this into consideration to minimize the risk of breaking stuff.
 10
 11--    Functions:
 12--      -> M.apply_default_lsp_settings  → Apply our default lsp settings.
 13--      -> M.apply_user_lsp_mappings     → Apply the user lsp keymappings.
 14--      -> M.apply_user_lsp_settings     → Apply the user lsp settings.
 15--      -> M.setup                       → It passes the user lsp settings to lspconfig.
 16
 17local M = {}
 18local utils = require "base.utils"
 19local stored_handlers = {}
 20
 21--- Apply default settings for diagnostics, formatting, and lsp capabilities.
 22--- It only need to be executed once, normally on mason-lspconfig.
 23---@return nil
 24M.apply_default_lsp_settings = function()
 25  -- Icons
 26  -- Apply the icons defined in ../icons/nerd_font.lua
 27  local get_icon = utils.get_icon
 28  local signs = {
 29    { name = "DiagnosticSignError",    text = get_icon("DiagnosticError"),        texthl = "DiagnosticSignError" },
 30    { name = "DiagnosticSignWarn",     text = get_icon("DiagnosticWarn"),         texthl = "DiagnosticSignWarn" },
 31    { name = "DiagnosticSignHint",     text = get_icon("DiagnosticHint"),         texthl = "DiagnosticSignHint" },
 32    { name = "DiagnosticSignInfo",     text = get_icon("DiagnosticInfo"),         texthl = "DiagnosticSignInfo" },
 33    { name = "DapStopped",             text = get_icon("DapStopped"),             texthl = "DiagnosticWarn" },
 34    { name = "DapBreakpoint",          text = get_icon("DapBreakpoint"),          texthl = "DiagnosticInfo" },
 35    { name = "DapBreakpointRejected",  text = get_icon("DapBreakpointRejected"),  texthl = "DiagnosticError" },
 36    { name = "DapBreakpointCondition", text = get_icon("DapBreakpointCondition"), texthl = "DiagnosticInfo" },
 37    { name = "DapLogPoint",            text = get_icon("DapLogPoint"),            texthl = "DiagnosticInfo" }
 38  }
 39  for _, sign in ipairs(signs) do
 40    vim.fn.sign_define(sign.name, sign)
 41  end
 42
 43  -- Borders
 44  -- Apply the option lsp_round_borders_enabled from ../1-options.lua
 45  if vim.g.lsp_round_borders_enabled then
 46    vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, { border = "rounded", silent = true })
 47    vim.lsp.handlers["textDocument/signatureHelp"] =
 48        vim.lsp.with(vim.lsp.handlers.signature_help, { border = "rounded", silent = true })
 49  end
 50
 51  -- Set default diagnostics
 52  local default_diagnostics = {
 53    virtual_text = true,
 54    signs = {
 55      text = {
 56        [vim.diagnostic.severity.ERROR] = utils.get_icon("DiagnosticError"),
 57        [vim.diagnostic.severity.HINT] = utils.get_icon("DiagnosticHint"),
 58        [vim.diagnostic.severity.WARN] = utils.get_icon("DiagnosticWarn"),
 59        [vim.diagnostic.severity.INFO] = utils.get_icon("DiagnosticInfo"),
 60      },
 61      active = signs,
 62    },
 63    update_in_insert = true,
 64    underline = true,
 65    severity_sort = true,
 66    float = {
 67      focused = false,
 68      style = "minimal",
 69      border = "rounded",
 70      source = "always",
 71      header = "",
 72      prefix = "",
 73    },
 74  }
 75
 76  -- Apply default diagnostics
 77  -- Applies the option diagnostics_mode from ../1-options.lua
 78  M.diagnostics = {
 79    -- diagnostics off
 80    [0] = vim.tbl_deep_extend(
 81      "force",
 82      default_diagnostics,
 83      { underline = false, virtual_text = false, signs = false, update_in_insert = false }
 84    ),
 85    -- status only
 86    vim.tbl_deep_extend("force", default_diagnostics, { virtual_text = false, signs = false }),
 87    -- virtual text off, signs on
 88    vim.tbl_deep_extend("force", default_diagnostics, { virtual_text = false }),
 89    -- all diagnostics on
 90    default_diagnostics,
 91  }
 92  vim.diagnostic.config(M.diagnostics[vim.g.diagnostics_mode])
 93
 94  -- Apply formatting settings
 95  M.formatting = { format_on_save = { enabled = true }, disabled = {} }
 96  if type(M.formatting.format_on_save) == "boolean" then
 97    M.formatting.format_on_save = { enabled = M.formatting.format_on_save }
 98  end
 99  M.format_opts = vim.deepcopy(M.formatting)
100  M.format_opts.disabled = nil
101  M.format_opts.format_on_save = nil
102  M.format_opts.filter = function(client)
103    local filter = M.formatting.filter
104    local disabled = M.formatting.disabled or {}
105    -- check if client is fully disabled or filtered by function
106    return not (vim.tbl_contains(disabled, client.name) or (type(filter) == "function" and not filter(client)))
107  end
108end
109
110--- This function has the sole purpose of passing the lsp keymappings to lsp.
111--- We have this function, because we use it on none-ls.
112---@param client string The client where the lsp mappings will load.
113---@param bufnr string The bufnr where the lsp mappings will load.
114function M.apply_user_lsp_mappings(client, bufnr)
115  local lsp_mappings = require("base.4-mappings").lsp_mappings(client, bufnr)
116  if not vim.tbl_isempty(lsp_mappings.v) then
117    lsp_mappings.v["<leader>l"] = { desc = utils.get_icon("ActiveLSP", 1, true) .. "LSP" }
118  end
119  utils.set_mappings(lsp_mappings, { buffer = bufnr })
120end
121
122--- Here you can specify custom settings for the lsp servers you install.
123--- This is not normally necessary. But you can.
124---@param server_name string The name of the server
125---@return table # The table of LSP options used when setting up the given language server
126function M.apply_user_lsp_settings(server_name)
127  local server = require("lspconfig")[server_name]
128
129  -- Define user server capabilities.
130  M.capabilities = vim.lsp.protocol.make_client_capabilities()
131  M.capabilities.textDocument.completion.completionItem.documentationFormat = { "markdown", "plaintext" }
132  M.capabilities.textDocument.completion.completionItem.snippetSupport = true
133  M.capabilities.textDocument.completion.completionItem.preselectSupport = true
134  M.capabilities.textDocument.completion.completionItem.insertReplaceSupport = true
135  M.capabilities.textDocument.completion.completionItem.labelDetailsSupport = true
136  M.capabilities.textDocument.completion.completionItem.deprecatedSupport = true
137  M.capabilities.textDocument.completion.completionItem.commitCharactersSupport = true
138  M.capabilities.textDocument.completion.completionItem.tagSupport = { valueSet = { 1 } }
139  M.capabilities.textDocument.completion.completionItem.resolveSupport =
140  { properties = { "documentation", "detail", "additionalTextEdits" } }
141  M.capabilities.textDocument.foldingRange = { dynamicRegistration = false, lineFoldingOnly = true }
142  M.flags = {}
143  local opts = vim.tbl_deep_extend("force", server, { capabilities = M.capabilities, flags = M.flags })
144
145  -- Define user server rules.
146  if server_name == "jsonls" then -- Add schemastore schemas
147    local is_schemastore_loaded, schemastore = pcall(require, "schemastore")
148    if is_schemastore_loaded then
149      opts.settings = { json = { schemas = schemastore.json.schemas(), validate = { enable = true } } }
150    end
151  end
152  if server_name == "yamlls" then -- Add schemastore schemas
153    local is_schemastore_loaded, schemastore = pcall(require, "schemastore")
154    if is_schemastore_loaded then opts.settings = { yaml = { schemas = schemastore.yaml.schemas() } } end
155  end
156  if server_name == "lua_ls" then -- Disable third party checking
157    pcall(require, "neodev")
158    opts.settings = { Lua = { workspace = { checkThirdParty = false } } }
159  end
160
161  -- Apply them
162  local old_on_attach = server.on_attach
163  opts.on_attach = function(client, bufnr)
164    -- If the server on_attach function exist → server.on_attach(client, bufnr)
165    if type(old_on_attach) == "function" then old_on_attach(client, bufnr) end
166    -- Also, apply mappings to the buffer.
167    M.apply_user_lsp_mappings(client, bufnr)
168  end
169  return opts
170end
171
172--- This function passes the `user lsp settings` to lspconfig,
173--- which is the responsible of configuring everything for us.
174---
175--- You are meant to call this function from the plugin `mason-lspconfig.nvim`.
176---@param server string A lsp server name.
177---@return nil
178M.setup = function(server)
179  -- Get the user settings.
180  local opts = M.apply_user_lsp_settings(server)
181
182  -- Get a handler from lspconfig.
183  local setup_handler = stored_handlers[server] or require("lspconfig")[server].setup(opts)
184
185  -- Apply our user settings to the lspconfig handler.
186  if setup_handler then setup_handler(server, opts) end
187end
188
189return M