Two options for upgrading Nvim from v0.10 to v0.11


When upgrading from Neovim v0.10 to v0.11, I explored two different configurations--one using blink and one without it--each with its own trade-offs. I’m currently using the blink setup because I encountered some issues with SCSS snippets in the native configuration. That said, the native approach has its perks too—for example, when typing console.l, "log" shows up as the first suggestion, while with blink, emmet shifts it to second place. There are a few annoyances like this that make me prefer the native setup. And I imagine not being able to get SCSS snippets working correctly is likely a skill issue.

lsp-config nvim v0.11

Nvim v0.11 LSP Configuration

LSP Configuration

Let’s start with how I set up language servers. I like to keep things modular, so I split my LSP configs into separate files and import them as needed. For instance, I import my Lua LSP config with require("config.lsp.lua_ls"). In my case, the entire lua_ls file is --


1vim.lsp.config.lua_ls = {
2 cmd = { "lua-language-server" },
3 filetypes = { "lua" },
4 root_markers = { ".luarc.json", ".luarc.jsonc", ".git" },
5 settings = {
6 Lua = {
7 runtime = {
8 -- Specify LuaJIT for Neovim
9 version = "LuaJIT",
10 -- Include Neovim runtime files
11 path = vim.split(package.path, ";"),
12 },
13 diagnostics = {
14 -- Recognize `vim` as a global
15 globals = { "vim" },
16 },
17 workspace = {
18 -- Make the server aware of Neovim runtime files
19 library = vim.api.nvim_get_runtime_file("", true),
20 checkThirdParty = false,
21 },
22 hint = {
23 enable = true,
24 arrayIndex = "Enable",
25 await = true,
26 paramName = "All",
27 paramType = true,
28 semicolon = "Disable",
29 setType = true,
30 },
31 telemetry = {
32 enable = false,
33 },
34 },
35 },
36}
37```

I enable it, with vim.lsp.enable({"lua_ls", })note I have other lsps configured --


1require("config.lsp.tailwindcss_ls")
2require("config.lsp.tsserver_ls")
3require("config.lsp.css-variables_ls")
4require("config.lsp.css_ls")
5require("config.lsp.eslint_ls")
6require("config.lsp.html_ls")
7require("config.lsp.emmet_ls")
8require("config.lsp.lua_ls")
9require("config.lsp.gop_ls")
10require("config.lsp.docker_ls")
11require("config.lsp.biome_ls")
12require("config.lsp.stylelint_ls")
13
14-- Enable the configured LSP servers
15vim.lsp.enable({
16 "tsserver_ls",
17 "eslint_ls",
18 "tailwindcss_ls",
19 "css-variables_ls",
20 "css_ls",
21 "html_ls",
22 "emmet_ls",
23 "lua_ls",
24 "stylelint_ls",
25 "gop_ls",
26 "docker_ls",
27 "biome_ls",
28})

I then attach (skip this, if you plan to use blink) --


1vim.api.nvim_create_autocmd("LspAttach", {
2 callback = function(ev)
3 local client = vim.lsp.get_client_by_id(ev.data.client_id)
4 if client and client:supports_method("textDocument/completion") then
5 vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
6 end
7 end,
8})

I actually have two more complicated functions for this, depending on whether you use tab or return, you may want to try them. The first --


1-- use emmet with lsp and tab completion
2function _G.tab_complete()
3 if vim.fn.pumvisible() == 1 then
4 return vim.api.nvim_replace_termcodes("<C-y>", true, true, true)
5 elseif vim.fn.exists("*emmet#is_expandable") == 1 and vim.fn["emmet#is_expandable"]() == 1 then
6 return vim.api.nvim_replace_termcodes('<C-r>=emmet#expandAbbreviation("")<CR>', true, true, true)
7 else
8 return vim.api.nvim_replace_termcodes("<Tab>", true, true, true)
9 end
10end
11
12-- enable built-in auto-completion and set up lsp features on attach
13vim.api.nvim_create_autocmd("LspAttach", {
14 callback = function(args)
15 local client = vim.lsp.get_client_by_id(args.data.client_id)
16 local buffer = args.buf
17
18 if not client then
19 return
20 end
21
22 if client:supports_method("textDocument/completion") and vim.lsp.completion then
23 vim.lsp.completion.enable(true, client.id, args.buf, { autotrigger = true })
24 end
25
26 if client:supports_method("workspace/symbol") then
27 vim.keymap.set("n", "grs", function()
28 vim.lsp.buf.workspace_symbol()
29 end, { buffer = args.buf, desc = "Select LSP workspace symbol" })
30 end
31
32 vim.api.nvim_buf_set_keymap(
33 buffer,
34 "i",
35 "<Tab>",
36 [[v:lua.tab_complete()]],
37 { noremap = true, expr = true, silent = true }
38 )
39
40 vim.api.nvim_buf_set_keymap(
41 buffer,
42 "i",
43 "<CR>",
44 [[pumvisible() ? "<C-e><CR>" : "<CR>"]],
45 { noremap = true, expr = true, silent = true }
46 )
47 end,
48})

The second --


1-- minimal setup for lsp
2function _G.tab_complete()
3 if vim.fn.pumvisible() == 1 then
4 return vim.api.nvim_replace_termcodes("<C-y>", true, true, true)
5 elseif vim.fn.exists("*emmet#is_expandable") == 1 and vim.fn["emmet#is_expandable"]() == 1 then
6 return vim.api.nvim_replace_termcodes('<C-r>=emmet#expandAbbreviation("")<CR>', true, true, true)
7 else
8 return vim.api.nvim_replace_termcodes("<CR>", true, true, true)
9 end
10end
11
12vim.api.nvim_create_autocmd("LspAttach", {
13 callback = function(ev)
14 local client = vim.lsp.get_client_by_id(ev.data.client_id)
15 if client and client:supports_method("textDocument/completion") then
16 vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
17 end
18 end,
19})

Regardless of whether blink is used, I disable lspconfig--it's no longer needed.


1return {
2 {
3 "nvim-lspconfig",
4 enabled = false,
5 opts = {
6 inlay_hints = { enabled = false },
7 diagnostics = {
8 virtual_text = false,
9 signs = false,
10 },
11 },
12 },
13}
14

And I set copilot-lua, so auto_trigger is false --


1return {
2 "zbirenbaum/copilot.lua",
3 opts = {
4 server_opts_overrides = {
5 settings = {
6 telemetry = {
7 telemetryLevel = "off",
8 },
9 },
10 },
11 panel = {
12 enabled = true,
13 auoo_refresh = false,
14 keymap = {
15 jump_prev = "[[",
16 jump_next = "]]",
17 accept = "<CR>",
18 refresh = "<M-r>",
19 open = "<M-CR>",
20 },
21 layout = {
22 position = "bottom", -- | top | left | right | horizontal | vertical
23 ratio = 0.4,
24 },
25 },
26 suggestion = {
27 enabled = true,
28 auto_trigger = false,
29 hide_during_completion = false,
30 keymap = {
31 accept = "<C-space>",
32 },
33 },
34 filetypes = {
35 markdown = true,
36 help = true,
37 },
38 },
39}

Additionally, I set global options for native auto complete vim.opt.completeopt = { "menuone", "fuzzy", "noinsert", "preview" } and vim.o.omnifunc = "v:lua.vim.lsp.omnifunc".How this works is simple... I can type, hit return and it selects the suggestion i.e. I don't need to hit the down arrow and return. If I want copilot ghost writing, I press ctrl + space, and this enables copilot. "<C-x><C-i>" and "<C-x><C-o>" in insert mode also pop up with info. Shift+k over something provides documentation (I love this!).


You can remove the "LspAttach" and enable blink. From there, you're good to go once your lsp servers are set up. However, the "<C-x><C-i>" and "<C-x><C-o>" commands may overlap with blinks suggestions; you can circumvent this by pressing ctrl + e.


I don't notice a performance difference; however, my workflow is a bit different with the new nvim 0.11. I love that I can hover something and get info, and bring up suggestions without pre-typing anything. Having control over the lsp's is nice, because you can enable and disable extras. But I prefer blink in that it shows documentation for each suggestion... I'm not sure if I really need documentation, what the performance cost for that is and whether it's possible to enable (it probably is somehow) natively.


The omnifunc setting is really cool, for example you can set `vim.o.omnifunc = "csscomplete#CompleteCSS"` and get CSS variables. This is more specific in what it provides you, so it has its uses. If you're unsure of the different options, you could type `flex:` press the command to bring up omnifunc, and then it will show you a list. Effectively, you have the alphanumerically sorted auto correct which is seemingly not as filtered as blink (I prefer blink), and then omnifunc which is more specific and content aware (this is better than blink but for highly specific use cases, in my opinion).


If you don't edit scss files (perhaps I simply made a mistake with my config?), I encourage you to use native lsps with the native attach method.


This is my lsp config with blink enabled --


1vim.lsp.config.css_ls = {
2 cmd = { "vscode-css-language-server", "--stdio" },
3 filetypes = { "css", "scss", "less" },
4 root_markers = { "package.json", ".git" },
5 init_options = { provideFormatter = true },
6 single_file_support = true,
7 settings = {
8 cssVariables = {
9 lookupFiles = { "**/*.less", "**/*.scss", "**/*.sass", "**/*.css" },
10 },
11 css = { validate = true },
12 scss = { validate = true },
13 less = { validate = true },
14 },
15
16 capabilities = vim.tbl_deep_extend("force", vim.lsp.protocol.make_client_capabilities(), {
17 textDocument = {
18 completion = {
19 completionItem = {
20 -- this causes an error using native lsp
21 snippetSupport = true, -- set to false if you don't use blink
22 },
23 },
24 },
25 }),
26}
27

This is my typescript lsp --


1vim.lsp.config.tsserver_ls = {
2 cmd = { "typescript-language-server", "--stdio" },
3 filetypes = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
4 root_markers = { "package.json", "tsconfig.json", "jsconfig.json", ".git" },
5 settings = {
6 typescript = {
7 inlayHints = {
8 includeInlayParameterNameHints = "all",
9 includeInlayParameterNameHintsWhenArgumentMatchesName = false,
10 includeInlayFunctionParameterTypeHints = true,
11 includeInlayVariableTypeHints = true,
12 includeInlayPropertyDeclarationTypeHints = true,
13 includeInlayFunctionLikeReturnTypeHints = true,
14 includeInlayEnumMemberValueHints = true,
15 },
16 },
17 javascript = {
18 inlayHints = {
19 includeInlayParameterNameHints = "all",
20 includeInlayParameterNameHintsWhenArgumentMatchesName = false,
21 includeInlayFunctionParameterTypeHints = true,
22 includeInlayVariableTypeHints = true,
23 includeInlayPropertyDeclarationTypeHints = true,
24 includeInlayFunctionLikeReturnTypeHints = true,
25 includeInlayEnumMemberValueHints = true,
26 },
27 },
28 },
29}

If you'd like more, please join the discord (link in the comments section).


Conform Configuration

My current conform configuration is below --


1return {
2 "stevearc/conform.nvim",
3 opts = {
4 formatters_by_ft = {
5 cpp = { "clang-format" },
6 css = { "prettier" },
7 scss = { "prettier" },
8 go = { "gofumpt", "goimports" },
9 html = { "prettier" },
10 javascript = { "prettier" },
11 javascriptreact = { "prettier" },
12 json = { "prettier" },
13 lua = { "stylua" },
14 markdown = { "prettier" },
15 sql = { "sqlfmt" },
16 typescript = { "biome" },
17 typescriptreact = { "biome" },
18 },
19 },
20}

Note that I keep mason installed, because it is used to set up the language server projects (for want of a better phrase) that run in the background; mason itself is not a language server provider, and it doesn't clash with nvim v0.11 to have it installed.


Diagnostics Configuration

I'm a bit torn between diagnostics shown on the current line, and more descriptive diagnostics, where text jumps around. So I made a function to give me both. When in normal mode, not on a specific line, you will see errors much like with nvim v0.10; however, when you're on the line with an error, it will change from virtual_text to current_line which is really nice! The code for that is below, I kept it in my main LSP config file.


1vim.diagnostic.config({
2 virtual_text = { spacing = 4, prefix = "●" },
3 virtual_lines = { current_line = true },
4 underline = true,
5 update_in_insert = false,
6 severity_sort = true,
7})
8
9-- Toggle virtual_text off when on the line with the error
10vim.api.nvim_create_autocmd("CursorMoved", {
11 callback = function()
12 local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1
13 local diagnostics = vim.diagnostic.get(0, { lnum = current_line })
14 vim.diagnostic.config({
15 virtual_text = vim.tbl_isempty(diagnostics) and { spacing = 0, prefix = "●" } or false,
16 })
17 end,
18})

Alternatively...

1-- Toggle virtual_text off when on the line with the error
2vim.diagnostic.config({
3 signs = true,
4 underline = true,
5 virtual_text = { spacing = 4, prefix = "●" },
6 virtual_lines = false,
7 update_in_insert = false,
8 severity_sort = true,
9 float = {
10 border = "single",
11 spacing = 4,
12 source = "always",
13 header = " Diagnostics:",
14 prefix = " ● ",
15 },
16})
17
18-- Special config for lazy buffer
19vim.api.nvim_create_autocmd("FileType", {
20 pattern = "lazy",
21 callback = function()
22 vim.diagnostic.config({
23 signs = false,
24 virtual_text = true,
25 virtual_lines = false,
26 underline = false,
27 })
28 end,
29})
30
31-- Toggle virtual text and show diagnostics on cursor hold
32vim.api.nvim_create_autocmd("CursorHold", {
33 callback = function()
34 if vim.bo.filetype == "lazy" then
35 return
36 end
37
38 local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1
39 local diagnostics = vim.diagnostic.get(0, { lnum = current_line })
40
41 vim.diagnostic.config({
42 virtual_text = vim.tbl_isempty(diagnostics) and { spacing = 4, prefix = "●" } or false,
43 })
44
45 -- Only show float when there are diagnostics
46 if not vim.tbl_isempty(diagnostics) then
47 vim.diagnostic.open_float(nil, { focusable = false })
48 end
49 end,
50})

This creates a float at the location, when you are on the line with the error.

If you have any questions, feel free to reach out!

Comments

Leave a comment
You must be a discord member to comment. Please register
No comments yet. Be the first to leave a comment!