my-nvim-lsp-setup.mdx
1 --- 2 draft: true 3 title: My setup for Neovim's builtin LSP client 4 date: 2020-12-18 5 description: A post where I explain about my setup for Neovim's builtin LSP 6 tags: 7 - neovim 8 --- 9 10 [lsp-guide-vscode]: https://code.visualstudio.com/api/language-extensions/language-server-extension-guide 11 [lsp-website]: https://microsoft.github.io/language-server-protocol/ 12 [tj-vimconf]: https://www.youtube.com/watch?v=C9X5VF9ASac 13 [nvim-lspconfig]: https://github.com/neovim/nvim-lspconfig 14 [nvim-compe]: https://github.com/hrsh7th/nvim-compe 15 [packer-nvim]: https://github.com/wbthomason/packer.nvim 16 [coc-nvim]: https://github.com/neoclide/coc.nvim 17 [lspsaga-nvim]: https://github.com/glepnir/lspsaga.nvim 18 [telescope-nvim]: https://github.com/nvim-telescope/telescope.nvim 19 [lsp-init-lua]: https://github.com/elianiva/dotfiles/blob/950ba38bda8230da8071fc72cf3d8617d6288565/config/nvim/lua/modules/lsp/init.lua 20 [vim-vsnip]: https://github.com/hrsh7th/vim-vsnip 21 [snippets-nvim]: https://github.com/norcalli/snippets.nvim 22 [completion-nvim]: https://github.com/nvim-lua/completion-nvim 23 [tj-twitch]: https://www.twitch.tv/teej_dv 24 [null-config]: https://github.com/elianiva/dotfiles/blob/950ba38bda8230da8071fc72cf3d8617d6288565/config/nvim/lua/plugins/null-ls.lua 25 [null-ls]: https://github.com/jose-elias-alvarez/null-ls.nvim 26 [lsp-mappings]: https://github.com/elianiva/dotfiles/blob/950ba38bda8230da8071fc72cf3d8617d6288565/config/nvim/lua/modules/lsp/mappings.lua 27 [diagnostic-nvim]: https://github.com/nvim-lua/diagnostic-nvim 28 [big-pr]: https://github.com/neovim/neovim/pull/12655 29 30 import Update from "~/components/Update.astro"; 31 32 # What is LSP and Why? 33 34 > **20-08-2021**: This post is no longer maintained because I've changed my config quite a bit since I wrote this and I don't feel like updating it :p 35 36 If you don't already know what LSP is, well, LSP is a Language Server Protocol and it was created by Microsoft. It's a better implementation of language support for a text editor. Instead of having to implement it for every language on every text editor, we only need a server for a specific language and a client for a text editor that can speak to the server. 37 38 Imagine the editor as `X` and language feature as `Y`, the first solution would take `X*Y` to implement because it needs to implements _every_ language features for _every_ editor. The second solution which is the LSP way would only take `X+Y` because it would only take a server for the language and a client that can speak to that server. The server can be used for any text editor that has a client and the client can speak to any LSP server. No more reinventing the wheel, great! 39 40 Here are some resources that explain LSP _way better_ and in more detail. 41 42 - [LSP guide for VScode][lsp-guide-vscode] 43 - [Official page for LSP][lsp-website] 44 - [TJ's talk about LSP on Vimconf 2020][tj-vimconf] 45 46 # Neovim builtin LSP client 47 48 I use Neovim's built-in LSP client which only available on the `master` branch of Neovim at the time of writing this. I was using [coc.nvim][coc-nvim] but it was slow on my machine because it uses node and it's a remote plugin which adds some overhead. It still works great nonetheless, it's just slow on my machine. 49 50 The new neovim's built-in LSP client is written in Lua and Neovim ships with LuaJIT which makes it super fast. 51 52 # Configuration 53 54 ## nvim-lspconfig 55 56 Neovim has a repo with LSP configuration for a various language called [nvim-lspconfig][nvim-lspconfig], this is _NOT_ where the LSP client lives, the client already ships with Neovim. It's just a repo that holds the configuration for the client. 57 58 I have this piece of code on my config to install it. I use [packer.nvim][packer-nvim] 59 60 ```lua 61 use {'neovim/nvim-lspconfig', opt = true} -- builtin lsp config 62 ``` 63 64 ## Setup 65 66 I have a directory filled with LSP related config. Here's some snippet that sets up the LSP. 67 68 ```lua 69 local custom_on_attach = function() 70 mappings.lsp_mappings() 71 72 end 73 74 local custom_on_init = function(client) 75 print('Language Server Protocol started!') 76 77 if client.config.flags then 78 client.config.flags.allow_incremental_sync = true 79 end 80 end 81 82 nvim_lsp.gopls.setup{ 83 on_attach = custom_on_attach, 84 on_init = custom_on_init, 85 } 86 ``` 87 88 I made a `custom_on_attach` function to attach LSP specific mappings. I also made a custom `on_init` function to notify me when the LSP is started and enable `incremental_sync`. Though, I'm not sure if `on_init` is the correct thing that I'm looking for. Sometimes it notifies me when the LSP server hasn't even started yet :p 89 90 <Update date="2021-02-04"> 91 92 I've updated my config to use a _better_ way to set them up. Basically, I have a key-value pair table, each item is a table with the server name as its key. This way, I wouldn't need to copy and paste `nvim_lsp.lsp_name.setup{...}`. 93 94 </Update> 95 96 You can find the full content of this file [here][lsp-init-lua] 97 98 ## Mappings 99 100 Here are some of my LSP related mappings which you can find in the file [here][lsp-mappings] 101 102 ```lua 103 local remap = vim.api.nvim_set_keymap 104 local M = {} 105 106 local signature = require("lspsaga.signaturehelp") 107 -- other LSP saga modules 108 109 M.lsp_mappings = function() 110 if type == "jdtls" then 111 nnoremap({ "ga", require("jdtls").code_action, { silent = true } }) 112 else 113 nnoremap({ "ga", require("plugins._telescope").lsp_code_actions, { silent = true } }) 114 end 115 116 inoremap({ "<C-s>", signature.signature_help, { silent = true } }) 117 -- some other mappings here 118 end 119 120 return M 121 ``` 122 123 ## Language-specific config 124 125 I have most of my LSP config to be default but I gave several LSP an option like `tsserver`, `svelteserver`, or `sumneko_lua`. 126 127 ### tsserver 128 129 I have my `tsserver` to be started on every JS/TS file regardless of its directory. The default config will only start when it found `package.json` or `.git`. 130 131 ````lua 132 nvim_lsp.tsserver.setup{ 133 ======= 134 I have my `tsserver` to be started on every JS/TS file regardless of its directory. With the default config, it will only start when it found `package.json` or `.git` which marks the root directory for the LSP. 135 136 ```lua 137 -- inside the `servers` table 138 tsserver = { 139 >>>>>>> 06f717c (I ACCIDENTALLY DELETED MY LOCAL REPOSITORY LMAO HELP) 140 filetypes = { 'javascript', 'typescript', 'typescriptreact' }, 141 on_attach = custom_on_attach, 142 on_init = custom_on_init, 143 root_dir = function() return vim.loop.cwd() end 144 } 145 ```` 146 147 ### svelteserver 148 149 I disabled its HTML emmet suggestion and removed `>` and `<` from `triggerCharacters`. They're so annoying to me. 150 151 ```lua 152 -- inside the `servers` table 153 svelteserver = { 154 on_attach = function(client) 155 mappings.lsp_mappings() 156 157 client.server_capabilities.completionProvider.triggerCharacters = { 158 ".", '"', "'", "`", "/", "@", "*", 159 "#", "$", "+", "^", "(", "[", "-", ":" 160 } 161 end, 162 on_init = custom_on_init, 163 handlers = { 164 ["textDocument/publishDiagnostics"] = is_using_eslint, 165 }, 166 filetypes = { 'html', 'svelte' }, 167 settings = { 168 svelte = { 169 plugin = { 170 -- some settings 171 }, 172 }, 173 }, 174 } 175 ``` 176 177 ### sumneko_lua 178 179 [lua-language-server][lua-ls] is a bit different because I compiled it from source so it needs some extra setup. 180 181 ```lua 182 local sumneko_root = os.getenv("HOME") .. "/repos/lua-language-server" 183 184 -- inside the `servers` table 185 sumneko_lua = { 186 cmd = { 187 sumneko_root .. "/bin/Linux/lua-language-server", 188 "-E", 189 sumneko_root .. "/main.lua", 190 }, 191 on_attach = custom_on_attach, 192 on_init = custom_on_init, 193 settings = { 194 Lua = { 195 runtime = { version = "LuaJIT", path = vim.split(package.path, ";") }, 196 diagnostics = { 197 enable = true, 198 globals = { 199 "vim", "describe", "it", "before_each", "after_each", 200 "awesome", "theme", "client", "P", 201 }, 202 }, 203 workspace = { 204 preloadFileSize = 400, 205 }, 206 }, 207 }, 208 } 209 ``` 210 211 ## Diagnostic 212 213 I was using [diagnostic-nvim][diagnostic-nvim] before [this big PR][big-pr] got merged which makes diagnostic-nvim redundant. Here's some of my diagnostic config. 214 215 ```lua 216 vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( 217 vim.lsp.diagnostic.on_publish_diagnostics, { 218 virtual_text = { 219 prefix = "»", 220 spacing = 4, 221 }, 222 signs = true, 223 update_in_insert = false, 224 } 225 ) 226 227 vim.fn.sign_define('LspDiagnosticsSignError', { text = "", texthl = "LspDiagnosticsDefaultError" }) 228 vim.fn.sign_define('LspDiagnosticsSignWarning', { text = "", texthl = "LspDiagnosticsDefaultWarning" }) 229 vim.fn.sign_define('LspDiagnosticsSignInformation', { text = "", texthl = "LspDiagnosticsDefaultInformation" }) 230 vim.fn.sign_define('LspDiagnosticsSignHint', { text = "", texthl = "LspDiagnosticsDefaultHint" }) 231 ``` 232 233 I set the prefix for `virtual_text` to be `»` because I don't really like the default one and enabled `signs` for the diagnostic hint. I also made it to only update the diagnostic when I switch between insert mode and normal mode because it's quite annoying when I haven't finished typing and get yelled at by LSP because it expects me to put `=` after a variable name that I haven't even finished typing yet. 234 235 ## Linting and Formatting 236 237 I recently started using [null-ls][efm-ls] to run [eslint](https://eslint.org) and formatters like [prettier](https://prettier.io) and [stylua](https://github.com/johnnymorganz/stylua). 238 239 You can get my full config for `null-ls` [here][null-config] 240 241 ## Diagnostic Conflict 242 243 When I use efm-langserver, the diagnostic that comes from the LSP (like `tsserver`) and external linter that efm-langserver uses are conflicting. So, I made a custom function for it to check if there's a file like `.eslintrc.js`, it will turn off the diagnostic that comes from LSP and use ESlint instead. 244 245 <Update date="2021-01-01"> 246 247 I've found a better way from one of [TJ's][tj-twitch] stream to do this which looks like this. 248 249 </Update> 250 251 ```lua 252 local is_using_eslint = function(_, _, result, client_id) 253 if is_cfg_present("/.eslintrc.json") or is_cfg_present("/.eslintrc.js") then 254 return 255 end 256 257 return vim.lsp.handlers["textDocument/publishDiagnostics"](_, _, result, client_id) 258 end 259 ``` 260 261 I've overridden the `vim.lsp.handlers["textDocument/publishDiagnostics"]` anyway so reusing it would also works and it looks way cleaner. 262 263 ## Completion and Snippets 264 265 I use a completion and snippet plugin to make my life easier. For completion, I use [nvim-compe][nvim-compe], previously I was using [completion-nvim][completion-nvim] but I had some issues with it such as path completion sometimes not showing up and flickering. 266 267 Snippet wise, I use [vim-vsnip][vim-vsnip]. I was going to use [snippets.nvim][snippets-nvim] but it doesn't integrate well enough with LSP's snippet. 268 269 Here's some of my `nvim-compe` config 270 271 ```lua 272 local remap = vim.api.nvim_set_keymap 273 274 vim.g.vsnip_snippet_dir = vim.fn.stdpath("config").."/snippets" 275 276 require("compe").setup({ 277 enabled = true, 278 debug = false, 279 min_length = 2, 280 preselect = "disable", 281 source_timeout = 200, 282 incomplete_delay = 400, 283 allow_prefix_unmatch = false, 284 285 source = { 286 path = true, 287 calc = true, 288 buffer = true, 289 vsnip = true, 290 nvim_lsp = true, 291 nvim_lua = true, 292 }, 293 }) 294 295 Util.trigger_completion = function() 296 if vim.fn.pumvisible() ~= 0 then 297 if vim.fn.complete_info()["selected"] ~= -1 then 298 return vim.fn["compe#confirm"]() 299 end 300 end 301 302 local prev_col, next_col = vim.fn.col(".") - 1, vim.fn.col(".") 303 local prev_char = vim.fn.getline("."):sub(prev_col, prev_col) 304 local next_char = vim.fn.getline("."):sub(next_col, next_col) 305 306 -- minimal autopairs-like behaviour 307 if prev_char == "{" and next_char == "" then return Util.t("<CR>}<C-o>O") end 308 if prev_char == "[" and next_char == "" then return Util.t("<CR>]<C-o>O") end 309 if prev_char == "(" and next_char == "" then return Util.t("<CR>)<C-o>O") end 310 if prev_char == ">" and next_char == "<" then return Util.t("<CR><C-o>O") end -- html indents 311 312 return Util.t("<CR>") 313 end 314 315 remap( 316 "i", 317 "<CR>", 318 "v:lua.Util.trigger_completion()", 319 { expr = true, silent = true } 320 ) 321 remap( 322 "i", 323 "<Tab>", 324 table.concat({ 325 "pumvisible() ? \"<C-n>\" : v:lua.Util.check_backspace()", 326 "? \"<Tab>\" : compe#confirm()", 327 }), 328 { silent = true, noremap = true, expr = true } 329 ) 330 331 remap( 332 "i", 333 "<S-Tab>", 334 "pumvisible() ? \"<C-p>\" : \"<S-Tab>\"", 335 { noremap = true, expr = true } 336 ) 337 remap( 338 "i", 339 "<C-Space>", 340 "compe#complete()", 341 { noremap = true, expr = true, silent = true } 342 ) 343 ``` 344 345 You can get the full config for my completion setup [here](https://github.com/elianiva/dotfiles/blob/5f813d893ff5a5928bac52995d6b4f806a8b3d2a/nvim/.config/nvim/lua/plugins/_completion.lua) 346 347 # Closing Note 348 349 I'm pretty pleased with my current setup. Kudos to Neovim's developer that brings LSP client to be a built-in feature! These are of course some other great LSP client alternatives for (Neo)vim, definitely check them out! 350 351 - [coc.nvim](https://github.com/neoclide/coc.nvim) (highly recommend this if you're just getting started) 352 - [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim) 353 - [vim-lsp](https://github.com/prabirshrestha/vim-lsp) 354 - [ALE](https://github.com/dense-analysis/ale) 355 356 Here's my [whole LSP config](https://github.com/elianiva/dotfiles/tree/master/nvim/.config/nvim/lua/modules/lsp) if you want them. If you've read this far then thank you and have a wonderful day :)