Skip to content

Commit

Permalink
nvim lsp
Browse files Browse the repository at this point in the history
  • Loading branch information
gierdo committed Sep 19, 2024
1 parent 94db5c9 commit 66a40c3
Showing 1 changed file with 356 additions and 0 deletions.
356 changes: 356 additions & 0 deletions _posts/2024-09-19-nvim-lsp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
---
title: neovim config - native lsp support instead of coc
categories:
- programming
- linux
tags:
- vim
---

# Migrating language server, linting and formatting away from [coc](https://github.com/neoclide/coc.nvim)

## Context

I am a big [vim (neovim)](https://neovim.io) fan and do basically all my
programming and writing in it, the exception being work things that need MS
Office. Luckily, most of those things are possible with the browser-based
version of office, the exception being macros and obscure things like
Powerpoint footer changes. Anyways, that is a completely different story.

Naked, vanilla vim is nice and present in basically every environment. So, it's
good to be able to use it, especially for remote
configuration/documentation/troubleshooting work. The real power is unleashed
with more advanced capabilities and extensions, which extend the barebone
editor with modern creature comforts, such as _linting_, _automatic fixing_,
_code completion_, _auto formatting_, _syntax highlighting_, _test execution_,
_debugging_ and _intelligent navigation_.

My _code completion_ and _intelligent navigation_ setup started with
[YouCompleteMe](https://github.com/ycm-core/YouCompleteMe) in 2017. I needed it
mainly for `C`, and it did a good job.

A bit later, Microsoft's _Language Server Protocol_ gained much more traction,
with more and more really usable [_language
servers_](https://microsoft.github.io/language-server-protocol/implementors/servers/)
being really usable and taking off, mainly n the context of _visual studio
code_. Most of the Language servers where installed and integrated into
_vscode_ through dedicated _plugins_.

In comes the neovim plugin [coc (conquer of
completion)](https://github.com/neoclide/coc.nvim). This plugin acts as a
_language server client_, meaning that it can use any _language server_ that
implements the lsp standard, and it can integrate slightly adapted _vscode
pluins_ as _coc plugins_. This meant that the managed installation of language
servers and the integration thereof became really simple and straightforward,
with a very active ecosystem and community around it. I migrated to _coc_ in
2019, adding support and better integration for many more languages into my
setup.

_coc_ with a set of plugins and a handful of "manually" integrated language
servers as well as with a set of linters and formatters, integrated with
[diagnostic-languageserver](https://github.com/iamcco/diagnostic-languageserver),
was my default setup for 5 years. I changed the supported languaged all the
time, based on what I needed, but besides that and [rewriting the config to
lua]({% link _posts/2023-12-01-nvim-config-lua.md %}), my setup stayed like
this for 5 years.

In the meantime, _neovim_ gained native _language server client_ support,
removing the need for a dedicated plugin for lsp support. However, _coc_ still
provided the easy integration and awesome out of the box functionality.

The awesome and very capable community embraced the feature, though. There is
[mason](https://github.com/williamboman/mason.nvim), an nvim native package/lsp
manager, and many integration layer plugins between neovim and mason, making
the integration of language servers slimmer (no extra layer), extremely
flexible and manageable, with concise and powerful configuration.

Powered by all of that, projects like [lazyvim](https://www.lazyvim.org/)
started, providing much more out of the box functionality and ease of use than
coc had. Lazyvim is basically a full-featured one size fits all
preconfiguration of nvim, with very sane defaults and a smart plugin and
configuration system that makes it feature rich without bloat. I know a few
people who recently went down the rabbithole of vim, and they started with
lazyvim right away. I respect it a lot, even though I want to have a bit more
control over my setup, so not for me. The ecosystem grew and grew and is now
more active and powerful than coc's ecosystem.

I was very interested in the principle of the powerful and manageable
modularization, but still, the normal out of the box experience was a lot worse
than with coc, as it was basically not a box, but a collection of elements you
had to build your box with yourself. The capabilities of coc are split into
separate and independent components for completion (with lsp as source amongst
others), dedicated components for formatters, linters, dedicated configuration
of all components and system integration.

Interesting, but it's a lot. Especially because my current setup just worked
and did mostly what I wanted it to do, and I had no real functioning reason to
migrate besides the itch. So, I put a migration on the back of my to do list,
to do some other time.

## Some other time is _now_

That is until for the first time ever I had to do things in `C#`. There is a
coc plugin for `C#`,
[coc-omnisharp](https://github.com/coc-extensions/coc-omnisharp), which in the
README greets you with the message:

```text
⛔ This project is lacking proper maintanence. I would recommend csharp-ls at this moment.
```

This leaves manual integration of the
[csharp-language-server](https://github.com/razzmatazz/csharp-language-server).
Awesome, I did it right away.

But there was a problem.

With the coc integration, I was unable to find a way to replace the default
sync handler for system libraries only available as binaries, used e.g. for
navigation, such as _go_to_definition_, with a buffer showing the decompiled
sourcecode. This is explicitly (and easily) possible with the flexible
configuration of native language servers in nvim, as
[documented](https://github.com/Decodetalkers/csharpls-extended-lsp.nvim).

So, last weekend, I did it.

I migrated my configuration.

### Structure

I use [lazy](https://github.com/folke/lazy.nvim) as plugin manager, which makes
it really simple to split up plugin configuration. I decided to split my
language server config into a dedicated file for the abstract definition of the
behaviour of lsps, e.g. with key mappings, sources of the completion system and
so on, a dedicated file for the specific definition of supported languages, and
a dedicated file for the separate configuration of linters and fixers, while
also revamping the test execution and debugging setup.

```text
/home/gierdo/.dotfiles/.config/nvim/lua/plugins
❯ tree
.
├── debugging.lua
├── filetree.lua
├── fuzzyness.lua
├── git.lua
├── init.lua
├── linters.lua
├── lsp-behaviour.lua
├── lsp-languages.lua
├── neoai.lua
├── testing.lua
├── theme.lua
└── treesitter.lua
```

Copy-pasting the few hundred lines of lua here probably makes this post even
less readable, but I'll point out some things here that are not just obvious
configuration.

If you want to see the full configuration, check out my
[dotfiles.](https://github.com/gierdo/dotfiles/tree/master/.config/nvim/lua/plugins)

#### The original motivation: C# decompiler integration

```lua
local cs_config = {
capabilities = lsp_capabilities,
handlers = {
["textDocument/definition"] = require("csharpls_extended").handler,
["textDocument/typeDefinition"] = require("csharpls_extended").handler,
},
}
lspconfig.csharp_ls.setup(cs_config)
```

#### Fuzzy and fancy selection for multiple navigation target hits

```lua
vim.keymap.set(
"n",
"gd",
"<cmd>lua require('telescope.builtin').lsp_definitions()<cr>",
{ buffer = event.buf, desc = "Go to definition." }
)
vim.keymap.set(
"n",
"gi",
"<cmd>lua require('telescope.builtin').lsp_implementations()<cr>",
{ buffer = event.buf, desc = "Go to implementation." }
)
vim.keymap.set(
"n",
"go",
"<cmd>lua require('telescope.builtin').lsp_type_definitions()<cr>",
{ buffer = event.buf, desc = "Go to type definition." }
)
vim.keymap.set(
"n",
"gr",
"<cmd>lua require('telescope.builtin').lsp_references()<cr>",
{ buffer = event.buf, desc = "Show references." }
)
```

#### Extended json- and yaml schema support

```lua
lspconfig.jsonls.setup({
capabilities = lsp_capabilities,
settings = {
json = {
schemas = require("schemastore").json.schemas(),
validate = { enable = true },
schemaDownload = { enable = false },
},
},
})

lspconfig.yamlls.setup({
capabilities = lsp_capabilities,
settings = {
yaml = {
schemaStore = {
enable = false,
url = "",
},
schemas = require("schemastore").yaml.schemas({
extra = {
{
name = "Cloudformation",
description = "Cloudformation Template",
fileMatch = { "*.template.y*ml", "*-template.y*ml" },
url = "https://raw.githubusercontent.com/awslabs/goformation/master/schema/cloudformation.schema.json",
},
},
}),
customTags = {
-- Cloudformation tags
"!And scalar",
"!If scalar",
"!Not",
"!Equals scalar",
"!Or scalar",
"!FindInMap scalar",
"!Base64",
"!Cidr",
"!Ref",
"!Sub",
"!GetAtt sequence",
"!GetAZs",
"!ImportValue sequence",
"!Select sequence",
"!Split sequence",
"!Join sequence",
},
},
},
})
```

#### imho smart test execution and debugging keymaps

```lua
local wk = require("which-key")

wk.add({
{ "<leader>t", group = "  Test" },
{ "<leader>tr", neotest.run.run, desc = "Run nearest test." },
{
"<leader>tf",
function()
neotest.run.run(vim.fn.expand("%"))
end,
desc = "Run current file.",
},
{
"<leader>td",
function()
neotest.run.run({ strategy = "dap" })
end,
desc = "Debug nearest test.",
},
{
"<leader>ts",
neotest.summary.toggle,
desc = "Show/hide summary.",
},
})

wk.add({
{ "<leader>d", group = "  Debug" },
{
"<leader>dd",
telescope.extensions.dap.commands,
desc = "Show debug commands.",
},
{
"<leader>dt",
dap.toggle_breakpoint,
desc = "Toggle line breakpoint on the current line.",
},
{
"<leader>dbs",
dap.set_breakpoint,
desc = "Set breakpoint.",
},
{
"<leader>dbl",
telescope.extensions.dap.list_breakpoints,
desc = "List breakpoints.",
},
{
"<leader>dv",
telescope.extensions.dap.variables,
desc = "Show variables.",
},
{
"<leader>dbc",
dap.clear_breakpoints,
desc = "Clear breakpoints.",
},
{
"<leader>dbe",
dap.set_exception_breakpoints,
desc = "Set Exception breakpoints.",
},
{
"<leader>dc",
dap.continue,
desc = "When debugging, continue. Otherwise start debugging.",
},
{
"<leader>dfd",
dap.down,
desc = "Move down a frame in the current call stack.",
},
{
"<leader>dfu",
dap.up,
desc = "Move up a frame in the current call stack.",
},
{ "<leader>dp", dap.pause, desc = "Pause debuggee." },
{
"<leader>dr",
dap.run_to_cursor,
desc = "Continues execution to the current cursor.",
},
{
"<leader>dre",
dap.restart,
desc = "Restart debugging with the same configuration.",
},
})
```

## tl;dr

- language servers and such + vim are great
- there are many ways to integrate vim with language servers
- [coc](https://github.com/neoclide/coc.nvim) is not that slim and flexible
with good out of the box experience
- native nvim lsp support is slim and flexible with worse out of the box
experience
- with existing plugins and thanks to the community, benefiting from the
advantages of native nvim lsp support is possible in a sane way

0 comments on commit 66a40c3

Please sign in to comment.