Option to install LSPs automatically when you open a new file type
I've searched open issues for similar requests
- [x] Yes
Is your feature request related to a problem? Please describe.
I'm just curious about different languages, and try new ones from time to time. I don't want to search through the massive list, I just want the first one associated to this language to be installed.
Describe the solution you'd like
An option automatic_install (and an ignore and manual_link option, see my implementation)
Describe potential alternatives you've considered
I've already effectively implemented it myself. It also ignores LSPs with 8+ languages because those are usually grammar checkers or otherwise generic LSPs.
code
-- Run updates on these packages
---@type table<string, string | false>
local updateable = {
grammarly = false, -- Ignore Grammarly
}
-- Do not bind multiple LSPs to the same filetype
---@type table<string, integer>
local autocmds = {}
-- Mason Lsp Config is not complete
---@type table<string, string>
local manual_link = {
["c3-lsp"] = "c3_lsp",
}
local GENERIC_THRESHOLD = 8
local nvim_lsp, mason_lsp, mapping, registry
local function bind_lsp(lsp_name)
local pkg = updateable[lsp_name]
if pkg ~= nil then
if type(pkg) == "string" then
-- Auto-update
pkg = registry.get_package(pkg)
pkg:check_new_version(function(_ok, version)
if _ok and version.current_version ~= version.latest_version then
pkg:install()
end
end)
end
return
end
local lsp = nvim_lsp[lsp_name]
local fts = lsp.config_def ~= nil and lsp.config_def.default_config.filetypes or {}
-- Generic LSP like harper_ls, not specific to language
if #fts > GENERIC_THRESHOLD or #fts == 0 then
return
end
local mason_name = mapping.lspconfig_to_package[lsp_name] or lsp_name
pkg = registry.get_package(mason_name)
-- Generic LSP like harper_ls, not specific to language
if #pkg.spec.languages > GENERIC_THRESHOLD then
return
end
local buf_fts = {}
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
table.insert(buf_fts, vim.bo[bufnr].filetype)
end
-- Install the first LSP for each filetype
for _, ft in ipairs(fts) do
if autocmds[ft] == nil then
if vim.tbl_contains(buf_fts, ft) then
pkg:install()
else
autocmds[ft] = vim.api.nvim_create_autocmd("FileType", {
pattern = ft,
callback = function()
pkg:install()
end,
once = true,
})
end
end
end
end
return { ---@type LazyPluginSpec[]
{
"williamboman/mason.nvim",
name = "mason",
build = ":MasonUpdate",
config = true,
opts = { ---@type MasonSettings
pip = {
upgrade_pip = true,
},
},
},
{
"williamboman/mason-lspconfig.nvim",
name = "mason-lspconfig",
dependencies = {
{ "williamboman/mason.nvim", name = "mason" },
{ "neovim/nvim-lspconfig" },
},
lazy = false,
config = function(_)
nvim_lsp = require("lspconfig")
mason_lsp = require("mason-lspconfig")
mapping = require("mason-lspconfig.mappings.server")
registry = require("mason-registry")
for mason_name, lsp_name in pairs(manual_link) do
mapping.package_to_lspconfig[mason_name] = lsp_name
mapping.lspconfig_to_package[lsp_name] = mason_name
end
mason_lsp.setup({
automatic_installation = true,
ensure_installed = {},
})
mason_lsp.setup_handlers({
function(lsp_name)
local lsp = nvim_lsp[lsp_name]
updateable[lsp_name] = mapping.lspconfig_to_package[lsp_name] or lsp_name
lsp.setup({})
local fts = lsp.config_def ~= nil and lsp.config_def.default_config.filetypes or {}
if #fts < GENERIC_THRESHOLD then
for _, ft in ipairs(fts) do
autocmds[ft] = 0
end
end
end,
})
-- Launch LSP immediately upon installation
registry:on(
"package:install:success",
vim.schedule_wrap(function(pkg)
local lsp = nvim_lsp[mapping.package_to_lspconfig[pkg.name] or pkg.name]
lsp.setup({})
lsp.launch()
end)
)
registry.update(vim.schedule_wrap(function(ok, msg)
if not ok then
error(msg)
end
for _, lsp_name in ipairs(mason_lsp.get_available_servers()) do
bind_lsp(lsp_name)
end
end))
end,
},
}
https://github.com/user-attachments/assets/4c38d86a-8196-46f6-9049-c6e45a09f638
[!NOTE] This video is outdated. The example above no longer shows any warnings.
mason-lspconfig now consumes 20ms of my startup time, but this is also an eight-year-old laptop and shouldn't be significant on modern machines. (The previous variant I completed last night consumed 50ms)
Additional context
No response
https://github.com/williamboman/mason-lspconfig.nvim/issues/535
Currently, the regular auto-install option is broken with the new lspconfig changes
It is slightly different, as that one only applies to ones you call setup on, but if you were unaware of said option, it is currently broken.
How does your idea determine which is "the first one associated with a filetype" for filetypes with multiple available LSPs?
Which ever shows up first in the registry. If you choose to install a different LSP, no new one will be installed.
New version that works with mason-lspconfig v2
-- Mason Lsp Config is not complete
---@type table<string, string>
local manual_link = {
["c3-lsp"] = "c3_lsp",
}
local GENERIC_THRESHOLD = 8
---@type { [string]: boolean }
local handled = {
grammarly = true, -- Disable grammarly install
}
local _bound = {}
local function launch_lsp(name, bufnr)
local active = vim.lsp.get_clients({ name = name })
if #active > 0 or _bound[name] == true then
return
end
vim.print("Launching " .. name)
_bound[name] = true
local lsp = require("lspconfig")[name]
lsp.setup({})
vim.lsp.start(lsp, {
bufnr = bufnr or 0,
})
end
---@param evt vim.api.keyset.create_autocmd.callback_args
---@param ft string
local function bind_lsp(evt, ft)
local map = require("mason-lspconfig").get_mappings()
local mason = {
lsp = require("mason-lspconfig"),
dap = require("mason-nvim-dap"),
registry = require("mason-registry"),
main = require("mason"),
}
for mason_name, lsp_name in pairs(manual_link) do
map.lspconfig_to_package[lsp_name] = mason_name
map.package_to_lspconfig[mason_name] = lsp_name
end
---@type { [string]: true }
local installed = {}
for _, name in ipairs(mason.lsp.get_installed_servers()) do
installed[name] = true
end
local ok, pkg
for name, _ in pairs(map.lspconfig_to_package) do
local valid = false
for _, t in ipairs((vim.lsp.config[name] or {}).filetypes or {}) do
if t == ft then
valid = true
break
end
end
if not valid then
goto continue
end
local tries = {
map.package_to_lspconfig[name] or name,
name,
}
local config, lsp_name
while config == nil and #tries > 0 do
lsp_name = table.remove(tries, 1)
config = vim.lsp.config[lsp_name]
end
if config == nil then
goto continue
end
local binaries = config.cmd
if type(binaries) == "table" then
if vim.fn.executable(binaries[1]) == 1 then
launch_lsp(lsp_name, evt.buf)
end
end
local fts = config.filetypes or {}
if #fts >= GENERIC_THRESHOLD or map.lspconfig_to_package[name] == nil then
-- pass
elseif installed[name] then
ok, pkg = pcall(mason.registry.get_package, map.lspconfig_to_package[name])
if ok then
break
end
end
::continue::
end
if pkg == nil then
return
end
local current = pkg:get_installed_version()
local latest = pkg:get_latest_version()
local name = map.package_to_lspconfig[pkg.name]
if current ~= latest then
pkg:install({
force = false,
strict = false,
version = pkg:get_latest_version(),
})
else
launch_lsp(name)
end
end
vim.api.nvim_create_autocmd({ "FileType", "BufEnter", "BufRead" }, {
pattern = { "*" },
callback = function(evt)
local ft = vim.bo[evt.buf or 0].filetype
if ft == nil or ft == "" or handled[ft] then
return
end
handled[ft] = true
vim.defer_fn(function()
bind_lsp(evt, ft)
end, 50)
end,
})
return { ---@type LazyPluginSpec[]
{
"williamboman/mason-lspconfig.nvim",
name = "mason-lspconfig",
dependencies = {
{ "williamboman/mason.nvim", name = "mason" },
{ "neovim/nvim-lspconfig" },
{ "jay-babu/mason-nvim-dap.nvim" },
},
lazy = false,
config = function(_)
require("mason").setup({
pip = {
upgrade_pip = true,
},
})
local registry = require("mason-registry")
local mapping = require("mason-lspconfig").get_mappings()
for mason_name, lsp_name in pairs(manual_link) do
mapping.package_to_lspconfig[mason_name] = lsp_name
mapping.lspconfig_to_package[lsp_name] = mason_name
end
-- Launch LSP immediately upon installation
registry:on(
"package:install:success",
vim.schedule_wrap(function(pkg) ---@param pkg Package
local is_lsp = false
for _, category in ipairs(pkg.spec.categories) do
if category == "LSP" then
is_lsp = true
break
end
end
if is_lsp then
launch_lsp(mapping.package_to_lspconfig[pkg.name] or pkg.name)
end
end)
)
local mason_dap = require("mason-nvim-dap")
mason_dap.setup({
automatic_installation = true,
ensure_installed = {},
handlers = {
mason_dap.default_setup,
},
})
end,
},
}