Multiple parallel langauge servers

New to nvim-lsp+lua. Been using a combination of ccls+cland with Coc for a while. They both complement each other well. ccls for completion+snippets, clangd for hover, switch to header etc…

With nvim-lsp I dont know how to configure these specifically for each use-type.

My question is

  1. Is there a way to only use clangd for hover function? or disable ccls hover so as to default to clangd for hover?
  2. Navigation/jump-location to sources. I see that there is a feature in clangd to switch_source_header. But dont know how to implement it. If someone has it working, could you please help me?
    Similar troubles with calling ccls navigation fuctions such as $ccls/member, $ccls/caller etc…

I am learning lua slowly, perhpahs in the future I will be able to help myself, but as of now I cannot, and hence cant fully switch from coc to buitin lsp

EDIT - apologies for the double post with git issue and topic here. Did not know this existed when creating the issue

1 Like

I believe you can setup multiple servers, but I haven’t tried it. I used to switch back and forth between ccls and clangd until finally I’ve settled on clangd since v12.0 is much improved (it lacks snippets and a few gotos, but is fairly stable especially with the incremental_sync feature in nvim that’s being worked on right now). You mention you use ccls for completion, do you find its completion better than clangd’s?

I can answer your #2 question with my config for clangd SwitchSourceHeader.
Basically you just need to call vim.lsp.buf_request(bufnr, method, params, handler) where bufnr is the buffer #, params contains the file uri, method is textDocument/switchSourceHeader, and the handler is a callback function which will handle the response.
The callback function gets called once clangd responds, and it’s given the uri of the header/source file (or an err, if clangd can’t find it). There’s a helper function built-into vim to convert a uri to a filename, which is handy here. Then you can have your callback open the file by using normal vim commands (:new filename), or do anything you want with the filename.

I made a little helper function so you can have it open in a new split, vsplit, or current window.

local function switch_source_header_splitcmd(bufnr, splitcmd)
    bufnr = require'lspconfig'.util.validate_bufnr(bufnr)
    local params = { uri = vim.uri_from_bufnr(bufnr) }
    vim.lsp.buf_request(bufnr, 'textDocument/switchSourceHeader', params, function(err, _, result)
        if err then error(tostring(err)) end
        if not result then print ("Corresponding file can’t be determined") return end
        vim.api.nvim_command(splitcmd..' '..vim.uri_to_fname(result))

require'lspconfig'.clangd.switch_source_header_splitcmd = switch_source_header_splitcmd

require'lspconfig'.clangd.setup {
    commands = {
    	ClangdSwitchSourceHeader = {
    		function() switch_source_header_splitcmd(0, "edit") end;
    		description = "Open source/header in a new vsplit";
    	ClangdSwitchSourceHeaderVSplit = {
    		function() switch_source_header_splitcmd(0, "vsplit") end;
    		description = "Open source/header in a new vsplit";
    	ClangdSwitchSourceHeaderSplit = {
    		function() switch_source_header_splitcmd(0, "split") end;
    		description = "Open source/header in a new split";

Then you can call the commands like ordinary vim commands, e.g.
nnoremap <leader>h :ClangdSwitchSourceHeader<CR>


@danngreen quite interesting, feel free to add a clangd section on Advanced configuration · neovim/nvim-lspconfig Wiki · GitHub I can see myself needing this later :wink:

Thanks, I’ll do that!

Thank you so much! This is just fantastic. Not only does it solve this problem, the fact that you detailed the process is an incredible help for me to handle similar functions/requests in the future. I am thinking of how do do similar steps for $ccls/memeber $ccls/callee or such hierarchy commands.

As to your question regarding ccls v clangd, for whatever reason, I so far found ccls to be much faster (possible that I did not config clangd well, either ways) And since ccls can handle snippets for all my use cases (I mostly use openFrameworks) I just start to miss it when I switch to clangd.
also, as I was mentioning in my post, it has these hierarchy based jump to location feature which is fantastic in large projects, clangd does not do this AFAIK,
Although one can say that some of those calls(without hierarchy) are already handled by lsp jump to definition/declaration/refs/ etc…

Thanks again for your wonderful help


It has been a while since I updated this place. if anyone else is interested I have been getting some good suggestions on the issue related to this on neovim github

I have managed to use custom on_init functions to selectively choose completion, hover, formatting between the parallel language servers active for the same buffer/ft.
Where I am stuck is having only one server publish diagnostics. Since it accepts a table and not a nil value I cannot just do
resolved.capabilities.publishDiagnostics = false or make_client_capabilities.textDocument.publishDiagnostics = nil/false does not work
I tried setting custom handler as suggested in LSP help doc, disabling all options such as sign and virtual text. No matter what I do, the LS in question publishes its diagnostics.

Anyone have any suggestions?


Untested, but something like this should do:

     vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(                                                                                                                             
       vim.lsp.diagnostic.on_publish_diagnostics, {                                                                                                                                                                                                                                                                                              
         virtual_text = function(_, client_id)                                                                                                                                                                     
           local client = vim.lsp.get_active_clients()[client_id]
           return == "ccls" -- or whatever is the name of the LSP you want virtual text to come from
           -- alternatively, instead of a boolean (which will get you the defaults,
           -- you can return a table with settings, say if ... then return { spacing = 4 } end                                                                                                                                                                               

You can of course, replicate that for signs, underline, etc.


Thank you. Just tried this (implementing for sings , underline etc)
I am still in the same situation. I even tried disabling all of then in the custom handler function as described in LSP.txt help file. I am sure I am sure I am overlooking some other function that sets this to default, else this should work.

EDIT - also, with your suggestion it disables the diagnostic publish to all other LS (lua for example)

I would instead override the handler argument with the above in the call to setup{}, that should only override the handler for the server you want to silence.

1 Like

@mjlbach I’m not sure what this would look like, could you show the code for such a call to setup{}?

If you want to disable diagnostics for a single language server:

nvim_lsp['pyright'].setup {
  handlers = {
    ["textDocument/publishDiagnostics"] =  function(...)
      return nil

Note diagnostics are still computed/sent from the language server side, just ignored.


@mjlbach thank you this is perfect! I knew I had to pass in a nil table (or function in this case). Just too new to lua to manage that well without help!

Thank you

If there is ever a possibility to make sure the server does not even compute the diagnostics that would be ideal. But this is good enough!

I’m not really sure, you can try altering the capabilities like so:

default_capabilities = vim.lsp.protocol.make_client_capabilities()
default_capabilities.textDocument.publishDiagnostics = nil
nvim_lsp['pyright'].setup {

For pyright, at least, the server ignores disabling the capabilities and provide diagnostics anyways, although I didn’t experiment with this much. To reiterate, we don’t send a diagnostics request, the server automatically updates diagnostics on each change and provides them back to the client, so there’s not much control we (really any language server client) have other than debouncing the buffer updates (which mfusseneger has a PR to do).


Thanks I tried that exact same thing in the beginning. I tried it on a couple of LS. clangd ignored it all the same. ccls just did not init.
I had just assumed I got the syntax wrong. Good to know the reason now!

Your earlier solution works perfectly anyway. So that is just fine!
Thanks for your help. Appreciate it

I think the latest update that added the ability to choose the language server for calls like textDocument/formatting broke something for clangd.

Where I use clangd in parallel with a severely restricted ccls as discussed above or if I use it with efm (cpplint), it has trouble executing ‘textDoucment/swithcSourceHeader’ as it tries to implement the call with ALL attached servers.
If it were other calls I could either set a nil handler or disable the resolve_capabilities for other LSs, but since only clangd has this functionality, I cant help this

Error executing vim.schedule lua callback: ...ack/packer/start/nvim-lspconfig/lua/lspconfig/clangd.lua:9: RPC[Error] code_name = MethodNotFound, message = "unknown request textDocument/swit

Why would that have affected that command in particular? Nothing was changed with other calls. You can easily test this by reverting the commit on a local checkout of neovim. I’m guessing the regression is orthogonal. The request type may have also been changed for clangd (server side).

Thank you for your response

These are my reasons to believe. I will verify against a previous commit, but this is why I thought it:

  1. havent updated clangd. all clang/llvm tools have been on 11.01 for a few months now
  2. After the commit in question, for all handlers that are common between running language servers, the lsp would give me the choice to execute the calls between them.
    I have disabled all calls from ccls execpt whats required for this amazing plugin, and hence lsp does not give me that choice for the disabled capabilities and manually set nil handlers anymore
  3. if its only clangd running without ccls or efm. I dont get the above mentioned error

I will verify against a reverted version and if it persists I file an issue on git

I can reproduce with minimal_init.lua

So I guess I will file a git issue on neovim?

Did you revert that specific series of commits with a minimal init.lua and verify that it was that PR? Otherwise, do not file an issue.

I could not figure out which specific commit that was so I just found a lsp PR from a week ago and did git reset --hard to that commit head.

I am now on 21035cf [two weeks ago] and I can still reproduce. I will keep trying