iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍫

Automating LSP Installation Without mason-lspconfig.nvim

に公開

TL;DR

The automatic installation feature for LSP can be implemented relatively easily using only mason.nvim, without needing mason-lspconfig.nvim.

Implementation Example
local function mason_install_lsp(servers)
  local registry = require("mason-registry")
  local pkg_specs_all = registry.get_all_package_specs()

  -- Create a mapping table from nvim-lspconfig to package names
  local lspconfig_to_pkg = {}
  for _, pkg_spec in ipairs(pkg_specs_all) do
    if vim.tbl_get(pkg_spec, "neovim", "lspconfig") ~= nil then
      local key = pkg_spec.neovim.lspconfig
      local value = pkg_spec.name or key
      lspconfig_to_pkg[key] = value
    end
  end

  -- Convert names from nvim-lspconfig to package names
  local packages = {}
  for _, lspcfg_name in ipairs(servers) do
    local lsp = lspconfig_to_pkg[lspcfg_name] or lspcfg_name
    table.insert(packages, lsp)
  end

  -- Automatic installation
  registry.refresh(function()
    for _, pkg_name in ipairs(packages) do
      if not registry.is_installed(pkg_name) then
        local pkg = registry.get_package(pkg_name)
        pkg:install()
      end
    end
  end)
end

-- Specify the LSPs you want to install
local servers = {
  "lua_ls",
  "basedpyright",
  "rust_analyzer",
}

-- Enabling LSP
-- vim.lsp.enable(servers)

-- When using a user-defined command
vim.api.nvim_create_user_command("MasonInstallLsp", function()
  mason_install_lsp(servers)
end, {})

-- When triggered upon opening the mason.nvim UI
vim.api.nvim_create_autocmd("FileType", {
  group = vim.api.nvim_create_augroup("mason-install-lsp", { clear = true }),
  once = true,
  pattern = "mason",
  callback = function()
    mason_install_lsp(servers)
  end,
})

History of LSP Configuration and the Role of mason-lspconfig.nvim

In this section, we will organize the history of LSP configuration in Neovim and reaffirm the role of mason-lspconfig.nvim.
Since this is a section for organizing the background, feel free to skip it if you just want to see the implementation.

History of LSP Configuration

The Neovim LSP environment has changed incrementally over the past few years.
Before diving into the main topic, let's organize the shift in the division of labor between the built-in API vim.lsp and nvim-lspconfig.

Typical Configuration Before v0.10

Neovim has had a built-in LSP client (vim.lsp) implemented since v0.5.
However, vim.lsp at that time focused primarily on low-level APIs for LSP communication, and no official configuration framework was provided for practical operation of Language Servers.

Bridging this gap, nvim-lspconfig became widely adopted.
nvim-lspconfig was not just a collection of configuration examples; it provided common schemas, project root detection logic, configuration presets for various language servers, and acted as the de facto standard for LSP configuration in Neovim[1].

Users could use LSP simply by calling setup() like require("lspconfig").clangd.setup({}), and as a result, the structure of "using LSP in Neovim = using nvim-lspconfig" became the de facto standard for a long time.

How LSP Configuration Changed in v0.11

In v0.11, a new official foundation for LSP configuration was established.

  • vim.lsp.config()
  • vim.lsp.enable()
  • Automatic configuration loading via the lsp/ directory
  • Standardization of project root detection via root_markers

As a result of some of the LSP configuration roles previously handled by nvim-lspconfig being incorporated into Neovim core, the role of nvim-lspconfig has now shifted to being a collection of LSP configuration presets.
This can be called a turning point where Neovim officially internalized the LSP configuration foundation that it had long depended on external plugins for.

Clarifying the Role of mason-lspconfig.nvim

With the internalization of the LSP configuration foundation in v0.11, the dependencies surrounding LSP are also being reorganized. The typical configuration before v0.10 consisted of the following three plugins:

In fact, many tutorial articles introduce this configuration as a prerequisite.

With such a configuration becoming widespread, the perception that "you need mason-lspconfig to use LSP in Neovim" became common.

However, in reality, mason-lspconfig.nvim is not a mandatory component for running LSP. Let's clarify the role mason-lspconfig.nvim plays. The README states the following:

mason-lspconfig bridges mason.nvim with the lspconfig plugin - making it easier to use both plugins together.

As this description indicates, the primary roles of mason-lspconfig.nvim can be summarized into the following two points:

  • Mapping between mason.nvim package names and the names defined in nvim-lspconfig[2]
  • Automatic installation of packages

In other words, mason-lspconfig.nvim is not an essential element for running LSP, but rather an auxiliary layer for simplifying configuration. And the important point of this article is that these functions can be reproduced with relatively simple code by using the mason.nvim API.

In the next chapter, I will introduce the specific implementation method.

Implementing Automatic Installation Using Only mason.nvim

Mapping Package Names

Why doesn't mason.nvim itself align the names?

mason.nvim is not a tool exclusively for LSP, but a package manager that manages external tools (LSP, DAP, Linter, Formatter) across the board. Therefore, package names are defined based on the official names of the tools themselves, rather than the naming conventions of nvim-lspconfig. In fact, in a mason.nvim issue, maintainers have stated their position that "it is independent of nvim-lspconfig's naming conventions"[3].

The difference in package names can be resolved by creating a translation table defining the names for both mason.nvim and nvim-lspconfig. In fact, NvChad, which does not adopt mason-lspconfig.nvim (much like SnaxVim), substitutes the name conversion formerly handled by mason-lspconfig with its own conversion table[4].

However, this approach has an inherent challenge. Language Servers are continuously being added or renamed, and the number of packages in mason.nvim continues to grow. This means that this mapping table is not something you create once and forget; it requires continuous synchronization and updates. Given the operational cost of manually tracking these name mappings, managing them yourself is not practical[5].

To solve this problem, you can utilize the official API available from mason.nvim, mason-registry. The repository of the same name (mason-registry) aggregates package information referenced by mason.nvim, and you can retrieve this information via the API.

First, retrieve all package information registered in the mason-registry.

local registry = require("mason-registry")
local pkg_specs_all = registry.get_all_package_specs()

If the nvim-lspconfig name is defined for a package, it can be referenced via pkg_specs_all[i]["neovim"]["lspconfig"].

local lspconfig_to_pkg = {}
for _, pkg_spec in ipairs(pkg_specs_all) do
  -- Check if the name defined in nvim-lspconfig is recorded in mason-registry
  if vim.tbl_get(pkg_spec, "neovim", "lspconfig") ~= nil then
    -- Create a table with the nvim-lspconfig name as the key and the mason.nvim name as the value
    local key = pkg_spec.neovim.lspconfig
    local value = pkg_spec.name or key
    lspconfig_to_pkg[key] = value
  end
end

Now the name conversion table is ready. All that remains is to resolve the LSP servers you want to install using this table.

-- LSP to install
local servers = {
  "lua_ls",
  "basedpyright",
  "rust_analyzer",
}

local packages = {}
for _, lspcfg_name in ipairs(servers) do
  local lsp = lspconfig_to_pkg[lspcfg_name] or lspcfg_name
  table.insert(packages, lsp)
end
If you want to dynamically reference enabled LSP servers

In the current version of Neovim, there is no official API to directly retrieve a list of enabled LSP configurations.

What is officially provided is limited to something like vim.lsp.is_enabled, which checks whether a specific configuration is enabled. Looking at the implementation of this function, you can see it references vim.lsp._enabled_configs. While this table is not a public API, it is currently the only practical means to retrieve a list of enabled LSP servers.

There is a possibility of breaking changes due to future specification updates, but for practical use, it can be retrieved as follows:

local servers = vim.tbl_keys(vim.lsp._enabled_configs)

Automatic Installation

As of mason.nvim v2.2.1, an automatic installation feature has not been implemented in the main repository.

Why is there no automatic installation feature in the main mason.nvim repository?

This feature has been proposed in the past, but a cautious stance has been taken from the perspective of separation of concerns in design. While maintainers have shown interest in a future introduction, they have noted the following design challenges[6]:

  • At what timing should detection and updates for new versions be performed?
  • How to safely handle updates that include breaking changes?
  • Should automatic updates be executed during short Neovim sessions?
  • Design for progress display other than :Mason

These are issues that relate to the entire package lifecycle management, not just a simple installation feature. Therefore, mason.nvim adopts a policy of letting the ecosystem develop this area as extensions, like mason-lspconfig.nvim, rather than embedding it into the core. This policy is still maintained today, and automatic installation is positioned as a function to be implemented on the extension side.

In practice, you can easily implement it on the user side by utilizing the API provided by mason.nvim. Furthermore, this method is the official implementation pattern recommended by the maintainers of mason.nvim[7].

A minimal implementation example is as follows:

-- Fetch and synchronize mason-registry contents
registry.refresh(function()
  -- Traverse the table of package names
  for _, pkg_name in ipairs(packages) do
    -- Install if not installed
    if not registry.is_installed(pkg_name) then
      local pkg = registry.get_package(pkg_name)
      pkg:install()
    end
  end
end)

This code is a minimal example of achieving the same processing as ensure_installed in mason-lspconfig.nvim using only the mason.nvim API. The execution timing can be designed freely according to individual operational policies, such as at startup, when executing a user-defined command, or at specific events.

Conclusion

In this article, I covered the following:

  • Transition of Neovim LSP configuration (v0.5-0.10 → v0.11)
  • Clarification of the roles of mason.nvim / nvim-lspconfig / mason-lspconfig.nvim
  • Mapping of names that differ between mason.nvim and nvim-lspconfig
  • Design and implementation of automatic LSP installation using the mason.nvim API

In SnaxVim, besides the LSP servers covered in this article, automatic installation for DAP, Linter, and Formatter is also implemented, so that each tool is installed upon the first launch of mason.nvim. If you are interested, please take a look at the repository to see the big picture.

🔗 Repository: https://github.com/SnaxVim/SnaxVim
If you found this article helpful, it would be a great encouragement if you could give a ⭐ to the repository above.

脚注
  1. If we go back a bit further, there were tools like coc.nvim, but I will omit them for brevity. ↩︎

  2. e.g., lua-language-server and lua_ls ↩︎

  3. Add package aliases to align with vim-lspconfig · Issue #602 · mason-org/mason.nvim ↩︎

  4. ui/lua/nvchad/mason/names.lua at v3.0 · NvChad/ui ↩︎

  5. Conversely, one could say that NvChad's approach is sustained by the energetic activity of Sidhanth Rathod and contributions from the community. ↩︎

  6. Configuration to auto install missing tools · Issue #103 · mason-org/mason.nvim ↩︎

  7. Changes to the mason.nvim registry · mason-org/mason.nvim · Discussion #1113 ↩︎

Discussion