iTranslated by AI
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-lspconfigbridgesmason.nvimwith thelspconfigplugin - 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.
-
If we go back a bit further, there were tools like coc.nvim, but I will omit them for brevity. ↩︎
-
e.g.,
lua-language-serverandlua_ls↩︎ -
Add package aliases to align with vim-lspconfig · Issue #602 · mason-org/mason.nvim ↩︎
-
Conversely, one could say that NvChad's approach is sustained by the energetic activity of Sidhanth Rathod and contributions from the community. ↩︎
-
Configuration to auto install missing tools · Issue #103 · mason-org/mason.nvim ↩︎
-
Changes to the mason.nvim registry · mason-org/mason.nvim · Discussion #1113 ↩︎
Discussion