Skip to content

feat: add support for new LSP config API in Neovim 0.11+ #1475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

umutondersu
Copy link

@umutondersu umutondersu commented Apr 16, 2025

Summary

  • Added support for the new LSP configuration API introduced in Neovim 0.11+
  • Maintains backward compatibility for Neovim 0.10 and below

@ro0gr
Copy link

ro0gr commented Apr 17, 2025

Kickstart.nvim targets only the latest 'stable' and latest 'nightly' of Neovim.

Do we really need backward compatibility with Neovim 0 10? I'd consider going with a lighter, happy-path solution. Since the project mainly targets newcomers and servers educational purposes, they'd likely prefer to avoid dealing with legacy approaches.

@umutondersu
Copy link
Author

I think that would be the right approach as well. The reason I intended to maintain backwards compatibility is that some parts of the code is still trying to maintain 0.10 like the client_supports_method. Depending on the others reaction I would also be in favor of removing it.

@umutondersu
Copy link
Author

There is also the problem new syntax not supporting all lsp configs. For example tailwindcss requires the old setup. Could be a good idea to delay this until most lsp configs migrate.

@nickgnd
Copy link

nickgnd commented Apr 17, 2025

Hey everyone 👋
I remember a few related conversations that happen lately on this topic, let me share them here

#1458
#1331

I hope they can be useful, cheers ✌️

@umutondersu
Copy link
Author

Hey everyone 👋 I remember a few related conversations that happen lately on this topic, let me share them here

#1458 #1331

I hope they can be useful, cheers ✌️

  • This setup config does not clash with blink.cmp so in that sense It should be okay.
  • I would be able to support this in an ongoing basis
  • I think using the native way would provide a better experience for newbies but that's up to debate

@kaezrr
Copy link
Contributor

kaezrr commented Apr 20, 2025

In my opinion, we shouldn't bother with backwards compatibility since Kickstart is mostly aimed at beginners who will download the latest version of Neovim as per the instructions in the repo readme

@umutondersu
Copy link
Author

I have removed backwards compatibility and added useful comments for LSPs that still require the old setup

@iton0

This comment was marked as resolved.

@umutondersu
Copy link
Author

Messing around with this and found that this will not respect the lsp_format = "fallback" option in the conform.nvim config. To replicate:

1. comment out the stylua formatter line from conform.nvim config

2. save and reload neovim

3. overindent a line or any other formatting change

4. write file

For me it did not fallback on the lua_ls server for formatting.

I couldn't replicate the issue. I do not think it would be related to these changes as conform.nvim does not use an LSP. In any case If you could check your conform logs by using :ConformInfo It could reveal more info about the issue.

@iton0

This comment has been minimized.

michaellisitsa added a commit to michaellisitsa/dotfiles that referenced this pull request May 5, 2025
init.lua Outdated
Comment on lines 710 to 716
automatic_installation = false,
handlers = {
function(server_name)
local server = servers[server_name] or {}
-- This handles overriding only values explicitly passed
-- by the server configuration above. Useful when disabling
-- certain features of an LSP (for example, turning off formatting for ts_ls)
server.capabilities = vim.tbl_deep_extend('force', {}, capabilities, server.capabilities or {})
require('lspconfig')[server_name].setup(server)
local config = servers[server_name] or {}
vim.lsp.config(server_name, config)
vim.lsp.enable(server_name)
end,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mason-lspconfig.nvim v2.0.0 removes this now. It auto-enables installed LSPs if they're using the new vim.lsp.* API. See https://github.com/mason-org/mason-lspconfig.nvim/releases/tag/v2.0.0

init.lua Outdated
Comment on lines 717 to 722
for server, config in pairs(servers) do
-- This handles overriding only values explicitly passed
-- by the server configuration above. Useful when disabling
-- certain features of an LSP (for example, turning off formatting for ts_ls)
config.capabilities = vim.tbl_deep_extend('force', {}, capabilities, server.capabilities or {})
vim.lsp.config(name, config)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the original hunk of the patch is relevant (and should be used?) now.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I don't know how to mention lines, but shouldn't it be vim.lsp.config(server, config)? As well as vim.lsp.enable(server)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that was fixed in a later commit in this PR, I was quoting from an earlier one.

@pandar00
Copy link

pandar00 commented May 10, 2025

AFAIK mason-lspconfig setting's handler is a no-op. (PR)
LSP settings are moved to after/lsp/<lang_server_name>.lua (credit; in JP)
lsp-config doc also explains the config is merged with the default.

Note: The merge semantics of configurations follow the behaviour of vim.tbl_deep_extend().

I've been updating my old neovim configs with the latest kickstart but the LSP settings weren't reflected in :LspInfo, but after following the above, LSP correctly reads them

@umutondersu
Copy link
Author

AFAIK mason-lspconfig setting's handler is a no-op. (PR) LSP settings are moved to after/lsp/<lang_server_name>.lua (credit; in JP) lsp-config doc also explains the config is merged with the default.

Note: The merge semantics of configurations follow the behaviour of vim.tbl_deep_extend().

I've been updating my old neovim configs with the latest kickstart but the LSP settings weren't reflected in :LspInfo, but after following the above, LSP correctly reads them

If we were to move LSP configs to their own file, I don't know how mason-tool-installer would work without the servers table maybe by iterating over file names?

In your config I see that you still have the servers table. If It is still gonna be preset wouldn't be better to use vim.lsp.config with a loop (like my first hunk) rather than having 2 separate places for LSP servers?

@umutondersu
Copy link
Author

As mentioned in previous conversations, Mason v2 has started introducing a significant delay during startup. For now, I’ve pinned the Mason version to avoid this issue.

From my testing, the problem appears to be related to mason-tool-installer; without it, I don’t experience any delays. It would be helpful to hear from someone with more insight into this before proceeding further with Mason v2.

@shv187
Copy link

shv187 commented May 10, 2025

My personal temporary solution is to set debounce_hours in mason-tool-installer, to 24 in my case.

@umutondersu
Copy link
Author

After the recent updates mason-tool-installer seems to not have this issue anymore. I will commit a change for Mason V2

-- Installed LSPs are configured and enabled automatically with mason-lspconfig
-- The loop below is for overriding the default configuration of LSPs with the ones in the servers table
for server_name, config in pairs(servers) do
vim.lsp.config(server_name, config)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, before all the servers work, should we just use require("lspconfig").server_name.setup{}? since that currently works for everything

Copy link
Contributor

@pbnj pbnj May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DanWlker - In my testing, the lsp server configs do not get merged with the defaults from lspconfig:

for server_name, server_config in pairs(servers) do
    require("lspconfig")[server_name].setup(server_config)
end

Unless I am missing something.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
image

I'm only using require("lspconfig").server_name.setup{}, not quite sure what you mean by lsp configs not getting merged

@tienbg-zigexn
Copy link

tienbg-zigexn commented May 12, 2025

Currently I'm using the handlers in mason-lspconfig like this:

                handlers = {
                    function(server_name)
                        if server_name == "standardrb" or server_name == "rubocop" then
                            local personal_path = vim.fn.expand("~") .. "/programming/me"
                            local current_dir = vim.fn.getcwd()
                            local in_personal_project = string.find(current_dir, "^" .. personal_path) ~= nil

                            if
                                (server_name == "standardrb" and not in_personal_project)
                                or (server_name == "rubocop" and in_personal_project)
                            then
                                return
                            end
                        end

                        local server = servers[server_name] or {}

                        server.capabilities = vim.tbl_deep_extend("force", {}, capabilities, server.capabilities or {})
                        require("lspconfig")[server_name].setup(server)
                    end,
                },

The purpose is to load standardrb only in personal projects and rubocop on others. How can I migrate this into new version?

@umutondersu
Copy link
Author

umutondersu commented May 12, 2025

Currently I'm using the handlers in mason-lspconfig like this:

                handlers = {
                    function(server_name)
                        if server_name == "standardrb" or server_name == "rubocop" then
                            local personal_path = vim.fn.expand("~") .. "/programming/me"
                            local current_dir = vim.fn.getcwd()
                            local in_personal_project = string.find(current_dir, "^" .. personal_path) ~= nil

                            if
                                (server_name == "standardrb" and not in_personal_project)
                                or (server_name == "rubocop" and in_personal_project)
                            then
                                return
                            end
                        end

                        local server = servers[server_name] or {}

                        server.capabilities = vim.tbl_deep_extend("force", {}, capabilities, server.capabilities or {})
                        require("lspconfig")[server_name].setup(server)
                    end,
                },

The purpose is to load standardrb only in personal projects and rubocop on others. How can I migrate this into new version?

Mason-lspconfig enables all installed LSPs by default, You can disable individual LSPs with vim.lsp.enable(server_name,enable) instead of returning.

oriori1703 added a commit to oriori1703/kickstart-modular.nvim that referenced this pull request May 15, 2025
Copy link
Contributor

@oriori1703 oriori1703 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you should only enable lsps that were installed using ensure_installed.
This way you will mirror the original behaviour.

i.e.

      ---@type MasonLspconfigSettings
      ---@diagnostic disable-next-line: missing-fields
      require('mason-lspconfig').setup {
        automatic_enable = vim.tbl_keys(servers or {}),
      }

@umutondersu umutondersu requested a review from oriori1703 May 17, 2025 11:57
---@type MasonLspconfigSettings
---@diagnostic disable-next-line: missing-fields
require('mason-lspconfig').setup {
automatic_enable = vim.tbl_keys(servers or {}),
Copy link

@SetsuikiHyoryu SetsuikiHyoryu May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@umutondersu @oriori1703

I looked into the source code of mason-lspconfig. When the keys of servers are passed as a table to automatic_enable, it causes mason-lspconfig to ignore any LSPs that are installed via Mason but not explicitly listed in servers.

In the mason-lspconfig.nvim\doc\mason-lspconfig.txtmason-lspconfig.nvim\doc\mason-lspconfig.txt document, it mentions that doing this will only start certain servers:

-- To only enable certain servers to be automatically enabled:
-- ```lua
--   automatic_enable = {
--     "lua_ls",
--     "vimls"
--   }
-- ```

In the setup that in the mason-lspconfig.nvim\lua\mason-lspconfig\init.lua,if automatic_enable is not set to false, both enable_all and init will be executed:

function M.setup(config)
    if config then
        settings.set(config)
    end

    --- ...

    local registry = require "mason-registry"
    registry.refresh(vim.schedule_wrap(function(success, updated_registries)
        if success and #updated_registries > 0 and settings.current.automatic_enable ~= false then
            require("mason-lspconfig.features.automatic_enable").enable_all()
        end
    end))

    if settings.current.automatic_enable ~= false then
        require("mason-lspconfig.features.automatic_enable").init()
    end

    -- ...
end

In mason-lspconfig\features\automatic_enable.lua, The logic for enabling servers in init and enable_all is the same:

return {
    init = function()
        enabled_servers = {}
        _.each(enable_server, registry.get_installed_package_names())
        -- We deregister the event handler primarily for testing purposes where .setup() is called multiple times in the
        -- same instance.
        registry:off("package:install:success", enable_server_scheduled)
        registry:on("package:install:success", enable_server_scheduled)
    end,
    enable_all = function()
        _.each(enable_server, registry.get_installed_package_names())
    end,
}

However, in enable_server, if automatic_enable is a table and an LSP installed by Mason is not listed in automatic_enable, it will not execute vim.lsp.config and vim.lsp.enable.

local function enable_server(mason_pkg)
    -- ...

    local lspconfig_name = mappings.get_mason_map().package_to_lspconfig[mason_pkg]

    -- ...

    local automatic_enable = settings.current.automatic_enable

    if type(automatic_enable) == "table" then
        local exclude = automatic_enable.exclude
        if exclude then
            if _.any(_.equals(lspconfig_name), exclude) then
                -- This server is explicitly excluded.
                return
            end
        else
            if not _.any(_.equals(lspconfig_name), automatic_enable) then
                -- This server is not explicitly enabled.
                return
            end
        end
    elseif automatic_enable == false then
        return
    end

    -- ...

    vim.lsp.enable(lspconfig_name)

    -- ...
end

In other words, even if I’ve already installed the LSPs via Mason, I still have to list all the LSPs I use in servers or in or {}. Isn't that kind of ... not very "lazy"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you are right about automatic_enable.
If set to true it will enable all the LSPs, but when given a table it will only enable those LSPs.
After looking into it more, enabling all the LSPs also seems to be the old behaviour mason.

Despite that , I still prefer declarativly specifying all the LSPs in the config file, instead of downloading them using the mason interface.
This way I can commit those LSPs and have a reproducible setup.
This is the same reason I commit the lazy-lock.json file.

Though I guess this is a matter of taste, and could break setups for people that were used to the old way, so I'm ok with both options :)

@umutondersu @SetsuikiHyoryu what do you think?

Copy link

@SetsuikiHyoryu SetsuikiHyoryu May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oriori1703 @umutondersu

Personally, I prefer the more “lazy” approach, but I tend to follow the method recommended by the Neovim official because I feel it’s more native. That means the operational habits won’t differ too much even when I have to work in a barebones environment. However, I’m not entirely sure what the official philosophy is on this. Judging from the existence of the vim.lsp.enable interface, it seems that the official intent is for LSP to be explicitly enabled.

Perhaps, if Mason provides an interface to retrieve the list of installed packages, a compromise could be to merge that list into the loop that calls vim.lsp.enable (the table passed to automatic_enable).

Sorry, it seems that
_.each(enable_server, registry.get_installed_package_names())
is already configuring only the installed packages. Setting automatic_enable = true doesn't appear to enable LSPs that haven't been installed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.