The Neovim ecosystem changed with the native implementation of the Language Server Protocol and Lua as the primary extension language, supplanting our most beloved VimScript... These new capabilities led to the development of more complex plugins that can bring Neovim closer to a full-blown IDE, or just make it an even more powerful text editor.
Still, the core features of Neovim are pretty barebone, so unless you want to configure everything from scratch, you have to rely on plugins to turn it into a "modern" editor. There are basically three main approaches that I know of to build a modern configuration (leaving aside the pre-LSP VimScript plugins):
-
The all-in-one approach with CoC, that provides most features out of the box, dedicated plugins for each language, but it can almost be considered a separate ecosystem as it is mostly based on JavaScript (there is some interoperability with VSCode plugins).
-
The all-in-one approach with a modern Neovim distribution (LazyVim, AstroNvim, NvChad, LunarVim), that will probably include most of the plugins presented in this article, with a layer of abstraction over the raw plugin configuration, which is probably fine for simple tweaks, but can be annoying for more advanced customizations.
-
The "from scratch" approach, maybe with the help of a minimalistic distribution like
kickstart.nvim
. Unfortunately it crams everything into one file, which doesn't make it particularly readable or maintainable. Another drawback of this approach is that you will have to apply for a PhD in Neovimology, but this is a small price to pay if you spend most of your coding career inside Neovim.
This article will help you build a Neovim config from scratch, which means choosing and configuring each plugin. Since I don't want to make you write the 15th Neovim distribution that covers all previous 14 distributions use cases, and I also don't want to depend on an overly complex configuration for such a critical piece of software, I will try to stay as close as possible to the vanilla (Neo)Vim experience. The purpose is to give you a simple, understandable, maintainable base that you can further customize anyway you want.
§Prerequisites
-
A terminal emulator with true colors and icon font support. If you don't know which terminal to choose, you cannot go wrong with Alacritty. You should also pick a variant of Nerd Fonts and configure it as your terminal font (nowadays, many CLI tools assume proper icon font support).
-
A decent understanding of Neovim. With enough plugins, you can turn Neovim into a completely different text editor, but I don't think you can truly benefit from it until you learn the core features inherited from Vim.
Fortunately, Neovim has an excellent builtin help system and learning material:
- The interactive tutorial (
:Tutor
). - The user manual (
:h usr_toc
). - The reference manual (
:h reference_toc
).
Knowing the core features is one thing, learning how to use them effectively is another. You could complement these resources with workflow-oriented tutorials (like this video).
- The interactive tutorial (
-
A reasonable knowledge of Lua. It is not a difficult programming language, but it has some oddities. After reading Lua: Getting Started, I would recommend skimming through Programming in Lua, with special attention to key subjects like the general syntax, builtin types and methods, functions, and how to work with tables, which are Lua's main data structure.
-
A backup of your existing configuration, including the following directories:
~/.cache/nvim/
.~/.config/nvim/
.~/.local/share/nvim/
.
You should also version your new configuration inside a Git repository, accidents happen...
-
Finally, I encourage you to test a battery-included distribution like LazyVim. The experience is different enough from vanilla Vim / NeoVim that it should give you a better idea of what's possible, and it will also serve as a hands-on introduction to the most popular plugins. Then you can come back to this article to build your config from scratch.
§Core configuration
This section explains how to convert common configurations options from VimScript to Lua, organizing the configuration as follows:
§Init
Neovim looks for Lua modules in ~/.config/nvim/nvim/lua
. The namespace is shared
between builtin modules, plugins that we will add later, and your configuration
files, which is why I recommend creating a dedicated user module.
There are two ways to create Lua modules that you can import with a statement
like require('user')
:
- It can be a single file like
~/.config/nvim/nvim/lua/user.lua
, which doesn't allow sub-modules. - Or you can create a
user
directory that contains aninit.lua
file, which allows sub-modules.
Let's create a directory-based module with an empty init.lua
:
In place of the traditional init.vim
, Neovim sources ~/.config/nvim/init.lua
if it
exists. Because we want most of our configuration inside ~/.config/nvim/lua/user
,
this file should only require the user module:
§Options
Create the module ~/.config/nvim/lua/user/options.lua
that will contain our
Neovim configuration:
You can source this file from user/init.lua
:
To set Vim options with Lua, you can assign values to the table
vim.opt
, which works like
:set
. For example:
vim.opt
also allows to update list or map-style options in a convenient way:
Note that under the hood, options are variables with different scopes (global,
buffer, window), and some global options can be changed locally using
vim.opt_local
(equivalent of
:setlocal
). Usually,
Neovim does sensible things with these options when switching buffers or
windows, so using vim.opt
is enough for most purposes.
In a few cases, we will explicitly set buffer-scoped variables using
vim.b[bufnr].foo
, where bufnr
is the buffer ID (0 for the active buffer).
You can also use vim.b
as a shortcut for vim.b[0]
). This is equivalent to
using :let b:foo
.
Finally, some configuration variables are not options, so they are not
available under vim.opt
, most notably the leader key mappings:
§Keymaps
Create the module ~/.config/nvim/lua/user/keymaps.lua
that will contain our
key mappings. You can source this file from user/init.lua
:
When defining keymaps, the benefit of using Lua becomes clear. Instead of the
convoluted variants of map
, Neovim offers a single
vim.keymap.set
method
with the following parameters:
mode: string | table
like'n'
or{'n', 'v'}
.keymap: string
like'<leader>f'
.action: string | function
likevim.buf.format
.options
likenoremap
,silent
,expr
.
For example:
You can access VimScript commands such as :cnext
and :cprev
through
vim.cmd
. Linters and LSP
servers can set diagnostics which are accessible through the
vim.diagnostic
API.
§Autocmds
When you want to change an option based on a buffer's filetype, or react to
specific events, you can define auto-commands. Create the module
~/.config/nvim/lua/user/autocmds.lua
that will contain our auto-commands, and
source it from ~/.config/nvim/lua/user/init.lua
:
The Lua equivalents of autocmd
and
augroup
are available
in the vim.api
module. The
following autocmd automatically enables wrapping and spell check for Git
messages and Markdown files:
Notice the use of vim.opt_local
to change options only in the current buffer
or window.
Another example is the following autocmd which returns the cursor to the last location when you open a buffer:
Notice the use of pcall
to catch eventual
errors (the buffer may have been modified externally, so the mark may be
out-of-range).
§Plugin management
Now is the time to extend Neovim with plugins. This section introduces Lazy.nvim, a plugin manager design to load plugins only when they are required, which improves startup time and resource usage.
§Installation
The plugins will be defined in the module ~/.config/nvim/lua/user/plugins/
:
Lazy.nvim will source any files (sub-modules) you put in
~/.config/nvim/lua/user/plugins/
. It expects each module to return a table
containing one or more plugin specifications. It is common practice to set
plugin-dependent keymaps and autocmds alongside the plugin configuration. For
now we will return an empty table from this module:
Edit ~/.config/nvim/lua/user/init.lua
to automatically download and setup
Lazy.nvim, sourcing the plugins from user.plugins
(you can do that after the
core modules, since they don't rely on plugins):
I also disabled the change detection because it will inevitably cause errors as
you tweak your config. The downside is that you will have to restart Neovim for
changes to take effect. After a restart, Lazy.nvim automatically installs the
missing plugins. You can also do this from the Lazy UI that you can access with
the command :Lazy
. The main keybindings are listed at the top, such as U
to
update the plugins.
§Configuration
To illustrate how to add a plugin, let's take
mini.pairs
, which automatically
insert the closing parenthesis, quote, bracket, and similar closing delimiter
after you insert the opening pair.
Since it is part of a family of "mini" plugins, you can add it to a dedicated
module ~/.config/nvim/lua/user/plugins/mini.lua
:
If you restart Neovim, you will see that Lazy.nvim has installed mini.pairs,
but it doesn't seem work. Many Lua plugins require you to call their
setup(opts: table)
method to properly initialize them, where opts
contains
the plugin configuration options.
Lazy.nvim supports a few attributes related to plugin configuration:
init: func(_)
always runs during startup whether the plugin is loaded immediately or not. This is where you should set the configuration for VimScript plugins that do not have a Lua API.opts: table | func -> table
is used to set plugins options from a table, or from a function that returns a table, which is how most Lua plugins represent their configuration.config: func(_, opts:table) | bool
can be defined as a function that accepts theopts
table as its second argument to configure the plugin. If this property is unset, butopts
is set, it defaults to a call toplugin.setup(opts)
, which is how most Lua plugins are setup.
If both opts
and config
are unset, which is the case here, there will be no
call to the plugin's setup
function, which explains why mini.pairs isn't
working. As a shorthand for a call to the setup function, you can set config = true
:
Alternatively, you can define an empty table of options:
Or define a function that returns these options:
Or define both the options and the config function:
Obviously, the most concise option is usually the better, depending on how much configuration each plugin needs.
§Lazy loading
Now we get to the main benefit of Lazy.nvim. There is no need to load
mini.pairs as soon as Neovim starts since it is only ever useful in insert mode
when we insert an opening character. We can improve the configuration to load
it only when the InsertEnter
event gets triggered for the first time:
If you restart your editor and go to the Lazy.nvim UI (:Lazy
), you will see
the InsertEnter
trigger next to the plugin name, and most importantly, it is
under the section "Not Loaded".
Lazy supports additional triggers:
- Filetypes defined with the
ft
attribute, especially useful when some plugins only apply to specific programming languages. - Keymaps defined with the
keys
attribute, which is similar to defining them in theinit
function, except they are listed in Lazy.nvim UI. - Commands defined with the
cmd
key, again mostly for documentation purposes.
Note that regardless of these triggers, plugins are automatically loaded when
explicitly required with a statement like require('mini-pairs')
. So when you
define a keymap before a plugin is loaded, you should always make sure to only
require the plugin lazily, for example:
Instead of this:
§Extensibility
You can specify a list of plugins specs as dependencies
:
Dependency here means that the dependent plugins get loaded before the main plugin is loaded, which is mostly equivalent to:
- Defining the dependent plugins alongside the main plugin.
- Setting
lazy = true
on the dependant plugins. - Explicitly calling
require()
in the main plugin'ssetup
function.
The important point is that these dependencies are lazy-loaded by default, contrary to top-level plugins.
As you've seen previously, many attributes of the plugin specs can be defined
as functions, like config: func(_, opts)
. This is an extension mechanism that
allows to define a parent plugin configuration, and then modify it when a
dependant plugin is enabled:
I wouldn't expect you to use this a lot in a static configuration, but this mechanism is very useful for Neovim distributions that bundle optional plugins. The main use of this you will see in the rest of this article is to specify simple lazy-loaded dependencies in a concise way.
§LSP (core)
The Language Server Protocol (LSP) provides many features to improve the programming experience. The client side is natively supported by Neovim, but each language has its own server and configuration options. A collection of LSP client configurations is maintained in the nvim-lspconfig repository, that you can use as a plugin, but it still requires extensive configuration from our end, which will be the focus of the next few sections.
§Setup
Each language server has its own module and configuration options that you can setup as follows:
These setup methods register each server with Neovim and configure the necessary hooks so they can attach to a buffer of the matching type. Because you will likely have to configure multiple languages with their own set of options, we can refactor the setup code as follows:
The LSP settings are in their own table so we can add our own attributes later without sending them to lspconfig. Repeating the server name in the settings is annoying but required.
Before continuing, I suggest you add lazydev.nvim (requires Neovim >= 0.10) to automatically configure LuaLS to get better completion and typing information when working with the Neovim Lua API and plugins:
§Mason
If you tried to restart Neovim, you may have seen a message like "Spawning
language server with cmd: lua-language-server
failed. The language server is
either not installed, missing from PATH, or not executable."
Indeed, in addition to the configuration of Neovim as an LSP client, you also
have to install the servers binaries such as
(luals
or
rust_analyzer
), though it would be nice if
we didn't have to look for how to install the various things we need to get a
functional LSP setup. This is exactly what
mason.nvim
was designed for.
Mason is a cross-platform package manager that provides an interface inside Neovim to quickly install many coding related tools like LSPs, linters, etc. You configure the base plugin as follows:
After restarting Neovim, you can open Mason with <leader>cm
or :Mason
.
Binaries installed through Mason are stored in ~/.local/share/nvim/mason/bin/
on Linux, and this directory is automatically added to your $PATH
, so most
language servers and tools should just work™.
There is one more thing you have to configure though, it is adding Mason as a
dependency of nvim-lspconfig, since the $PATH
is changed only after Mason is
setup:
Now you can try to install lua-language-server
by pressing i
over its
entry, open a Lua file, and run :LspInfo
to check if the server is attached
to the buffer.
There is an extra plugin that improves the integration with lspconfig
called mason-lspconfig
. It hooks into the LSP client configuration to
make their installation with Mason work, and it can also automate the
installation of LSP servers.
Beyond the languages servers distributed as standalone binaries, it is sometimes better to use the version distributed with the language's toolchain or installed inside a dedicated environment like a Python virtualenv.
§On attach
The settings are server-specific, but there is a common set of options shared
by all language servers, such as the on_attach
callback invoked when a
language server attaches to a buffer. This callback receives a handle to the
LSP client and the buffer number as arguments:
The on_attach
callback is where you can setup the buffer for use with a
language server, enabling features like diagnostics, formatting, code actions.
To make our life easier, we will immediately use pcall
to log any errors that
may arise when this callback is invoked:
§Keymaps
We will configure the standard LSP keymaps for the most common queries
available under vim.lsp.buf
.
Most implementations seem to configure these in the on_attach
callback, but
even if you call these LSP functions in a buffer that has no client attached,
they are basically a no-op.
All the keymaps we set in the on_attach
callback should be scoped to the
buffer the client is attached to. Generally you just have to pass the buffer
option to vim.keymap.set
, which can be done automatically by defining an
auxiliary function:
These commands may ask for input with the
vim.ui.input
API and may output
multiple results with an
lsp-on-list-handler
(by default, the quickfix list). It will be enough for now, but you will see
later how we can improve this using Telescope.
§Auto-completion
Let's tackle the subject of completion. By default, Neovim supports a number of sources for keyword completion, and customizable omni-completion, but they are all pretty limited in term of extensibility.
This section explains how to setup
nvim-cmp
, a completion plugin that
supports the features you would expect like customizing the matching algorithm,
the sort order, the formatting of the entries, and showing documentation on
hover.
It is also easily extensible, so there is a number of third-party sources available. It can integrate with a snippet engine like LuaSnip, support LSP completions, and even suggestions from AI coding assistants. You can also tune the priorities of each source.
§nvim-cmp
Let's start with the recommended configuration (with the exception of the cmdline mappings):
The configuration above uses the standard Neovim insert-mode completion keymaps (see :h ins-completion):
- Previous item:
<C-p>
. - Next item:
<C-n>
. - Confirm selection:
<C-y>
- Abort completion:
<C-e>
I prefer to stick with the default
keybindings,
but there are ways to configure a supertab
mapping.
Note that you can still trigger the native Neovim completion with keymaps like
<C-x><C-f>
for file path completion even if you don't have the path
source
in nvim-cmp
.
By default, nvim-cmp prioritizes entries from sources that are defined first
(but this is configurable as you will see in the next section). Sources can
also be grouped, this is why nvim_lsp
is inside a sub-table. The effect of
grouping is that nvim-cmp will only show the suggestions from the first group
that matches.
If nvim_lsp
returns any suggestions, you will not see suggestions based on
words in the buffer, though you may get them while writing a comment, since the
LSP is unlikely to return any match at this location. This fallback will also
happen when no LSP is attached to the buffer.
To enable the integration with the LSP, you have to inform the language server which completion candidates are supported:
§LuaSnip
A complementary form of completion is snippet expansion. It inserts a pre-defined templates, like a function definition with placeholders for the arguments and the body, with the ability to jump between these placeholders and edit them. This feature relies on a snippet engine to interpret and expand snippets.
The first step is to configure the snippet engine (I prefer LuaSnip, but others are supported):
This configuration defines two keybindings to jump between the fields of the
expanded snippet. LSP servers may already provide some snippets, but using a
plugin like
friendly-snippets
adds a
lot more. These snippets are written in the VSCode format, so you have to
configure the appropriate loader.
nvim-cmp
relies on a snippet engine for both snippet expansion, regardless of
their source, and for sourcing additional snippets, like our friendly snippets:
§Suggestions order
The core of what makes a good autocompletion engine is how relevant the suggestions are. If you open a Rust file with our current configuration, you may have a bunch of unrelated snippets at the top or the suggestion list, called postfix snippets, which means they apply after any expression. It worth understanding why they appear first.
The items are sorted according to a sequence of comparators taking two elements
as arguments and returning bool | nil
, indicating whether one element is
greater than the other, or whether they are considered equal.
The default sort configuration is as follows:
You may have to look at the source code to understand the purpose of the most obscure comparators:
offset
: prefers items where the offset at which the input text matches the completion is the lowest.exact
: prefers items that contain the input text.scopes
: prefers narrower scopes (like local variables to global variables).score
: prefers higher scores, computed according to the index of the source and its weight.recently_used
: prefers items that were most recently used (based on their label, likeinto()
, regardless of the context).locality
: prefers items that appear close to the current cursor location.kind
: prefers LSP entities with the smallest ordinal value (likeMethod
beforeClass
, with a few exceptions).sort_text
: lexicographic order.length
: prefers shorter suggestions.order
: prefers smallest index (derived from the original order of suggestions returned by each source).
Here's the source for cmp.config.compare.kind
:
It applies a penalty to Text
items, and ensures Snippet
s appear first,
which explains the behavior with Rust suggestions. Since postfix snippets match
everything, they will appear first if there aren't any better suggestions
according to the previous comparators. As you complete the input, the other
comparators should kick in like recently_used
and locality
, which should
improve the relevance.
§LSP (plugins)
We are still missing one last key LSP feature: auto-formatting. This is where things get a little bit complicated because the LSP is not the only way to auto-format files, especially for markup languages which have their own set of of formatting tools. With a few plugins, the LSP can be configured as a fallback formatting source, allowing other formatting and linting tools when necessary. We will also install a plugin which prints status information from the LSP.
§Auto-formatting
The easy way to setup LSP formatting is to define a keymap like we've done previously, but it's not as simple to make it work reliably:
- Some formatting tools (especially for markup or configuration languages) do not support the LSP.
- Some language servers like gopls do not include things like organizing imports in the code formatting action.
- If you have multiple LSP servers attached to the same buffer, you will get a prompt asking you to choose which client should perform the formatting.
- You may want to disable formatting for specific buffers or file types if it gets in the way.
- You may want to enable auto-formatting on save, which requires an auto-command.
- Formatting can mess up the buffer if done asynchronously.
- Some servers do not support efficient formatting (like only formatting a range of lines and not the entire buffer).
Conform.nvim handles most of these issues:
- It supports non-LSP code formatters, and can fallback to the LSP.
- It doesn't replace the whole buffer, which maintains folds and extmarks.
- It implements range formatting, even when unsupported by the language server.
The base configuration is pretty simple:
Since it uses the builtin autocmd, there isn't any room for customization, like organizing imports before formatting, or toggling auto-formatting on and off for a specific buffer.
To decouple the LSP config and conform.nvim, it is best to set per-buffer
variables like autoformat
and autoimport
using a custom BufWritePre
(replacing the default format_on_save
option):
The organize import code, that we put in a new user.utils
module, is a little
bit more involved (source:
gopls/doc/vim.md):
With this custom autocmd in place, you can turn off the autoformatting with a
command like :lua vim.b.autoformat = false
, same for autoimport
. You can
update them automatically with a FileType autocmd:
You can also update the LSP configuration to support per-server on_attach
hooks that set these variables:
§Linting
Some languages have additional tools that provide formatting and linting which are not bundled in their main LSP server. The deprecated plugin null-ls.nvim used to expose these tools through a general-purpose LSP server, making the LSP the common denominator. none-ls.nvim is a maintained fork that provides the same features.
The main drawback is that you have to handle multiple language servers attached
to the same buffer, which wouldn't work with our generic on_attach
hook since
the keybindings, for example, do not specify which LSP client to use.
An alternative that seems to be the way to go at the moment for the formatting part is conform.nvim that we've already covered in the previous section. The equivalent for linting is nvim-lint, which is as easy to configure:
§fidget
LSP servers may appear stuck while they are starting up or after some actions. The LSP provides a way to send status messages that indicate what the server is doing in the background (for instance, indexing your project). fidget.nvim is a plugin that can display these status messages in a non-intrusive way:
Don't forget to add it as a dependency of nvim-lspconfig so it is started automatically:
§Syntax highlighting
Text and UI elements in Vim are associated to highlight groups. A colorscheme maps each highlight group to a set of colors (foreground, background) and styles (italic, underline, etc).
There are three main sources of syntax highlighting in Vim:
- The "legacy" Vim syntax files based on regexes.
- TreeSitter parsers which provides a more precise syntax tree for the supported languages.
- The LSP that also provides some semantic highlights.
- Plugins that define their own highlight groups.
Customizing the theme is just a matter of finding the name of the highlight group you want to change, and set its style in your colorscheme.
§Colorscheme
If you don't want to build a colorscheme from scratch, you can choose among the high quality colorschemes like catppuccin that support the standard highlight groups and many plugins. For the DIY approach, you can start by creating a basic colorscheme module:
There are three main steps:
highlight clear
resets the highlight groups to the default settings.syntax reset
resets the colors to their default values.- The loop uses
nvim_set_hl
to set the style for each highlight group based ongroups
andcolors
.
Next, create the "real" colorscheme in ~/.config/nvim/colors/
, which will
just import the previous module:
The first line makes sure the package user.colors
is reloaded each time this
file is sourced, so you can iterate on it quickly using :luafile colors/user.lua
(:colorscheme user
will do the same thing under the hood).
You can then enable this colorscheme in the options, along with termguicolors
for 24-bit terminal colors:
Except for the Normal
group that we explicitly set, everything else uses the
default values. So now you have to find the list of highlight groups:
- UI:
:help highlight-groups
. - Syntax:
:help group-name
. You should start with the main groups prefixed by *, since all other highlight groups are linked to them. - Diagnostics:
:help diagnostic-highlights
. - Plugins: check the plugin documentation.
You can work iteratively: when you stumble upon some really ugly color, fix it
in your colorscheme. If you don't know where it comes from, you can list all
the defined highlight groups and their current colors with
:highlight
(or :hi
for short). Alternatively, if you can put your cursor over it, you can run the
command :Inspect
.
To fix the style of an highlight group, you can:
-
Override the options
fg
(foreground color),bg
(background color),sp
(decoration color), and a number of styles likeitalic
,bold
,underline
,undercurl
, etc.:h nvim_set_hl
for the full list of options. If you don't specify a color or set it to"none"
, it will be "transparent" (the color of the first UI element under it). -
Link the highlight group to another highlight group, which is useful for some plugins like fugitive.vim:
Since we have the power of Lua, we can do interesting things like blending colors:
Using these functions, you can add a halo effect to the diagnostics by mixing 10% of the diagnostic color with 90% of the background color:
§TreeSitter
TreeSitter provides a collection of incremental parsers for the most common programming languages. It is a library to build higher-level tools so it's not usable as-is, but there are plugins that provide features based on it. One of them is nvim-treesitter, which among other things provides more detailed syntax highlighting than Neovim's builtin regex-based syntax files.
You can configure it like so (note that you need a C compiler like gcc
,
clang
, or zig
):
It relies on a number of extra highlight groups listed in :h treesitter-highlight-groups
,
that are either global, like @variable
, or language-specific if you append
the language name, like @variable.lua
:
There are other use cases for TreeSitter like indentation, folding, additional text-objects, some of which are still experimental (see: available modules).
§LSP
The LSP can also be a source of syntax highlighting. It defines semantic tokens, which are designed to provide refinements on top of the language grammar syntax highlighting.
Currently it causes flashes when it gets refreshed, so I opted-out of this feature with the following code:
If you choose to keep it active, you can find the available highlight groups
with :h lsp-semantic-highlight
.
§UI
The last missing piece in the modern text editing experience is the UI. The most important plugin is telescope.nvim, which provides an interface to quickly filter arbitrary lists of items like files, buffers, diagnostics with a fuzzy finding algorithm. I will cover two additional plugins: heirline.nvim to allow simple customization of the status bar, and which-key.nvim to display the available key mappings.
§Telescope
Telescope is a general purpose fuzzy finder over lists. It is helpful to go over some terminology:
- Finders are the program that generate a list of items (list of buffers, list of files in the current directory, etc).
- Sorters are the functions that order and filter the items according to a prompt.
- Previewers are the functions that display a preview of the active item.
- Actions are the operations that can be applied to one or more selected items, usually triggered by a key mapping.
- Pickers are the builtin functions (presets) that tie together all the previous concepts.
Let's first add the plugin:
To start a picker, you have to bind it to some key:
As we've seen previously, you cannot require('telescope.builtin')
directly in
the init
function because it would load Telescope as soon as this function is
invoked, which is when Neovim starts. Each keybinding defined here has to
require it lazily, hence the lazy_telescope
wrapper.
Note that the Telescope prompt has the same modes as regular Vim text editing.
For instance, you can use j
and k
to move up or down in the list in normal
mode. Since the default mode is insert, you may want to add a few movement and
editing keybindings for this mode:
You may find these builtin keybindings particularly useful:
?
(normal) and<C-/>
(insert): show the list of keybindings.<Tab>
: select / unselect of the focused item (can be used to select multiple items).<C-x>
(split),<C-v>
(vsplit),<C-t>
(tab): open selection with the specified mode.<C-q>
send the selected items to the quickfix list.<Esc>
(normal) and<C-c>
: exit Telescope.
Each picker accepts dedicated options in addition to the parameters from
opts.defaults
, like custom keybindings, layout options, and themes. For
instance, you can hide the preview in the builtin current_buffer_fuzzy_find
picker:
Finally, Telescope supports extensions that provides alternative sorting algorithms, including a fast implementation of fzf that you can configure as follows:
You should also run :checkhealth telescope
which
will indicate the status of the fzf
extension and other possible
optimizations, like installing rg
and fd
to improve the performance of the
grep and file search pickers.
§Heirline
If you ever attempted to change the default Neovim statusline, you may have
encountered the mesmerizing printf-inspired format strings you can use in the
statusline
option.
Here is what it looks like for the default status line:
There are ways to insert the result of an expression so you can really put arbitrary things in there, add custom highlight groups, and even change it on the fly, except it is an absolute nightmare to deal with.
Heirline provide a high-level API based on reusable components to generate these format strings for you. Here is the configuration that matches the default status line:
The first thing you may want to improve is %f
, which doesn't work exactly
like in the builtin status line since it doesn't shorten intermediate
directories in long paths, and poorly handles relative paths (going to a
different location with the LSP leaves the full absolute path).
The
cookbook
contains a lot of examples from which the following snippets are largely
inspired. We can implement the FilePath
provider as follows:
And use it for the status line:
We can continue with few additional providers like the git branch name:
The diagnostics info:
The names of the LSP server(s) attached to the buffer:
And you can add them to the main config:
Note that default StatusLine
styling reverses the colors, so you may have to
change it in your colorscheme. Additionally, the colors should be evaluated
again each time the colorscheme changes, which can be implemented using an
autocommand:
§Which key
Which-key.nvim, inspired by the plugin of the same name in
the doomed world of Emacs, displays the available keybindings when you start to
press the leading keys. It can also show the values of the registers when you
press "
, the marks when you press '
or `
, and the list of spelling
suggestions after you press z=
, and more.
Since it relies on the desc
attributes that we've set to all our keybindings,
it is pretty easy to configure:
Note that the group names don't seem to work with buffer-local keybindings at
the moment (they stay labeled with the default +prefix
instead of the defined
name). Hopefully that will be fixed in a future release.
§Going further
The purpose of this article was to show you how to configure the core plugins so you can have a basis you can extend yourself. There would be a lot more to cover, but it is now a matter of preference, and you should be able to install and configure any other plugins on your own.
The best way to see the most popular plugins in action is to try one of the most configurable Neovim distributions like LazyVim, but here are some ideas to further improve the editing experience:
- comment.nvim: pretty self-explanatory, this is a commenting plugin.
- flash.nvim: one of the many motion enhancement plugins.
- fugitive.vim: Git integration within Vim, you may be interested by Vim Fugitive in action.
- trouble.nvim: a quickfix list on steroids.
- copilot.lua: Lua version of copilot.vim that can be integrated into nvim-cmp with copilot-cmp.
- nvim-dap: when printf-debugging is not enough, with nvim-dap-ui.
- dressing.nvim: visual
improvements to the
vim.ui.input()
andvim.ui.select()
prompts. - vim-notify: visuel improvements to
the core
vim.notify()
feature. - neo-tree.nvim: a pretty file explorer.
- bufferline.nvim: buffer list in the tabline, enabled by default in many Neovim distributions.
Happy Neovim-ing!