/ src / content / posts / my-nvim-lsp-setup.mdx
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 :)