Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Administering with Neovim

By updated

Improve this page

neovim logo

You master Vim but you are frustrated by its archaic configuration language, the lack of native LSP and the slowness of some plugins? Neovim keeps everything that makes Vim strong, modes, motions, text objects, and adds a modern Lua API, a built-in LSP client, Tree-sitter for accurate syntax highlighting, and an asynchronous terminal. This guide takes you from installation to a productive environment with autocompletion, diagnostics, fuzzy search and a file explorer. The whole configuration shown here loaded without error on Neovim 0.12.5, the current stable release, in a Debian 13 container.

What you will learn

  • Install Neovim on your Linux distribution
  • Understand the layout of the Lua configuration (init.lua)
  • Set the essential options and shortcuts
  • Install plugins with the lazy.nvim manager
  • Turn on LSP for real-time autocompletion and diagnostics
  • Use Tree-sitter for smart syntax highlighting
  • Search files and text with Telescope
  • Browse files with a built-in explorer

What Neovim brings compared with Vim

Neovim is a fork of Vim started in 2014 with a clear goal: modernise Vim's source code without breaking compatibility. In practice, if you know Vim, you already know Neovim: the modes, the shortcuts and the philosophy are the same. What changes is everything that happens under the hood and in the plugin ecosystem.

FeatureVimNeovim
Configuration languageVimScript onlyNative Lua (faster, more readable)
LSP clientThrough a plugin (coc.nvim, vim-lsp)Built in (vim.lsp), no plugin needed
Tree-sitterNoBuilt in, AST-based syntax highlighting
Built-in terminalBasicAsynchronous with a full API
External APILimitedRPC API, drive Neovim from an external program
Asynchronous executionLimited (Vim 8+)Native, plugins do not block the editor
DefaultsMinimalistModern (syntax on, filetype on, incsearch, mouse on)
ClipboardRequires +clipboard at build timeUses an external provider (xclip, wl-copy)
Vim compatibilityNot applicableMost Vim plugins work in Neovim
System healthNo equivalent:checkhealth diagnoses the configuration

In short: Vim is a powerful editor with 30 years of heritage. Neovim keeps that power and adds a modern architecture that makes plugins faster, the configuration more readable and IDE features available natively.

Install Neovim

This guide's configuration targets Neovim 0.12: the Tree-sitter plugin it uses requires at least that version. Yet distribution packages lag behind, measured on 1 October 2026: 0.9.5 on Ubuntu 24.04, 0.10.4 on Debian 13, 0.11.6 on Fedora 43. The neovim-ppa/stable PPA, long recommended, only publishes 0.7.2, and only for Ubuntu 20.04 and 22.04. The official archive is therefore the recommended method on every distribution.

The project publishes one archive per version, and GitHub shows its SHA-256 checksum on the release page. Check it before any extraction: the sha256sum --check command must answer OK.

Terminal window
NVIM_VERSION=v0.12.5
curl -sSLO "https://github.com/neovim/neovim/releases/download/${NVIM_VERSION}/nvim-linux-x86_64.tar.gz"
echo "bce0f56eda1f1b1db6eee8f4133d7a38813ea07933837dd1777411ca384c6875 nvim-linux-x86_64.tar.gz" \
| sha256sum --check
sudo tar -xzf nvim-linux-x86_64.tar.gz -C /opt
sudo ln -sf /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim

The checksum changes with every version: read the new one on the release page before bumping NVIM_VERSION. The old name nvim.appimage has not existed since 0.10.4, hence the 404 errors in the tutorials that still use it; the AppImage is now called nvim-linux-x86_64.appimage and needs FUSE, which is rarely present on a server.

Check: confirm the installation by printing the version:

Terminal window
nvim --version | head -2

With the archive above, the output is:

NVIM v0.12.5
Build type: Release

Install a Nerd Font

Several plugins (Neo-tree, lualine, which-key) use Unicode icons to show file types, modes and shortcuts. These icons need a Nerd Font, a monospace font enriched with extra glyphs.

Terminal window
# Download and install FiraCode Nerd Font (pinned version, checksum verified)
mkdir -p ~/.local/share/fonts
cd ~/.local/share/fonts
curl -fLO https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/FiraCode.zip
echo "239395baf60c89b2eaf4862b6b09db0ef95605cd3e8eef51c00345822a81a665 FiraCode.zip" \
| sha256sum --check
unzip -o FiraCode.zip -d FiraCodeNerdFont
rm FiraCode.zip
fc-cache -fv

Then set your terminal emulator to use the FiraCode Nerd Font Mono font (or another Nerd Font of your choice).

Configuration layout

Neovim's configuration lives in ~/.config/nvim/. It is the first change compared with Vim: instead of a single ~/.vimrc, Neovim uses a structured folder that lets you organise the configuration into Lua modules, one file per topic, easy to version.

~/.config/nvim/
├── init.lua # Entry point (equivalent of .vimrc)
├── lua/
│ ├── options.lua # Editor options
│ ├── keymaps.lua # Custom shortcuts
│ └── plugins/ # Configuration of each plugin
│ ├── colorscheme.lua
│ ├── cmp.lua
│ ├── gitsigns.lua
│ ├── lsp.lua
│ ├── lualine.lua
│ ├── misc.lua
│ ├── neo-tree.lua
│ ├── telescope.lua
│ └── treesitter.lua

Create this full tree with the following commands:

Terminal window
# Create the configuration folder and its subfolders
mkdir -p ~/.config/nvim/lua/plugins
# Create every configuration file
touch ~/.config/nvim/init.lua
touch ~/.config/nvim/lua/options.lua
touch ~/.config/nvim/lua/keymaps.lua
touch ~/.config/nvim/lua/plugins/{colorscheme,cmp,gitsigns,lsp,lualine,misc,neo-tree,telescope,treesitter}.lua

Check: the find command must list every file created:

Terminal window
find ~/.config/nvim -type f -name "*.lua" | sort
Expected output
/home/user/.config/nvim/init.lua
/home/user/.config/nvim/lua/keymaps.lua
/home/user/.config/nvim/lua/options.lua
/home/user/.config/nvim/lua/plugins/cmp.lua
/home/user/.config/nvim/lua/plugins/colorscheme.lua
/home/user/.config/nvim/lua/plugins/gitsigns.lua
/home/user/.config/nvim/lua/plugins/lsp.lua
/home/user/.config/nvim/lua/plugins/lualine.lua
/home/user/.config/nvim/lua/plugins/misc.lua
/home/user/.config/nvim/lua/plugins/neo-tree.lua
/home/user/.config/nvim/lua/plugins/telescope.lua
/home/user/.config/nvim/lua/plugins/treesitter.lua

The following sections of this guide detail the content of each file, in the logical order of configuration.

The init.lua file is the entry point: Neovim loads it automatically at startup. From this file, you can import (require) other Lua files to organise your configuration.

Set the basic options

Create the ~/.config/nvim/lua/options.lua file to group all the editor options. The Lua syntax is more readable than the equivalent VimScript.

-- ~/.config/nvim/lua/options.lua
-- === PATH (mise + Mason) ===
-- LSP servers installed through Mason and tools set up through mise
-- must be reachable in Neovim's PATH
vim.env.PATH = vim.fn.expand("~/.local/share/nvim/mason/bin") .. ":" .. vim.env.PATH
-- Add node/python from mise when present
local mise_shims = vim.fn.expand("~/.local/share/mise/shims")
if vim.fn.isdirectory(mise_shims) == 1 then
vim.env.PATH = mise_shims .. ":" .. vim.env.PATH
end
-- === Appearance ===
vim.opt.number = true -- Line numbers
vim.opt.relativenumber = true -- Relative numbers (handy for 5j, 12k)
vim.opt.cursorline = true -- Highlight the cursor line
vim.opt.signcolumn = "yes" -- Sign column always visible (Git, diagnostics)
vim.opt.termguicolors = true -- Turn on 24-bit colours (true colour)
vim.opt.showmode = false -- Hide the mode (the lualine plugin shows it)
vim.opt.colorcolumn = "80" -- Vertical line at 80 characters
-- === Indentation ===
vim.opt.tabstop = 4 -- A tab is 4 spaces wide on screen
vim.opt.shiftwidth = 4 -- Automatic indentation shifts by 4 spaces
vim.opt.expandtab = true -- Insert spaces instead of tabs
vim.opt.smartindent = true -- Smart indentation after {, if, etc.
-- === Search ===
vim.opt.ignorecase = true -- Case-insensitive search
vim.opt.smartcase = true -- UNLESS you type an uppercase letter
vim.opt.hlsearch = true -- Highlight every match
vim.opt.incsearch = true -- Real-time results
-- === Behaviour ===
vim.opt.mouse = "a" -- Mouse on everywhere
vim.opt.clipboard = "unnamedplus" -- Shared system clipboard
vim.opt.scrolloff = 8 -- Keep 8 lines above/below the cursor
vim.opt.undofile = true -- Persistent undo (history saved to disk)
vim.opt.swapfile = false -- No swap file (fewer messages at startup)
vim.opt.updatetime = 250 -- Shorter delay (snappier Git/diagnostics)
vim.opt.splitright = true -- Vertical splits open on the right
vim.opt.splitbelow = true -- Horizontal splits open below
-- === Wrapping ===
vim.opt.wrap = false -- No automatic line wrapping
vim.opt.linebreak = true -- If wrap is on, break at words (not mid-word)

Each vim.opt.X = value option matches VimScript's set X=value. The difference: in Lua, booleans are true/false (not set X / set noX), and strings use quotes, as in any VimScript translation.

Check: open Neovim and check that the options are applied:

" Check a boolean option (must print "number")
:set number?
" Check a numeric value (must print 4)
:lua print(vim.opt.tabstop:get())
" Show every option changed from its default
:set

Neovim also turns on by default some options that Vim leaves off: syntax on, filetype plugin indent on, incsearch, hlsearch, autoindent, and the mouse (mouse=nvi, that is normal, visual and insert modes). If you are moving from Vim, you can remove these lines from your configuration: they are already active.

Set up shortcuts

Create the ~/.config/nvim/lua/keymaps.lua file for your custom shortcuts. The Leader key is the base of every custom shortcut: it acts as a prefix to avoid clashing with native Vim commands.

-- ~/.config/nvim/lua/keymaps.lua
-- Set the Leader key (Space)
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- Helper shortcut
local map = vim.keymap.set
-- === Files ===
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })
map("n", "<leader>x", "<cmd>x<cr>", { desc = "Save and quit" })
-- === Moving between splits ===
map("n", "<C-h>", "<C-w>h", { desc = "Go to left split" })
map("n", "<C-j>", "<C-w>j", { desc = "Go to lower split" })
map("n", "<C-k>", "<C-w>k", { desc = "Go to upper split" })
map("n", "<C-l>", "<C-w>l", { desc = "Go to right split" })
-- === Resizing splits ===
map("n", "<C-Up>", "<cmd>resize +2<cr>", { desc = "Grow horizontal split" })
map("n", "<C-Down>", "<cmd>resize -2<cr>", { desc = "Shrink horizontal split" })
map("n", "<C-Left>", "<cmd>vertical resize -2<cr>", { desc = "Shrink vertical split" })
map("n", "<C-Right>", "<cmd>vertical resize +2<cr>", { desc = "Grow vertical split" })
-- === Buffers ===
map("n", "<S-l>", "<cmd>bnext<cr>", { desc = "Next buffer" })
map("n", "<S-h>", "<cmd>bprevious<cr>", { desc = "Previous buffer" })
map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Close the buffer" })
-- === Comfort ===
map("n", "<leader>h", "<cmd>nohlsearch<cr>", { desc = "Clear search highlighting" })
map("n", "<Esc>", "<cmd>nohlsearch<cr>", { desc = "Clear highlighting" })
map("v", "J", ":m '>+1<cr>gv=gv", { desc = "Move the selection down" })
map("v", "K", ":m '<-2<cr>gv=gv", { desc = "Move the selection up" })
map("n", "J", "mzJ`z", { desc = "Join lines without moving the cursor" })
-- === Terminal ===
map("n", "<leader>t", "<cmd>terminal<cr>", { desc = "Open a terminal" })
map("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Leave terminal mode" })

The vim.keymap.set(mode, key, action, options) syntax is the Lua equivalent of nnoremap / vnoremap in VimScript. The desc field matters: it lets plugins such as which-key show a description for each shortcut.

Check: make sure your shortcuts are registered:

" List every normal-mode shortcut starting with Leader
:nmap <leader>
" Look for a given shortcut
:verbose nmap <leader>w
" List ALL shortcuts (long, but exhaustive)
:map

The :verbose nmap command also shows where the shortcut was defined (file and line number), very useful to understand conflicts between plugins.

The init.lua file

The entry file imports the modules you have just created and prepares the ground for plugins:

-- ~/.config/nvim/init.lua
-- Load options and shortcuts
require("options")
require("keymaps")
-- The plugin manager will be loaded here (next section)

require("options") looks for the lua/options.lua file in the configuration folder. It is Lua's standard import mechanism.

Check: at this stage (before plugins), start Neovim to check that the base works:

Terminal window
# Start Neovim
nvim

You should see line numbers, the highlighted cursor line and the column at 80 characters. If an error appears, the message points to the faulty line: fix it before going on.

" Check Neovim's full health status
:checkhealth

:checkhealth analyses the whole installation and prints a detailed report. Each section shows OK, WARNING or ERROR. Focus on the ERROR lines; the WARNING lines are often optional features, such as a Python provider you will not use.

Install plugins with lazy.nvim

lazy.nvim is the most widespread plugin manager for Neovim. It replaces vim-plug and packer.nvim with a faster, more modern approach: lazy loading (plugins only load when you need them), a graphical interface, version locking and simpler updates. Neovim 0.12 also ships a native manager, vim.pack, which its own help still calls experimental: this guide sticks with lazy.nvim.

Install lazy.nvim

Add this block at the start of your init.lua, before the require lines:

-- ~/.config/nvim/init.lua
-- Bootstrap lazy.nvim (automatic installation on first launch)
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
-- Load options and shortcuts
require("options")
require("keymaps")
-- Load plugins
require("lazy").setup("plugins")

The last line, require("lazy").setup("plugins"), tells lazy.nvim to load every Lua file in the lua/plugins/ folder. Each file in that folder defines one or more plugins. The (vim.uv or vim.loop) expression follows lazy.nvim's official bootstrap: vim.uv is the current name of the libuv library, vim.loop its old alias.

Check: on first launch, lazy.nvim clones itself from GitHub automatically. You should see cloning messages in the terminal:

Terminal window
# Start Neovim
nvim

If the bootstrap works, the :Lazy command must open the manager's interface. If you see a module 'lazy' not found error, check that git is installed (git --version) and that the lazypath path is correct.

The lazy.nvim interface

Open the manager's interface with the command:

:Lazy

lazy.nvim interface showing the list of installed plugins and their status

From this interface, you can:

KeyAction
IInstall missing plugins
UUpdate every plugin
SSync (install + update + clean)
XRemove undeclared plugins
PShow load performance
qClose the interface

The essential plugins

Here are the plugins that turn Neovim into a complete working environment. Each one is configured in a separate file in lua/plugins/.

Gruvbox: a readable colour scheme

Create lua/plugins/colorscheme.lua:

-- ~/.config/nvim/lua/plugins/colorscheme.lua
return {
"ellisonleao/gruvbox.nvim",
priority = 1000, -- Load first (before the other plugins)
config = function()
require("gruvbox").setup({
contrast = "hard", -- "hard", "soft" or "" (default)
})
vim.cmd.colorscheme("gruvbox")
end,
}

Neovim with the Gruvbox scheme on a Python file, rich highlighting in warm tones

The Neovim version of the Gruvbox scheme uses 24-bit colours (true colour) for a more faithful rendering than the 256-colour Vim version.

Check: after a restart, the scheme must apply straight away. If the colours look wrong (no dark background, flat colours):

" Check that the scheme is active (must print "gruvbox")
:colorscheme
" Check that true colour is on (must print "termguicolors")
:set termguicolors?
" List every available scheme
:Telescope colorscheme

Lualine: a modern status line

Create lua/plugins/lualine.lua:

-- ~/.config/nvim/lua/plugins/lualine.lua
return {
"nvim-lualine/lualine.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("lualine").setup({
options = {
theme = "gruvbox",
section_separators = { left = "", right = "" },
component_separators = { left = "", right = "" },
},
sections = {
lualine_a = { "mode" },
lualine_b = { "branch", "diff", "diagnostics" },
lualine_c = { "filename" },
lualine_x = { "encoding", "fileformat", "filetype" },
lualine_y = { "progress" },
lualine_z = { "location" },
},
})
end,
}

Lualine replaces lightline (Vim) with extra features: the Git branch, a counter of LSP diagnostics and a diff indicator, all without manual configuration.

Telescope is the feature that, on its own, justifies the move to Neovim for many users. It is a universal finder: files, text, buffers, Git history, LSP diagnostics, everything is reachable by typing a few letters.

Create lua/plugins/telescope.lua:

-- ~/.config/nvim/lua/plugins/telescope.lua
return {
"nvim-telescope/telescope.nvim",
version = "*",
dependencies = {
"nvim-lua/plenary.nvim",
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
config = function()
local telescope = require("telescope")
telescope.setup({
defaults = {
path_display = { "truncate" },
sorting_strategy = "ascending",
layout_config = {
horizontal = { prompt_position = "top" },
},
},
})
telescope.load_extension("fzf")
-- Telescope shortcuts
local map = vim.keymap.set
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find a file" })
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>", { desc = "Search text" })
map("n", "<leader>fb", "<cmd>Telescope buffers<cr>", { desc = "List buffers" })
map("n", "<leader>fh", "<cmd>Telescope help_tags<cr>", { desc = "Search the help" })
map("n", "<leader>fo", "<cmd>Telescope oldfiles<cr>", { desc = "Recent files" })
map("n", "<leader>fd", "<cmd>Telescope diagnostics<cr>", { desc = "LSP diagnostics" })
end,
}

Telescope showing fuzzy file search in a project

The table lists the six shortcuts the configuration has just defined. They all start with Space f, for find, which makes them easy to remember and groups them in the which-key panel:

ShortcutWhat it does
Space ffFind a file by name (fuzzy)
Space fgSearch text across the whole project (live grep)
Space fbBrowse open buffers
Space fhSearch Neovim's help
Space foRecently opened files
Space fdList of LSP errors and warnings

Check: try each Telescope command to confirm everything works:

" Must open the file finder (needs fd)
:Telescope find_files
" Must open text search (needs ripgrep)
:Telescope live_grep
" If an error appears, check the dependencies in a shell:
" - fd --version (fdfind --version on Debian and Ubuntu)
" - rg --version
" If fzf-native fails, check that make is installed:
" - make --version

Tree-sitter: smart syntax highlighting

Tree-sitter is an incremental parser built into Neovim. Instead of colouring code with regular expressions (as Vim does), it builds a syntax tree (AST) of the file. The result: more accurate highlighting, smart code folding and a better understanding of the file's structure.

The nvim-treesitter plugin downloads then compiles one parser per language. Its main branch, now the default branch, requires three things that its README lists first: Neovim 0.12 at least, a C compiler, and the tree-sitter CLI version 0.26.1 or later. Debian 13 only ships 0.22.6: install the binary published by the project, with its checksum verified.

Terminal window
TS_VERSION=v0.27.0
curl -sSLO "https://github.com/tree-sitter/tree-sitter/releases/download/${TS_VERSION}/tree-sitter-linux-x64.gz"
echo "20a1f39ec1c45f2211492dcb8881c802b643b554bb196869a29ac3778277fa77 tree-sitter-linux-x64.gz" \
| sha256sum --check
gunzip tree-sitter-linux-x64.gz
sudo install -m 0755 tree-sitter-linux-x64 /usr/local/bin/tree-sitter
tree-sitter --version

The last command must answer tree-sitter 0.27.0. Without this CLI, every parser fails at compile time with the error ENOENT: no such file or directory (cmd): 'tree-sitter', measured in a Debian 13 container without it.

Create lua/plugins/treesitter.lua:

-- ~/.config/nvim/lua/plugins/treesitter.lua
-- Rewritten nvim-treesitter API (main branch, Neovim ≥ 0.12, tree-sitter CLI required)
return {
"nvim-treesitter/nvim-treesitter",
lazy = false,
build = ":TSUpdate",
config = function()
require("nvim-treesitter").setup({})
-- Install the parsers (asynchronous, no-op if already installed)
require("nvim-treesitter").install({
"bash", "c", "css", "dockerfile", "go", "hcl",
"html", "javascript", "json", "lua", "markdown",
"python", "rust", "terraform", "toml", "typescript",
"vim", "vimdoc", "yaml",
})
-- Turn on Tree-sitter highlighting for these filetypes
vim.api.nvim_create_autocmd("FileType", {
pattern = {
"bash", "sh", "c", "css", "dockerfile", "go", "hcl",
"html", "javascript", "json", "lua", "markdown",
"python", "rust", "terraform", "toml", "typescript",
"vim", "yaml",
},
callback = function()
pcall(vim.treesitter.start)
end,
})
-- Turn on Tree-sitter-based code folding
vim.api.nvim_create_autocmd("FileType", {
pattern = {
"bash", "sh", "c", "go", "javascript", "json", "lua",
"python", "rust", "typescript", "yaml",
},
callback = function()
vim.wo[0][0].foldmethod = "expr"
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.wo[0][0].foldenable = false -- Do not fold by default
end,
})
end,
}

The new nvim-treesitter API (main branch) works differently from the old one. Parsers are installed through require("nvim-treesitter").install({...}); the call is asynchronous and does nothing if the parser is already compiled. Syntax highlighting is turned on with vim.treesitter.start() in a FileType autocmd, which allows fine control per language.

Code folding uses vim.treesitter.foldexpr() instead of regular expressions. The foldenable = false option prevents every block from being folded on opening the file; you can fold by hand with za (toggle) or zM (fold everything).

Check: open a file in one of the configured languages and check that Tree-sitter highlighting is active:

" Open a Python, Lua, YAML file, etc.
:edit test.py
" Check that Tree-sitter is active for this buffer
" (must print the parser name, "python" here)
:lua print(vim.inspect(vim.treesitter.get_parser():lang()))
" List every installed parser
:lua print(vim.inspect(require('nvim-treesitter').get_installed()))
" If a parser is missing, install it
:lua require('nvim-treesitter').install({ 'terraform' })

LSP: real-time autocompletion and diagnostics

LSP (Language Server Protocol) is the feature that turns Neovim into a real IDE. A language server analyses your code in the background and provides autocompletion, error diagnostics, jumping to definitions, symbol renaming and much more.

The LSP configuration needs two pieces:

  1. mason.nvim, which installs and manages language servers
  2. nvim-cmp, which shows autocompletion in the editor

The nvim-lspconfig plugin is still useful alongside them: it provides each server's default configuration (launch command, project root detection, file types). These files are loaded automatically into the runtimepath, so there is no need to call require('lspconfig').

Since Neovim 0.11, LSP servers are configured through the native API vim.lsp.config and vim.lsp.enable instead of the old require('lspconfig').server.setup(). The same version added default LSP shortcuts, active without a single line of configuration:

Default shortcutAction
KHover documentation
grrSymbol references
griGo to implementation
grnRename the symbol
graCode actions
[d / ]dPrevious / next diagnostic
gcc / gcComment the line / the selection (since 0.10)

The configuration below therefore only defines what is missing. Redefining gr alone would make Neovim wait on every grr or grn, while it works out whether a second letter follows.

Create lua/plugins/lsp.lua:

-- ~/.config/nvim/lua/plugins/lsp.lua
-- vim.lsp.config / vim.lsp.enable API (Neovim ≥ 0.11)
return {
{
"mason-org/mason.nvim",
config = function()
require("mason").setup()
end,
},
{
"neovim/nvim-lspconfig",
dependencies = { "mason-org/mason.nvim" },
config = function()
-- Capabilities enriched by nvim-cmp (if installed)
local capabilities = vim.lsp.protocol.make_client_capabilities()
local ok, cmp_lsp = pcall(require, "cmp_nvim_lsp")
if ok then
capabilities = cmp_lsp.default_capabilities(capabilities)
end
-- LSP shortcuts on top of the defaults (K, grr, gri, grn, gra, [d, ]d)
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local map = function(keys, func, desc)
vim.keymap.set("n", keys, func, { buffer = ev.buf, desc = desc })
end
map("gd", vim.lsp.buf.definition, "Go to definition")
map("gD", vim.lsp.buf.declaration, "Go to declaration")
map("<leader>rn", vim.lsp.buf.rename, "Rename the symbol")
map("<leader>ca", vim.lsp.buf.code_action, "Code actions")
map("<leader>e", vim.diagnostic.open_float, "Diagnostic details")
end,
})
-- Default configuration (applied to every server)
vim.lsp.config("*", {
capabilities = capabilities,
})
-- Specific configuration for lua_ls (avoid Neovim warnings)
vim.lsp.config("lua_ls", {
settings = {
Lua = {
diagnostics = { globals = { "vim" } },
workspace = { library = vim.api.nvim_get_runtime_file("", true) },
},
},
})
-- Turn on every server installed through Mason
vim.lsp.enable({
"lua_ls", "pyright", "bashls", "yamlls",
"dockerls", "terraformls", "jsonls",
})
end,
},
}

To install the language servers, open the Mason interface with :Mason and install them by hand, or run the command below directly. The servers written in JavaScript (pyright, bashls, yamlls, dockerls, jsonls) need Node.js and npm on the machine; lua_ls and terraform-ls are standalone binaries.

:MasonInstall pyright lua-language-server bash-language-server yaml-language-server dockerfile-language-server terraform-ls json-lsp

The key changes compared with the old API:

  • vim.lsp.config("*", {...}) replaces passing capabilities to each server: the "*" configuration applies to all of them.
  • vim.lsp.config("lua_ls", {...}) replaces lspconfig.lua_ls.setup({...}) for specific configurations.
  • vim.lsp.enable({...}) replaces the for server in servers loop.
  • The LspAttach autocmd replaces the on_attach callback: it is the recommended approach since Neovim 0.11.
  • vim.diagnostic.goto_next() and goto_prev() are deprecated: :checkhealth vim.deprecated announces their removal in 0.13 and advises vim.diagnostic.jump(). The [d and ]d defaults already use it.

nvim-cmp: the autocompletion engine

Create lua/plugins/cmp.lua:

-- ~/.config/nvim/lua/plugins/cmp.lua
return {
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp", -- LSP source
"hrsh7th/cmp-buffer", -- Current buffer source
"hrsh7th/cmp-path", -- File path source
"L3MON4D3/LuaSnip", -- Snippet engine
"saadparwaiz1/cmp_luasnip", -- Snippet source for cmp
"rafamadriz/friendly-snippets", -- Snippet collection
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
-- Load the friendly-snippets snippets
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup({
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert({
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" }, -- LSP suggestions first
{ name = "luasnip" }, -- Snippets
{ name = "buffer" }, -- Words from the current buffer
{ name = "path" }, -- File paths
}),
})
end,
}

LSP autocompletion in Neovim with a drop-down menu and documentation

The most important LSP shortcuts to remember mix Neovim's defaults and those of the configuration above; the table groups them by use, from navigation to autocompletion:

ShortcutAction
gdGo to the definition of the function/variable under the cursor
grrShow references (every place that uses this symbol)
KHover documentation (signature, description)
Space rn (or grn)Rename the symbol across the project
Space ca (or gra)Code actions (quick fix, missing import)
Space eDiagnostic details (full error message)
[d / ]dMove between diagnostics (errors, warnings)
Tab / Shift+TabMove through the autocompletion menu
EnterAccept the suggestion
Ctrl+SpaceTrigger autocompletion by hand

Full LSP check: open a file of the language concerned and check that the server attaches. On Neovim 0.12, nvim-lspconfig's old :LspInfo and :LspLog commands no longer exist: the plugin only creates them when the native :lsp command is missing.

" Open a Lua file (or Python, YAML, etc.)
:edit test.lua
" Check the active LSP servers ("Active Clients" section)
:checkhealth vim.lsp
" Check that Mason installed the servers
:Mason
" Installed servers show a green tick
" List the clients attached to the current buffer
:lua print(vim.inspect(vim.tbl_map(function(c) return c.name end, vim.lsp.get_clients({ bufnr = 0 }))))
" If the server does not start, open the LSP log
:lua vim.cmd('tabnew ' .. vim.lsp.log.get_filename())
" Restart the buffer's servers after a fix
:lsp restart

Measured with lua_ls installed by Mason: :checkhealth vim.lsp lists lua_ls (id: 1) under Active Clients. If that section is empty, the language server is probably not installed through Mason (check with :Mason) or Neovim's PATH does not contain the binaries (check the PATH section in options.lua).

Neo-tree: file explorer

Create lua/plugins/neo-tree.lua:

-- ~/.config/nvim/lua/plugins/neo-tree.lua
return {
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
config = function()
require("neo-tree").setup({
filesystem = {
follow_current_file = { enabled = true },
filtered_items = {
hide_dotfiles = false,
hide_gitignored = false,
},
},
window = {
width = 35,
},
})
vim.keymap.set("n", "<leader>n", "<cmd>Neotree toggle<cr>",
{ desc = "Open/close the explorer" })
end,
}

Neo-tree showing the project tree with icons and Git indicators

Neo-tree replaces NERDTree (Vim) with a richer display: icons per file type, built-in Git indicators and smooth navigation. Press Space n to open or close it.

Check: press Space n; the explorer must open on the left with the tree of the current directory. If icons do not show, check that you are using a Nerd Font (see the installation section above).

Gitsigns: Git markers in the gutter

Create lua/plugins/gitsigns.lua:

-- ~/.config/nvim/lua/plugins/gitsigns.lua
return {
"lewis6991/gitsigns.nvim",
config = function()
require("gitsigns").setup({
signs = {
add = { text = "│" },
change = { text = "│" },
delete = { text = "_" },
topdelete = { text = "‾" },
changedelete = { text = "~" },
},
on_attach = function(bufnr)
local gs = package.loaded.gitsigns
local map = function(mode, l, r, desc)
vim.keymap.set(mode, l, r, { buffer = bufnr, desc = desc })
end
map("n", "]c", function() gs.nav_hunk("next") end, "Next Git hunk")
map("n", "[c", function() gs.nav_hunk("prev") end, "Previous Git hunk")
map("n", "<leader>gp", gs.preview_hunk, "Preview the change")
map("n", "<leader>gb", gs.blame_line, "Git blame of the line")
map("n", "<leader>gr", gs.reset_hunk, "Undo the change")
end,
})
end,
}

Git markers in Neovim's gutter with gitsigns

Both navigation shortcuts go through nav_hunk, which the gitsigns documentation names as the replacement for next_hunk and prev_hunk, both marked DEPRECATED. The markers are read in the gutter, left of the line numbers, as soon as the file is tracked by Git. The colour depends on the scheme; with Gruvbox, the colour code is as follows:

MarkerMeaning
Green │Added line
Blue │Changed line
Red _Deleted line

Create lua/plugins/misc.lua for utility plugins. Code commenting is no longer there: since 0.10, Neovim provides gcc (comment the line) and gc (comment a selection or a motion) natively, which makes Comment.nvim and vim-commentary unnecessary.

-- ~/.config/nvim/lua/plugins/misc.lua
return {
-- Automatic closing of brackets and quotes
{
"windwp/nvim-autopairs",
event = "InsertEnter",
config = function()
require("nvim-autopairs").setup()
end,
},
-- Surround text (like vim-surround, in Lua)
{
"kylechui/nvim-surround",
event = "VeryLazy",
config = function()
require("nvim-surround").setup()
end,
},
-- Visual indentation guides
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
config = function()
require("ibl").setup()
end,
},
-- Which-key: shows the available shortcuts
{
"folke/which-key.nvim",
event = "VeryLazy",
config = function()
require("which-key").setup()
end,
},
}

The which-key plugin deserves special attention: when you press Space (the Leader key) and wait a second, a panel appears with every available shortcut and its description. It is a built-in cheat sheet that makes the configuration explorable.

Which-key showing the Leader shortcuts available in Neovim

Final check: once every file is created, start Neovim and run these commands to validate the whole configuration:

" 1. Check that every plugin is installed (none in red)
:Lazy
" 2. Check the full health of the installation
:checkhealth
" 3. Check that the LSP servers are installed
:Mason
" 4. Check that Telescope works
:Telescope find_files
" 5. Check that Tree-sitter compiled the parsers
:lua print(#require('nvim-treesitter').get_installed() .. ' parsers installed')
" 6. Open a Lua file and check LSP
:edit test.lua
:checkhealth vim.lsp

If all these commands work without error, your configuration is operational.

Full configuration summary

Here are the final tree and the complete init.lua file, followed by a quick installation that chains every step of the guide on a Debian or Ubuntu machine:

~/.config/nvim/
├── init.lua
└── lua/
├── options.lua
├── keymaps.lua
└── plugins/
├── colorscheme.lua
├── lualine.lua
├── telescope.lua
├── treesitter.lua
├── lsp.lua
├── cmp.lua
├── neo-tree.lua
├── gitsigns.lua
└── misc.lua

Neovim-specific commands

On top of the Vim commands you already know, Neovim and this guide's plugins add commands of their own, all checked on 0.12.5 with the configuration above:

CommandWhat it does
:checkhealthFull diagnosis of the installation (providers, plugins, LSP)
:LazyOpen the plugin manager (install, update, remove)
:MasonOpen the LSP server manager
:TSUpdateUpdate the installed Tree-sitter parsers
:checkhealth vim.lspShow the active LSP servers and their configuration
:lsp restartRestart the LSP servers of the current buffer
:TelescopeOpen Telescope (fuzzy search)
:Neotree toggleOpen/close the file explorer
:lua vim.print(vim.lsp.get_clients())List the connected LSP clients

Migrating from Vim

If you have an existing Vim configuration, moving to Neovim can be done gradually, without ever breaking your working environment: each step is usable on its own.

Step 1: use your existing .vimrc

Create a minimal init.vim that uses your Vim configuration:

" ~/.config/nvim/init.vim
source ~/.vimrc

At this stage, Neovim works exactly like your configured Vim. The vim-plug plugins keep working, and you already gain Neovim's modern defaults.

Step 2: move to Lua gradually

Keep your init.vim and add Lua files as you go. In init.vim, you can load Lua with the lua command, which lets you convert one topic at a time, options first:

" ~/.config/nvim/init.vim
source ~/.vimrc
lua require('options') " Loads lua/options.lua

Step 3: switch to init.lua

When you are comfortable, replace init.vim with init.lua and move the plugins to lazy.nvim. Keep the old file under another name while you check that :checkhealth reports no regression.

Troubleshooting

Most failures of a Neovim configuration come from a missing external dependency (git, ripgrep, the tree-sitter CLI, Node.js for an LSP server) or from a tutorial written for an older version. The table follows that order: missing tool, then a command that has changed.

SymptomLikely causeSolution
No true colourTerminal does not support termguicolorsCheck that $TERM contains 256color or truecolor
Error when lazy.nvim startsGit not installedsudo apt install git
:Telescope live_grep failsripgrep missingsudo apt install ripgrep
LSP does not startServer not installedOpen :Mason and install the server
checkhealth shows "provider not found"Python/Node provider missingsudo apt install python3-pynvim and/or npm install -g neovim
The clipboard does not workClipboard tool missingInstall xclip (X11) or wl-clipboard (Wayland)
Icons are squaresNerd Font missing or not set in the terminalInstall a Nerd Font and set it in your terminal. On WSL, the font must be installed on the Windows side
ENOENT … (cmd): 'tree-sitter' while installing parserstree-sitter CLI missing or too oldInstall CLI 0.26.1 or later (Tree-sitter section) and build-essential
E492: Not an editor command: LspInfoNeovim 0.12: nvim-lspconfig no longer creates this command:checkhealth vim.lsp
E492: Not an editor command: TSInstallInfoCommand removed from nvim-treesitter's main branch:lua print(vim.inspect(require('nvim-treesitter').get_installed()))
Plugin not loadedError in the lazy.nvim specOpen :Lazy and look for errors in red
Slow startupToo many plugins loaded at startupUse lazy loading (event, cmd, ft in lazy.nvim)

Key points

  • Neovim keeps the essentials of Vim (modes, motions, text objects): your editing reflexes work as they are, only a few old plugins such as YouCompleteMe cause trouble.
  • Distribution packages lag behind: install 0.12 from the official archive, with its SHA-256 checksum verified.
  • Lua configuration is more readable, faster and better structured than VimScript (modular files in lua/).
  • Native LSP provides autocompletion, diagnostics, jumping to definitions and renaming, IDE features without leaving the terminal; K, grr, grn and [d/]d are active by default.
  • Tree-sitter offers syntax highlighting based on code analysis (not regexes), more accurate and more reliable; nvim-treesitter needs the tree-sitter CLI to compile its parsers.
  • Telescope replaces fzf.vim with a richer interface and more sources (files, text, buffers, diagnostics, help).
  • lazy.nvim manages plugins with lazy loading, version locking and a graphical interface.
  • The :checkhealth command is your first reflex when something goes wrong: it diagnoses the whole configuration; :checkhealth vim.lsp replaces the old :LspInfo.
  • Moving from Vim is gradual: start with source ~/.vimrc, then move to Lua as you go.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn