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.

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 Neovim9 version = "LuaJIT",10 -- Include Neovim runtime files11 path = vim.split(package.path, ";"),12 },13 diagnostics = {14 -- Recognize `vim` as a global15 globals = { "vim" },16 },17 workspace = {18 -- Make the server aware of Neovim runtime files19 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")1314-- Enable the configured LSP servers15vim.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") then5 vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })6 end7 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 completion2function _G.tab_complete()3 if vim.fn.pumvisible() == 1 then4 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 then6 return vim.api.nvim_replace_termcodes('<C-r>=emmet#expandAbbreviation("")<CR>', true, true, true)7 else8 return vim.api.nvim_replace_termcodes("<Tab>", true, true, true)9 end10end1112-- enable built-in auto-completion and set up lsp features on attach13vim.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.buf1718 if not client then19 return20 end2122 if client:supports_method("textDocument/completion") and vim.lsp.completion then23 vim.lsp.completion.enable(true, client.id, args.buf, { autotrigger = true })24 end2526 if client:supports_method("workspace/symbol") then27 vim.keymap.set("n", "grs", function()28 vim.lsp.buf.workspace_symbol()29 end, { buffer = args.buf, desc = "Select LSP workspace symbol" })30 end3132 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 )3940 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 lsp2function _G.tab_complete()3 if vim.fn.pumvisible() == 1 then4 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 then6 return vim.api.nvim_replace_termcodes('<C-r>=emmet#expandAbbreviation("")<CR>', true, true, true)7 else8 return vim.api.nvim_replace_termcodes("<CR>", true, true, true)9 end10end1112vim.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") then16 vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })17 end18 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 | vertical23 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 },1516 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 lsp21 snippetSupport = true, -- set to false if you don't use blink22 },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})89-- Toggle virtual_text off when on the line with the error10vim.api.nvim_create_autocmd("CursorMoved", {11 callback = function()12 local current_line = vim.api.nvim_win_get_cursor(0)[1] - 113 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 error2vim.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})1718-- Special config for lazy buffer19vim.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})3031-- Toggle virtual text and show diagnostics on cursor hold32vim.api.nvim_create_autocmd("CursorHold", {33 callback = function()34 if vim.bo.filetype == "lazy" then35 return36 end3738 local current_line = vim.api.nvim_win_get_cursor(0)[1] - 139 local diagnostics = vim.diagnostic.get(0, { lnum = current_line })4041 vim.diagnostic.config({42 virtual_text = vim.tbl_isempty(diagnostics) and { spacing = 4, prefix = "●" } or false,43 })4445 -- Only show float when there are diagnostics46 if not vim.tbl_isempty(diagnostics) then47 vim.diagnostic.open_float(nil, { focusable = false })48 end49 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