Switching to NeoVim Native LSP
I do all of my day to day work in vim, specifically NeoVim . One of my favorite plugins as of late has been the CoC plugin. CoC provides language server integrations, autocompletion, linting, and formatting all in one easy use package. With the success of LSP in VScode and plugins like CoC, the NeoVim team has provided a native implementation of a language server client in 0.5.0. In addition to language-server support, the 0.5.0 release contains a host of new features including lua scripting, and a new lanaguage syntax compiler (treesitter). I was interested in finding out how the native LSP support compared to CoC and wanted to share the resulting configuration I came up with.
Language Server?
I’ve used this term and its abbreviation LSP a few times already. The language-server protocol is a client/server model that editors can implement to connect to compilers and interpreters running in a special mode. LSP clients make RPC requests to attached server processes. These commands get symbol information, diagnostic results, or queries for context on what the user is doing. Language servers can be implemented in any language, and run in their own process. This client/server relationship is similar to the relationship that web-browser and a web-servers have. Instead of HTTP, LSP uses more limited and specific set of operations with pre-defined structures. The split between client and server allow languages like PHP, Python and Typescript provide platform specific features and integrate with any client. Clients like NeoVim, CoC, or VSCode benefit from only having to implement one way to collect autocomplete and diagnostic errors from the outside world.
Converting to the new language server
Now that you’re done drinking the kool-aid lets get going. First we need to install a few global npm packages. For me that means using volta for you that might be npm install -g
. Either way you’ll need the following npm packages:
- # For python, php and linter bridge
- volta install pyright intelephense diagnostic-languageserver
- # For typescript
- volta install typescript typescript-language-server
Next we have to install a few new vim plugins. Again there are many ways to do this but I’m using vim-plug:
Next, update vimrc
to include a new lua module as LSP configuration can be a bit verbose.
Next we have to define which language servers to use, and define keymappings to interact with the language servers.
- -- Put in ~/.vim/lua/lsp-config.lua
- --- Configuration for LSP, formatters, and linters.
- local nvim_lsp = require("lspconfig")
- local saga = require("lspsaga")
- -- Completion setup
- local compe = require("compe")
- vim.o.completeopt = "menuone,noselect"
- compe.setup {
- enabled = true;
- autocomplete = true;
- throttle_time = 200;
- source_timeout = 150;
- source = {
- -- only read from lsp and lua as I find buffer, path and others noisy.
- nvim_lsp = true;
- nvim_lua = true;
- }
- }
- -- short cut methods.
- local t = function(str)
- return vim.api.nvim_replace_termcodes(str, true, true, true)
- end
- local check_back_space = function ()
- local col = vim.fn.col('.') - 1
- return col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') ~= nil
- end
- -- Global function used to send <C-n> to compe
- _G.tab_complete = function()
- if vim.fn.pumvisible() == 1 then
- return t "<C-n>"
- elseif check_back_space() then
- return t "<Tab>"
- else
- return vim.fn['compe#complete']()
- end
- end
- -- Handler to attach LSP keymappings to buffers using LSP.
- local on_attach = function(client, bufnr)
- -- helper methods for setting keymaps
- local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
- local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end
- vim.opt_local.omnifunc = "v:lua.vim.lsp.omnifunc"
- --- Mappings
- local opts = { noremap=true, silent=true }
- buf_set_keymap('n', 'gh', "<cmd>lua require('lspsaga.provider').lsp_finder()<CR>", opts)
- buf_set_keymap('n', 'K', "<cmd>lua require('lspsaga.hover').render_hover_doc()<CR>", opts)
- -- Scroll down in popups
- buf_set_keymap('n', '<C-b>', "<cmd>lua require('lspsaga.action').smart_scroll_with_saga(1)<CR>", opts)
- -- Navigate and preview
- buf_set_keymap('n', 'gs', "<cmd>lua require('lspsaga.signaturehelp').signature_help()<CR>", opts)
- buf_set_keymap('n', 'gd', "<cmd>lua require('lspsaga.provider').preview_definition()<CR>", opts)
- buf_set_keymap('n', 'gD', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
- buf_set_keymap('n', 'gr', "<cmd>lua require('lspsaga.rename').rename()<CR>", opts)
- -- View diagnostics
- buf_set_keymap('n', '<space>e', "<cmd>lua require('lspsaga.diagnostic').show_line_diagnostics()<CR>", opts)
- buf_set_keymap('n', '[d', "<cmd>lua require('lspsaga.diagnostic').lsp_jump_diagnostic_prev()<CR>", opts)
- buf_set_keymap('n', ']d', "<cmd>lua require('lspsaga.diagnostic').lsp_jump_diagnostic_next()<CR>", opts)
- -- Autocomplete
- buf_set_keymap("i", "<C-Space>", 'compe#complete()', {noremap = true, silent = true, expr = true})
- buf_set_keymap("i", "<CR>", "compe#confirm('<CR>')", {noremap = true, silent = true, expr = true})
- buf_set_keymap("i", "<Esc>", "compe#close('<Esc>')", {noremap = true, silent = true, expr = true})
- buf_set_keymap("i", "<Tab>", "v:lua.tab_complete()", {expr = true})
- end
- -- Typescript
- nvim_lsp.tsserver.setup {
- on_attach = function(client, bufnr)
- -- Disable tsserver formatting as prettier/eslint does that.
- client.resolved_capabilities.document_formatting = false
- on_attach(client, bufnr)
- end
- }
- -- Python
- nvim_lsp.pyright.setup {
- on_attach = on_attach,
- }
- -- PHP
- nvim_lsp.intelephense.setup {
- on_attach = on_attach,
- }
- saga.init_lsp_saga {
- error_sign = '\u{F658}',
- warn_sign = '\u{F071}',
- hint_sign = '\u{F835}',
- }
This configuration is what I’m running at time of writing and I’m quite happy with it. I feel that the native LSP client is a bit faster than what I was using before, but I’ve not actually measured response times. The UX from LSP is lovely and I apprecate the file previews that it renders.
Linters
Not all the linters I use have direct LSP implementations. Thankfully for these we can use diagnosticls
to make tools like eslint
and flake8
quack like an LSP server.
- -- Put in ~/.vim/lua/lsp-config.lua
- --- Linter setup
- local filetypes = {
- typescript = "eslint",
- typescriptreact = "eslint",
- python = "flake8",
- php = {"phpcs", "psalm"},
- }
- local linters = {
- eslint = {
- sourceName = "eslint",
- command = "./node_modules/.bin/eslint",
- rootPatterns = {".eslintrc.js", "package.json"},
- debouce = 100,
- args = {"--stdin", "--stdin-filename", "%filepath", "--format", "json"},
- parseJson = {
- errorsRoot = "[0].messages",
- line = "line",
- column = "column",
- endLine = "endLine",
- endColumn = "endColumn",
- message = "${message} [${ruleId}]",
- security = "severity"
- },
- securities = {[2] = "error", [1] = "warning"}
- },
- flake8 = {
- command = "flake8",
- sourceName = "flake8",
- args = {"--format", "%(row)d:%(col)d:%(code)s: %(text)s", "%file"},
- formatPattern = {
- "^(\\d+):(\\d+):(\\w+):(\\w).+: (.*)$",
- {
- line = 1,
- column = 2,
- message = {"[", 3, "] ", 5},
- security = 4
- }
- },
- securities = {
- E = "error",
- W = "warning",
- F = "info",
- B = "hint",
- },
- },
- phpcs = {
- command = "vendor/bin/phpcs",
- sourceName = "phpcs",
- debounce = 300,
- rootPatterns = {"composer.lock", "vendor", ".git"},
- args = {"--report=emacs", "-s", "-"},
- offsetLine = 0,
- offsetColumn = 0,
- sourceName = "phpcs",
- formatLines = 1,
- formatPattern = {
- "^.*:(\\d+):(\\d+):\\s+(.*)\\s+-\\s+(.*)(\\r|\\n)*$",
- {
- line = 1,
- column = 2,
- message = 4,
- security = 3
- }
- },
- securities = {
- error = "error",
- warning = "warning",
- },
- requiredFiles = {"vendor/bin/phpcs"}
- },
- psalm = {
- command = "./vendor/bin/psalm",
- sourceName = "psalm",
- debounce = 100,
- rootPatterns = {"composer.lock", "vendor", ".git"},
- args = {"--output-format=emacs", "--no-progress"},
- offsetLine = 0,
- offsetColumn = 0,
- sourceName = "psalm",
- formatLines = 1,
- formatPattern = {
- "^[^ =]+ =(\\d+) =(\\d+) =(.*)\\s-\\s(.*)(\\r|\\n)*$",
- {
- line = 1,
- column = 2,
- message = 4,
- security = 3
- }
- },
- securities = {
- error = "error",
- warning = "warning"
- },
- requiredFiles = {"vendor/bin/psalm"}
- }
- }
- nvim_lsp.diagnosticls.setup {
- on_attach = on_attach,
- filetypes = vim.tbl_keys(filetypes),
- init_options = {
- filetypes = filetypes,
- linters = linters,
- },
- }
Formatters
Because linters are also often used to do formatting, more bridge code is necessary. While diagnosticls
seems like it should support formatters, I wasn’t able to get it working. Instead I found a small plugin that does just what I need. The formatter.nvim package lets you define a list of formatters by filetype. I did need to apply the following pull request to get phpcbf
working as it returns a non-zero exit code even when it ‘works’.
- -- Put in ~/.vim/lua/formatting.lua
- -- Formatters
- -- Formatting can be run via :Format
- local formatter = require('formatter')
- local eslint_fmt = {
- function()
- return {
- exe = "./node_modules/.bin/eslint",
- args = {"--fix", "--stdin-filename", vim.api.nvim_buf_get_name(0)},
- stdin = false,
- }
- end
- }
- formatter.setup {
- logging = true,
- filetype = {
- typescript = eslint_fmt,
- typescriptreact = eslint_fmt,
- javascript = eslint_fmt,
- javascriptreact = eslint_fmt,
- python = {
- function ()
- return {
- exe = '~/.pyenv/shims/black',
- args = {"-"},
- stdin = true,
- }
- end,
- function ()
- return {
- exe = '~/.pyenv/shims/isort',
- args = {"-"},
- stdin = true,
- }
- end
- },
- php = {
- function ()
- return {
- exe = './vendor/bin/phpcbf',
- args = {'--stdin-path=' .. vim.api.nvim_buf_get_name(0), '-'},
- stdin = true,
- ignore_exitcode = true,
- }
- end
- }
- }
- }
And that’s it. While it is significantly more code than getting CoC working, I’m happy with the results as it forced me to learn some lua, and do some housekeeping in my overall vim configuration. I’m also pleased with the output from lspsaga
as it makes the whole experience lovely. If you’re looking for the finished results you can find them in my vim-files repository
There are no comments, be the first!