Skip to content

Latest commit

 

History

History
745 lines (529 loc) · 28.2 KB

lsp.md

File metadata and controls

745 lines (529 loc) · 28.2 KB

LSP

Introduction

Language servers are configured and initialized using nvim-lspconfig.

Ever wondered what lsp-zero does under the hood? Let me tell you.

First it adds data to an option called capabilities in lspconfig's defaults. This new data comes from cmp-nvim-lsp. It tells the language server what features nvim-cmp adds to the editor.

Then it creates an autocommand on the event LspAttach. This autocommand will be triggered every time a language server is attached to a buffer. Is where all keybindings and commands are created.

Finally it calls the .setup() of each language server.

If you were to do it all by yourself, the code would look like this.

local lspconfig = require('lspconfig')
local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'LSP actions',
  callback = function(event)
    local opts = {buffer = event.buf}

    vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
    vim.keymap.set('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>', opts)
    vim.keymap.set('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>', opts)
    vim.keymap.set('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>', opts)
    vim.keymap.set('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>', opts)
    vim.keymap.set('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>', opts)
    vim.keymap.set('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<cr>', opts)
    vim.keymap.set('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>', opts)
    vim.keymap.set({'n', 'x'}, '<F3>', '<cmd>lua vim.lsp.buf.format({async = true})<cr>', opts)
    vim.keymap.set('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>', opts)

    vim.keymap.set('n', 'gl', '<cmd>lua vim.diagnostic.open_float()<cr>', opts)
    vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>', opts)
    vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>', opts) 
  end
})

-- call mason.nvim if installed 
-- require('mason').setup()
-- require('mason-lspconfig').setup()

lspconfig.tsserver.setup({capabilities = lsp_capabilities})
lspconfig.eslint.setup({capabilities = lsp_capabilities})

Commands

  • LspZeroFormat {server} timeout={timeout}: Formats the current buffer or range. Under the hood lsp-zero is using the function vim.lsp.buf.format(). If the "bang" is provided formatting will be asynchronous (ex: LspZeroFormat!). If you provide the name of a language server as a first argument it will try to format only using that server. Otherwise, it will use every active language server with formatting capabilities. With the timeout parameter you can configure the time in milliseconds to wait for the response of the formatting requests.

  • LspZeroWorkspaceRemove: Remove the folder at path from the workspace folders. See :help vim.lsp.buf.remove_workspace_folder().

  • LspZeroWorkspaceAdd: Add the folder at path to the workspace folders. See :help vim.lsp.buf.add_workspace_folder().

  • LspZeroWorkspaceList: List workspace folders. See :help vim.lsp.buf.list_workspace_folders().

  • LspZeroSetupServers [{servers}]: It takes a space separated list of servers and configures them.

Creating new keybindings

Just like the default keybindings the idea here is to create them only when a language server is active in a buffer. For this use the .on_attach() function, and then use neovim's built-in functions create the keybindings.

Here is an example that replaces the default keybinding gr with a telescope command.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})

  vim.keymap.set('n', 'gr', '<cmd>Telescope lsp_references<cr>', {buffer = bufnr})
end)

lsp.setup()

Disable keybindings

To disable all keybindings just delete the call to .default_keymaps().

If you want lsp-zero to skip only a few keys you can add the omit property to the .default_keymaps() call. Say you want to keep the default behavior of K and gs, you would do this.

lsp.default_keymaps({
  buffer = bufnr,
  omit = {'gs', 'K'},
})

Install new language servers

Manual install

You can find the instruction for each language server in lspconfig's documentation: server_configurations.md.

Via command

If you have mason.nvim and mason-lspconfig installed you can use the command :LspInstall to install a language server. If you call this command while you are in a file it'll suggest a list of language server based on the type of that file.

Automatic installs

If you have mason.nvim and mason-lspconfig installed you can use the function .ensure_installed() to list the language servers you want to install with mason.nvim.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.ensure_installed({
  -- Replace these with whatever servers you want to install
  'tsserver',
  'eslint',
  'rust_analyzer'
})

lsp.setup()

Keep in mind the names of the servers must be in this list.

Configure language servers

To pass arguments to a language server you can use the lspconfig directly. Just make sure you call lspconfig after the require of lsp-zero.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

require('lspconfig').eslint.setup({
  single_file_support = false,
  on_attach = function(client, bufnr)
    print('hello eslint')
  end
})

lsp.setup()

For backwards compatibility with the v1.x branch the .configure() function is still available. So this is still valid.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.configure('eslint', {
  single_file_support = false,
  on_attach = function(client, bufnr)
    print('hello eslint')
  end
})

lsp.setup()

The name of the server can be anything lspconfig supports and the options are the same as lspconfig's setup function.

Disable semantic highlights

Neovim v0.9 allows an LSP server to define highlight groups, this is known as semantic tokens. This new feature is enabled by default. To disable it we need to modify the server_capabilities property of the language server, more specifically we need to "delete" the semanticTokensProvider property.

We can disable this new feature in every server using the function .set_server_config().

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.set_server_config({
  on_init = function(client)
    client.server_capabilities.semanticTokensProvider = nil
  end,
})

lsp.setup()

Note that defining an on_init hook in a language server will override the one in .set_server_config().

If you just want to disable it for a particular server, use lspconfig to assign the on_init hook to that server.

require('lspconfig').tsserver.setup({
  on_init = function(client)
    client.server_capabilities.semanticTokensProvider = nil
  end,
})

Disable formatting capabilities

Sometimes you might want to prevent Neovim from using a language server as a formatter. For this you can use the on_init hook to modify the client instance.

require('lspconfig').tsserver.setup({
  on_init = function(client)
    client.server_capabilities.documentFormattingProvider = false
    client.server_capabilities.documentFormattingRangeProvider = false
  end,
})

Disable a language server

Use the function .skip_server_setup() to tell lsp-zero to ignore a particular set of language servers.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.skip_server_setup({'eslint'})

lsp.setup()

Custom servers

There are two ways you can use a server that is not supported by lspconfig:

Add the configuration to lspconfig (recommended)

You can add the configuration to the module lspconfig.configs then you can call the .setup function.

You'll need to provide the command to start the LSP server, a list of filetypes where you want to attach the LSP server, and a function that detects the "root directory" of the project.

Note: before doing anything, make sure the server you want to add is not supported by lspconfig. Read the list of supported LSP servers.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.setup()

local lsp_configurations = require('lspconfig.configs')

if not lsp_configurations.my_new_lsp then
  lsp_configurations.my_new_lsp = {
    default_config = {
      name = 'my-new-lsp',
      cmd = {'my-new-lsp'},
      filetypes = {'my-filetype'},
      root_dir = require('lspconfig.util').root_pattern('some-config-file')
    }
  }
end

require('lspconfig').my_new_lsp.setup({})

Use the function .new_server()

If you don't need a "robust" solution you can use the function .new_server(). This function is just a thin wrapper that calls vim.lsp.start() in a FileType autocommand.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.setup()

lsp.new_server({
  name = 'my-new-lsp',
  cmd = {'my-new-lsp'},
  filetypes = {'my-filetype'},
  root_dir = function()
    return lsp.dir.find_first({'some-config-file'}) 
  end
})

Enable Format on save

You have two ways to enable format on save.

Note: When you enable format on save your LSP server is doing the formatting. The LSP server does not share the same style configuration as Neovim. What do I mean? Tabs and indents can change after the LSP formats the code in the file. Read the documentation of the LSP server you are using, figure out how to configure it to your prefered style.

Explicit setup

If you want to control exactly what language server is used to format a file call the function .format_on_save(), this will allow you to associate a language server with a list of filetypes.

Note: asynchronous formatting on save is experimental right now (2023-05-11).

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.format_on_save({
  format_opts = {
    async = false,
    timeout_ms = 10000,
  },
  servers = {
    ['lua_ls'] = {'lua'},
    ['rust_analyzer'] = {'rust'},
    -- if you have a working setup with null-ls
    -- you can specify filetypes it can format.
    -- ['null-ls'] = {'javascript', 'typescript'},
  }
})

lsp.setup()

Always use the active servers

If you only ever have one language server attached in each file and you are happy with all of them, you can call the function .buffer_autoformat() in the .on_attach hook.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
  lsp.buffer_autoformat()
end)

lsp.setup()

If you have multiple servers active in one file it'll try to format using all of them, and I can't guarantee the order.

Is worth mention .buffer_autoformat() is a blocking (synchronous) function.

If you want something that behaves like .buffer_autoformat() but is asynchronous you'll have to use lsp-format.nvim.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})

  -- make sure you use clients with formatting capabilities
  -- otherwise you'll get a warning message
  if client.supports_method('textDocument/formatting') then
    require('lsp-format').on_attach(client)
  end
end)

lsp.setup()

Format buffer using a keybinding

Using built-in functions

You'll want to bind the function vim.lsp.buf.format() to a keymap. The next example will create a keymap gq to format the current buffer using all active servers with formatting capabilities.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
  local opts = {buffer = bufnr}

  vim.keymap.set({'n', 'x'}, 'gq', function()
    vim.lsp.buf.format({async = false, timeout_ms = 10000})
  end, opts)
end)

lsp.setup()

If you want to allow only a list of servers, use the filter option. You can create a function that compares the current server with a list of allowed servers.

local lsp = require('lsp-zero').preset({})

local function allow_format(servers)
  return function(client) return vim.tbl_contains(servers, client.name) end
end

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
  local opts = {buffer = bufnr}

  vim.keymap.set({'n', 'x'}, 'gq', function()
    vim.lsp.buf.format({
      async = false,
      timeout_ms = 10000,
      filter = allow_format({'lua_ls', 'rust_analyzer'})
    })
  end, opts)
end)

lsp.setup()

Ensure only one LSP server per filetype

If you want to control exactly what language server can format, use the function .format_mapping(). It will allow you to associate a list of filetypes to a particular language server.

Here is an example using gq as the keymap.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.format_mapping('gq', {
  format_opts = {
    async = false,
    timeout_ms = 10000,
  },
  servers = {
    ['lua_ls'] = {'lua'},
    ['rust_analyzer'] = {'rust'},
    -- if you have a working setup with null-ls
    -- you can specify filetypes it can format.
    -- ['null-ls'] = {'javascript', 'typescript'},
  }
})

lsp.setup()

Troubleshooting

Automatic setup failed

To figure out what happened use the function require('lsp-zero.check').run() in command mode, pass a string with the name of the language server.

Here is an example with lua_ls.

:lua require('lsp-zero.check').run('lua_ls')

The name of the language server must match with one in this list: server_configurations.

If the language server is not being configured you'll get a message like this.

LSP server: lua_ls
- was not installed with mason.nvim
- hasn't been configured with lspconfig

This means mason.nvim doesn't have the server listed as "available" and that's why the automatic setup failed. Try re-install with the command :LspInstall.

When everything is fine the report should be this.

LSP server: lua_ls
+ was installed with mason.nvim
+ was configured using lspconfig
+ "lua-language-server" is executable

If it says - "lua-language-server" was not found it means Neovim could not find the executable in the "PATH".

You can inspect your PATH using this command.

:lua vim.tbl_map(print, vim.split(vim.env.PATH, ':'))

Note: if you use windows replace ':' with ';' in the second argument of vim.split.

The executable for your language server should be in one of those folders. Make sure it is present and the file itself is executable.

Root directory not found

You used the command :LspInfo and it showed root directory: Not found. This means nvim-lspconfig couldn't figure out what is the "root" folder of your project. In this case you should go to lspconfig's github repo and browse the server_configurations file, look for the language server then search for root_dir, it'll have something like this.

root_pattern("somefile.json", ".somefile" , ".git")

root_pattern is a function inside lspconfig, it tries to look for one of those files/folders in the current folder or any of the parent folders. Make sure you have at least one of the files/folders listed in the arguments of the function.

Now, sometimes the documentation in lspconfig just says see source file. This means you need to go the source code to figure out what lspconfig looks for. You need to go to the server config folder, click in the file for the language server, look for the root_dir property that is inside a "lua table" called default_config.

Inspect server settings

Let say that you added some "settings" to a server... something like this.

lsp.configure('tsserver', {
  settings = {
    completions = {
      completeFunctionCalls = true
    }
  }
})

Notice here that we have a property called settings, and you want to know if lsp-zero did send your config to the active language server. Use the function require('lsp-zero.check').inspect_settings() in command mode, pass a string with the name of the language server.

:lua require('lsp-zero.check').inspect_settings('tsserver')

If everything went well you should get every default config lspconfig added plus your own.

If this didn't showed your settings, make sure you don't call lspconfig in another part of your neovim config. lspconfig can override everything lsp-zero does.

Inspect the entire server config

Use the function require('lsp-zero.check').inspect_server_config() in command mode, pass a string with the name of the language server.

Here is an example.

:lua require('lsp-zero.check').inspect_server_config('tsserver')

The name of the language server must match with one in this list: server_configurations.

Diagnostics

That's the name neovim uses for error messages, warnings, hints, etc. lsp-zero only does two things to diagnostics: add borders to floating windows and enable "severity sort". All of that can be disable from the .preset() call.

local lsp = require('lsp-zero').preset({
  float_border = 'none',
  configure_diagnostics = false,
})

If you want to disable the "virtual text" you'll need to use the function vim.diagnostic.config().

vim.diagnostic.config({
  virtual_text = false,
})

Use icons in the sign column

If you don't know, the "sign column" is a space in the gutter next to the line numbers. When there is a warning or an error in a line Neovim will show you a letter like W or E. Well, you can turn that into icons if you wanted to, using the function .set_sign_icons.

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.set_sign_icons({
  error = '',
  warn = '',
  hint = '',
  info = '»'
})

lsp.setup()

Language servers and mason.nvim

Install and updates of language servers is done with mason.nvim.

With mason.nvim you can also install formatters and debuggers, but lsp-zero will only configure LSP servers.

To install a server manually use the command LspInstall with the name of the server you want to install. If you don't provide a name mason-lspconfig.nvim will try to suggest a language server based on the filetype of the current buffer.

To check for updates on the language servers use the command Mason. A floating window will open showing you all the tools mason.nvim can install. You can filter the packages by categories for example, language servers are in the second category, so if you press the number 2 it'll show only the language servers. The packages you have installed will appear at the top. If there is any update available the item will display a message. Navigate to that item and press u to install the update.

To uninstall a package use the command Mason. Navigate to the item you want to delete and press X.

To know more about the available bindings inside the floating window of Mason press g?.

If you need to customize mason.nvim make sure you do it before calling the lsp-zero module.

require('mason').setup({
  ui = {
    border = 'rounded'
  }
})

local lsp = require('lsp-zero').preset({})

lsp.on_attach(function(client, bufnr)
  lsp.default_keymaps({buffer = bufnr})
end)

lsp.setup()

Opt-out of mason.nvim

Really all you need is to do is uninstall mason.nvim and mason-lspconfig. Or call .preset() and use modify these settings:

setup_servers_on_start = false
call_servers = 'global'

Then you need to specify which language server you want to setup, for this use .setup_servers() or .configure().

You might not need lsp-zero

Before we start, let me just say this: if you are willing to make an effort, you don't need any plugins at all. You can use Neovim's LSP client without plugins.

But if you do want to have nice things, like automatic setup of language servers, keep reading.

You are going to need these plugins:

And then put the pieces together. So the code I'm about to show does the following: Setup some default keybindings. Use mason.nvim and mason-lspconfig.nvim to manage all the language servers. And finally setup nvim-cmp, which is the autocompletion plugin.

-- note: diagnostics are not exclusive to lsp servers
-- so these can be global keybindings
vim.keymap.set('n', 'gl', '<cmd>lua vim.diagnostic.open_float()<cr>')
vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>')
vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>') 

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'LSP actions',
  callback = function(event)
    local opts = {buffer = event.buf}

    -- these will be buffer-local keybindings
    -- because they only work if you have an active language server

    vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
    vim.keymap.set('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>', opts)
    vim.keymap.set('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>', opts)
    vim.keymap.set('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>', opts)
    vim.keymap.set('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>', opts)
    vim.keymap.set('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>', opts)
    vim.keymap.set('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<cr>', opts)
    vim.keymap.set('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>', opts)
    vim.keymap.set({'n', 'x'}, '<F3>', '<cmd>lua vim.lsp.buf.format({async = true})<cr>', opts)
    vim.keymap.set('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>', opts)
  end
})

local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()

local default_setup = function(server)
  require('lspconfig')[server].setup({
    capabilities = lsp_capabilities,
  })
end

require('mason').setup({})
require('mason-lspconfig').setup({
  ensure_installed = {},
  handlers = {
    default_setup,
  },
})

local cmp = require('cmp')

cmp.setup({
  sources = {
    {name = 'nvim_lsp'},
  },
  mapping = cmp.mapping.preset.insert({
    -- Enter key confirms completion item
    ['<CR>'] = cmp.mapping.confirm({select = false}),

    -- Ctrl + space triggers completion menu
    ['<C-Space>'] = cmp.mapping.complete(),
  }),
  snippet = {
    expand = function(args)
      require('luasnip').lsp_expand(args.body)
    end,
  },
})

If you need a custom config for a language server, add a handler to mason-lspconfig. Like this.

require('mason-lspconfig').setup({
  ensure_installed = {},
  handlers = {
    default_setup,
    lua_ls = function()
      require('lspconfig').lua_ls.setup({
        capabilities = lsp_capabilities,
        ---
        -- This is where you place
        -- your custom config
        ---
      })
    end,
  },
})

Here a "handler" is a lua function that we add to the handlers option. Notice the name of the handler is lua_ls, that is the name of the language server we want to configure. Inside this new lua function we can do whatever we want... but in this particular case what we need to do is use the lspconfig to configure lua_ls.

Inside the {} of lua_ls.setup() is where you configure the language server. Some options are apply to all language servers, these are documented in the help page of lspconfig, see :help lspconfig-setup. Some options are unique to each language server, these live under a property called settings. To know what settings are available, you will need to visit the documentation of the language server you are using.

Here an example using lua_ls. This config is specific to Neovim, so that you don't get annoying warnings in your code.

require('lspconfig').lua_ls.setup({
  capabilities = lsp_capabilities,
  settings = {
    Lua = {
      runtime = {
        version = 'LuaJIT'
      },
      diagnostics = {
        globals = {'vim'},
      },
      workspace = {
        library = {
          vim.env.VIMRUNTIME,
        }
      }
    }
  }
})