LSP mix and match - cherry-pick capabilities from different LSP servers

4 minute read

LSP mix and match - cherry-pick capabilities from different LSP servers

As I have written about here, for most of my file edititing / programming / note-taking / … I use vim (neovim).

Not a bare-bones neovim, though. I use many plugins, and I have even more of the set up. Too many? Maybe. Probably. Especially considering the ever growing number of software supply chain attacks..

Anyways, that is a different topic for a different day.

Besides convenient plugins, the main building block for developer experience enhancement and efficiency are language servers, which provide stuff like diagnostics, code completion, navigation enhancements and more.

Besides playing around with its config, I try to actually use vim for productive work, including programming in python.

My go-to language server for python has been basedpyright, a fork of microsoft’s pyright, which basically adds the functionality of the proprietary pylance extension to pyright and adds some more bells and whistles.

While basedpyright is stable, helpful, convenient and generally excellent, there are a few new kids on the block:

There is a new generation of python language servers, written in Rust, targeting high performance and productivity in large and complex code bases:

There is Astral’s ty, Meta’s pyrefly and David Halter’s (Jedi’s original author) zuban, and probably some more.

I have been intrigued by them, mainly hoping for a smoother and more performant workflow in large codebases. So, I have played around with them for a while now, eventually ending up using ty only for type checking while still having to rely on basedpyright for completion, auto-import, symbol renaming, navigation etc.

Why?

In single capabilities, none of the new lsp servers match basedpyright in my workflow. Yet.

E.g.:

  • pyrefly has great completion, navigation and even auto-import, but it’s really bad in renaming symbols. It can rename variables, but no functions.
  • ty has limited support of pydantic, which I use a lot, completion is not as powerful as pyrefly’s, but renaming works great.

Why not use both?

If you enable several language servers with the (nominally) same capabilities for the same file type, you get duplicate results/actions for every capability. Go to definition? Select where you want to jump to from these two identical alternatives!

My goal was to mix-and-match, e.g. use ty for renaming and pyrefly for everything else. However, while especially pyrefly’s configuration is very flexible, the lsps don’t support tailored enabling and disabling of select capabilities.

I tried for an unsuccessful while to configure the lsp server by restricting the handed-over client capabilities, but that didn’t go anywhere.

The only almost compromise that worked for me with the configuration through the lsp servers themselves was disabling basedpyright’s type checking and disabling ty’s language server capabilities, besides type checking. Like this, at least a little bit of the heavy lifting could be offloaded from basedpyright to ty, but I was not happy.

Now, I sort of am happy! I found out how to disable specific lsp capabilities for specific lsp servers through nvim’s lsp configuration. Instead of trying to get the lsp server to not provide the functionality, the client ignores it!

The Problem

I like working with Python, also with larger code bases. There are LSP servers that offer excellent performance. However, none match the full functionality of basedpyright when used independently. Unfortunately, basedpyright lacks in performance. Combining multiple LSP servers introduces duplicate functionality and a bit of a mess. Such combination could be feasible if we could selectively disable or enable conflicting capabilities. However, it is not possible to instruct LSP servers to withhold certain functionalities unless they provide an interface for that, e.g. with dedicated configuration options.

The solution

Enable conflicting lsp servers, but tell the lsp client (nvim) that it should not use specific lsp server’s capabilities through an on_init hook 🥳!

The configuration is probably not fully final and will change, but this is the general thing.

  local lsp_capabilities = require("blink.cmp").get_lsp_capabilities(nil, true)

  ---@param client vim.lsp.Client
  local ty_on_init = function(client)
    client.server_capabilities.callHierarchyProvider = false
    client.server_capabilities.completionProvider = false
    client.server_capabilities.inlineCompletionProvider = false
    client.server_capabilities.inlineValueProvider = false
    client.server_capabilities.declarationProvider = false
    client.server_capabilities.definitionProvider = false
    client.server_capabilities.implementationProvider = false
    client.server_capabilities.inlayHintProvider = false
    client.server_capabilities.signatureHelpProvider = false
    client.server_capabilities.hoverProvider = false
    client.server_capabilities.referencesProvider = false
  end
  vim.lsp.config("ty", {
    capabilities = lsp_capabilities,
    on_init = { ty_on_init },
    settings = {
      ty = {
        disableLanguageServices = false,
        diagnosticMode = "workspace",
        experimental = {
          rename = true,
        },
      },
    },
  })

  ---@param client vim.lsp.Client
  local pyrefly_on_init = function(client)
    client.server_capabilities.renameProvider = false
  end
  vim.lsp.config("pyrefly", {
    capabilities = lsp_capabilities,
    on_init = { pyrefly_on_init },
    settings = {
      python = {
        pyrefly = {
          disableLanguageServices = false,
          displayTypeErrors = "force-on",
        },
        analysis = {
          diagnosticMode = "workspace",
          inlayHints = {
            callArgumentNames = "all",
            variableTypes = true,
            functionReturnTypes = true,
            pytestParameters = true,
          },
        },
      },
    },
  })

tl;dr

  • Some lsp servers are not powerful enough to fully replace alternatives but would be great enhancements or replacements for single capabilities
  • nvim’s lsp config API allows for specifying different hooks, on_init and on_attach etc.
  • It’s possible to disable specific capabilities in those hooks

Leave a comment