Writing a configuration for Neovim
Hello, today I will write a configuration for the Neovim terminal editor in pure Lua. We will not use VimScript from the word at all. I will tell you what are the advantages of creating such configurations, how to follow KISS (Keep It Stupid Simple) all the time supplementing such configurations, and also talk about useful plugins for web development and more.

What are the advantages?
Configurations in VimScript are not the most understandable for new Vim users. They are often overloaded with unnecessary information, the commands they use are not at all simple, and in order to minimally set up the same space instead of a tab, you have to surf the documentation from top to bottom, due to the fact that you will have syntax errors🐛 (and they will certainly be🤐) .
This problem has been solved in different ways. Now, for example, a new language has been released for Vim configuration, which is called vim9script. But it doesn’t solve the problem either.
The problem was solved by Neovim. This is the same Vim that is being rewritten by a team of enthusiasts and the community to make Vim more accessible to people. It was in Neovim for the first time that it was discussed not to use VimL (VimScript), but Lua. And the guys did it, the editor has full Lua support.
Installation
You can install Neovim on all three platforms (Windows, Linux, MacOS). This is all done quite simply.
windows
We go into PowerShell and download Chocolatey:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
Download Neovim itself:
choco install neovim --pre
Linux
We go into the terminal and, depending on your distribution, download the package with Neovim
# Debian, Ubuntu, Elementary, Mint
sudo apt install neovim python3-neovim
# Fedora, RH
sudo dnf install neovim python-neovim
# Arch
sudo pacman -Sy neovim python-pynvim
macOS 🍏
Download homebrew on our poppy:
/bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Download Neovim itself with the necessary packages
xcode-select --install
brew install --HEAD tree-sitter
brew install --HEAD luajit
brew install --HEAD neovim
Configuration
We will be configuring the editor step by step, you can safely skip the sections if you have already dealt with writing configurations for Neovim/Vim. For beginners, I recommend reading everything in sequence for a better understanding of the material🤗
Directory structure
First we need to understand what the root configuration file is (init.lua
).
init.lua is the file that Neovim will load first by default. It is with its creation that you should start the configuration of the editor.
In my experience of 3 years of using this editor, I have seen many configurations. There are two camps:
Those who write to
init.lua
only importing other files;Those who write to
init.lua
basic settings and importing other files.
I find the second approach not too clean in terms of code. You dump in one file and basic settings, and hotkeys, and plugins, leaving only a few imports. What to hide, I used to do this too😄
However, we will do the first way. It guarantees us that over time our configuration will not look like a garden with weeds, where fixing one thing you want to chop off half of the configuration🔪
The structure will be as follows:
.
└── lua
├── base
├── keys
└── plugins
All configurations written in Lua must be stored in a special directory called lua
this is the starting point from which Neovim will import user configurations.
Basic setup
After we have created the base directories, we need to write the general rules for Neovim.
Search
Let’s create a file called search.lua
and write down all the necessary settings for the search there:
--[[ Переменные ]]--
-- Переменная для настройки опций Neovim
-- Все время использовать vim.opt - не удобно, поэтому сделаем алиас
local opt = vim.opt
-- Глобальные переменные
-- То же самое делаем и с vim.global
local g = vim.g
--[[ Поиск ]]--
-- Игнорировать регистр при поиске
-- Если теперь введем "IGNORE", то он найдет и "ignore"
opt.ignorecase = true
-- Не игнорировать регистр, если есть символы в верхнем регистре
-- Если напишем "Ignore", то он будет искать только "Ignore"
opt.smartcase = true
-- Подсвечивать найденные текстовые объекты
opt.showmatch = true
Neovim provides us with an API that we can use with Lua. If we enter the command in the editor :h <команда>
then at the top, in addition to the full name of the command, we will see the namespace in which the command is located.
Basically all commands are in vim.opt
so now, therefore, you should not bother about it, but still, if there is such a need, then all the documentation is at hand📕
Here we see abbreviations for vim.opt
and vim.g
. In other words, with local
in Lua we simply declare a variable and assign a reference to an object as a value vim.opt
and vim.g
respectively.
Tabs
Let’s create another file called tabs.lua
in the directory base/
. We write the following in it:
--[[ Настройка табов ]]--
-- Установка количества пробельных символов, когда сдвигаем с помощью "<", ">"
-- По сути не смотря на expandtab в терминале все равно отображаются пробельные символы, поэтому установим их количество на одно нажатие этих кнопок
opt.shiftwidth = 2
-- 1 таб == 2 пробела c новой строки
-- При нажатии <CR> будут вставлять табы. Табы рисуются как пробелы. Именно поэтому мы должны установить что каждый таб в новой строке - 2 пробела
opt.tabstop = 2
-- Подстраивать новые строки под предыдущий отступ
opt.smartindent = true
Here we simply set options for tabs in the editor. Please note that I did not write the abbreviated parts for vim.opt
and vim.g
it will also need to be specified here.
Additional settings
For all other settings, I create a file called other.lua
in the same directory base/
. Here we write the following:
--[[ Настройка панелей ]]--
-- Вертикальные сплиты становятся справа
-- По умолчанию панели в Neovim ставятся в зависимости от расположения текущей панели. Данная настройка поможет нам держать панели в порядке
opt.splitright = true
-- Горизонтальные сплиты становятся снизу
opt.splitbelow = true
--[[ Дополнительные настройки ]]--
-- Используем системный буфер обмена
opt.clipboard = 'unnamedplus'
-- Отключаем дополнение файлов в конце
opt.fixeol = false
-- Автодополнение (встроенное в Neovim)
opt.completeopt="menuone,noselect"
-- Не автокомментировать новые линии при переходе на новую строку
vim.cmd [[autocmd BufEnter * set fo-=c fo-=r fo-=o]]
Everything is the same here, except for the last command. Pay attention to the line with vim.cmd[[]]
. vim.cmd
is a special command that allows you to execute VimScript commands from a Lua file. The double brackets indicate that we will be passing a string. In general, it could be written like this:
vim.cmd("autocmd BufEnter * set fo-=c fo-=r fo-=o")
However, the two-bracket syntax seems to me easier to read and concise. It’s up to you to choose 👋
Include files in init.lua
We have now created three files: search.lua
, tabs.lua
, other.lua
they are all located in the directory base/
, however, the settings in them will not work yet. In order for everything to work, we need to import all three files into init.lua
, since Neovim will include all configurations from this root file. The connection is made quite easily:
require('base/search')
require('base/tabs')
require('base/other')
Please note that we do not specify the full path, we specify the path starting from the directory lua
. Also, we do not specify file extensions, Lua understands that by default we will include Lua files, so the part with extensions should be omitted.
Now our configuration structure looks like this:
.
└── lua
├── base
│ ├── other.lua
│ ├── search.lua
│ └── tabs.lua
├── keys
└── plugins
Hotkeys
It’s time to set up custom hotkeys! I usually create two files for hotkeys:
main.lua – Hot keys for default actions in the editor
plugins.lua – Hotkeys for plugins
Now we will only create the first file🤗
Let’s first create a special file and name it alias.lua
we will write abbreviations for the key assignment commands into it, it will look like this:
-- Алиас для быстрого доступа к методу установки горячих клавиш
local map = vim.api.nvim_set_keymap
--[[
Метод для установки горячих клавиш (normal)
key - {string} Строка с горячей клавишей
command - {string} Команда
]]--
function nm(key, command)
map('n', key, command, {noremap = true})
end
--[[
Метод для установки горячих клавиш (input)
key - {string} Строка с горячей клавишей
command - {string} Команда
]]--
function im(key, command)
map('i', key, command, {noremap = true})
end
--[[
Метод для установки горячих клавиш (visual)
key - {string} Строка с горячей клавишей
command - {string} Команда
]]--
function vm(key, command)
map('v', key, command, {noremap = true})
end
--[[
Метод для установки горячих клавиш (terminal)
key - {string} Строка с горячей клавишей
command - {string} Команда
]]--
function tm(key, command)
map('t', key, command, {noremap = true})
end
The function is responsible for setting hotkeys. vim.api.nvim_set_keymap
however, we reduced it to map
by creating a variable with a reference to this function. If we did not do this, then the same function nm
would look like this:
--[[
Метод для установки горячих клавиш (normal)
key - {string} Строка с горячей клавишей
command - {string} Команда
]]--
function nm(key, command)
vim.api.nvim_set_keymap('n', key, command, {noremap = true})
end
I think no one will say that it looks compact😅 That’s why we use abbreviations.
Function vim.api.nvim_set_keymap
takes 4 arguments:
The mode in which this hotkey will be used
The key combination itself
The command to be executed when the hotkey is pressed
Additional arguments (of type noremap, which replaces an already existing key combination, if any)
There are four functions in total:
nm
– creates a hotkey in normal modeim
– creates a hotkey in input modevm
– creates a hotkey in visual modetm
– creates a hotkey in terminal mode
If you don’t know what modes the editor has, then I advise you to go through the main tutorial, which can be launched by typing in the terminal: vimtutor
Basic hotkeys
After we have created aliases (abbreviations) for our commands for setting hotkeys, we can start creating the hotkeys themselves.
By creating a file main.lua
we can write the following:
require('keys/alias')
-- Назначает дополнительной клавишей для отключения режима Ctrl + K
im('<C-k>', '<escape>')
Thus, we can now easily create new hotkeys.
At the moment our directory structure looks like this:
.
└── lua
├── base
│ ├── other.lua
│ ├── search.lua
│ └── tabs.lua
├── keys
│ ├── alias.lua
│ ├── main.lua
│ └── plugins.lua
└── plugins
Plugins
It’s time for the sweetest thing – plugins😳
Plugins in Neovim are already written scripts/functionality that extend the capabilities of the editor, making it even cooler!😎
In order for us to be able to install plugins, we need to install Packer. This is such a plugin manager for Neovim. As soon as we install it, we can immediately move on to configuring and installing plugins.
Installing plugins
Let’s create a file packer_install.lua
in the directory plugins/
. It is in this file that we will list all the plugins that we will download. After downloading Packer, we should have a command PackerSync
now we will use it regularly🥹
-- Добавляем Packer как пакет в Neovim
vim.cmd [[packadd packer.nvim]]
-- Используем данный коллбэк как список для плагинов
return require('packer').startup(function()
-- Добавляем Packer в список, чтобы он обновлял сам себя
use 'wbthomason/packer.nvim'
-- LSP сервер
use 'neovim/nvim-lspconfig'
-- Иконки для автодополнения
use {
'onsails/lspkind-nvim',
config = function()
require('plugins/lspkind')
end
}
-- Инсталлер для серверов LSP
use {
'williamboman/nvim-lsp-installer',
config = function()
require('plugins/lsp-installer')
end
}
-- Удобное меню для обозрения проблем LSP
use {
"folke/trouble.nvim",
requires = "kyazdani42/nvim-web-devicons",
config = function()
require("trouble").setup {}
end,
}
end)
This is what the list of plugins will look like. Naturally, here it is incomplete, I reduced it so that it would be convenient for you to read. A full list of plugins and what I use them for will be at the bottom of the article.
Please note that I write the entire list of plugins inside the Packer callback, and each new plugin begins with the keyword use
followed by curly braces describing the plugin. use
this is a special method from Packer that takes an object as an argument. Again, we could write it like this, but we didn’t, so as not to spoil the readability and conciseness:
-- Статуслайн
use({
'nvim-lualine/lualine.nvim',
requires = { 'kyazdani42/nvim-web-devicons', opt = true },
config = function()
require('plugins/lualine')
end
})
Objects that can contain variables and methods are called tables in Lua. Learn more about tables can be read here.
Note that in many variable tables (or table fields) the following is repeated:
Name in quotes
Dependent plugins
requires
Plugin configuration
config
which is written through a nested function
Of course, we can write the configuration for the plugin right inside config
, however, I do not recommend doing so. Your file will just grow. packer_install.lua
.
Let’s take a look at the configuration for the autocomplete plugin. AT packer_install.lua
I added it to the very bottom:
-- Добавляем Packer как пакет в Neovim
vim.cmd [[packadd packer.nvim]]
-- Используем данный коллбэк как список для плагинов
return require('packer').startup(function()
-- Добавляем Packer в список, чтобы он обновлял сам себя
use 'wbthomason/packer.nvim'
-- LSP сервер
use 'neovim/nvim-lspconfig'
-- Иконки для автодополнения
use {
'onsails/lspkind-nvim',
config = function()
require('plugins/lspkind')
end
}
-- Инсталлер для серверов LSP
use {
'williamboman/nvim-lsp-installer',
config = function()
require('plugins/lsp-installer')
end
}
-- Удобное меню для обозрения проблем LSP
use {
"folke/trouble.nvim",
requires = "kyazdani42/nvim-web-devicons",
config = function()
require("trouble").setup {}
end,
}
-- Автодополнение
use {
'hrsh7th/nvim-cmp',
requires = {
'L3MON4D3/LuaSnip',
'saadparwaiz1/cmp_luasnip',
'hrsh7th/cmp-nvim-lsp',
'hrsh7th/cmp-path',
'hrsh7th/cmp-emoji',
'hrsh7th/cmp-nvim-lsp-signature-help',
'hrsh7th/cmp-nvim-lua'
},
config = function()
require('plugins/cmp')
end
}
end)
Note that this plugin has a lot of dependencies, and the configuration is in a separate file, which is located in ~/.config/nvim/lua/plugins/cmp.lua
if we go to this file, we will see the following:
local cmp = require('cmp')
local lspkind = require('lspkind')
cmp.setup{
snippet = {
-- REQUIRED - you must specify a snippet engine
expand = function(args)
require'luasnip'.lsp_expand(args.body) -- Luasnip expand
end,
},
-- Клавиши, которые будут взаимодействовать в nvim-cmp
mapping = {
-- Вызов меню автодополнения
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
['<CR>'] = cmp.config.disable, -- Я не люблю, когда вещи автодополняются на <Enter>
['<C-y>'] = cmp.mapping.confirm({ select = true }), -- А вот на <C-y> вполне ок
-- Используем <C-e> для того чтобы прервать автодополнение
['<C-e>'] = cmp.mapping({
i = cmp.mapping.abort(), -- Прерываем автодополнение
c = cmp.mapping.close(), -- Закрываем автодополнение
}),
['<C-p>'] = cmp.mapping(cmp.mapping.select_prev_item(), { 'i', 'c' }),
['<C-n>'] = cmp.mapping(cmp.mapping.select_next_item(), { 'i', 'c' }),
},
sources = cmp.config.sources({
{ name="nvim_lsp" }, -- LSP 👄
{ name="nvim_lsp_signature_help" }, -- Помощь при введении параметров в методах 🚁
{ name="luasnip" }, -- Luasnip 🐌
{ name="buffer" }, -- Буфферы 🐃
{ name="path" }, -- Пути 🪤
{ name = "emoji" }, -- Эмодзи 😳
}, {
}),
formatting = {
format = lspkind.cmp_format({
mode="symbol", -- show only symbol annotations
maxwidth = 50, -- prevent the popup from showing more than provided characters (e.g 50 will not show more than 50 characters)
})
}
}
It’s easy to get confused here, isn’t it? However, at the moment you do not need to understand everything written, since you have not read documentation for this plugin.
You need to understand a few basic principles:
We always try to separate the configuration from the list of plugins.
For each configuration, we create our own separate file. In this case, this is the file
lua/plugins/cmp.lua
Let’s take another look at how the plugin is described in the list for plugins:
-- Автодополнение
use {
'hrsh7th/nvim-cmp',
requires = {
'L3MON4D3/LuaSnip',
'saadparwaiz1/cmp_luasnip',
'hrsh7th/cmp-nvim-lsp',
'hrsh7th/cmp-path',
'hrsh7th/cmp-emoji',
'hrsh7th/cmp-nvim-lsp-signature-help',
'hrsh7th/cmp-nvim-lua'
},
config = function()
-- ДАННАЯ ЧАСТЬ ОЧЕНЬ ВАЖНА
require('plugins/cmp')
end
}
I have marked the line of code to look at for convenience. If we do not import our configuration that was in the listing before, then the configuration will not work. Importing works exactly the same as importing into init.lua
we’re just through require()
specify the path to the file.
How to set up plugins
For example, let’s take a plugin lualine
let’s say that you have already listed it (don’t be afraid, half of the repositories already indicate how you need to list the plugin in the list) and now you start configuring it.
In most cases, you will already have an example configuration on the README on Github, however we should note a couple of points that are often buggy. Consider the configuration:
require('lualine').setup {
options = {
icons_enabled = true,
theme="auto",
component_separators = { left="", right=""},
section_separators = { left="", right=""},
disabled_filetypes = {},
always_divide_middle = true,
globalstatus = false,
},
sections = {
lualine_c = {'filename', 'lsp_progress'},
lualine_y = {'progress'},
lualine_z = {'location'}
},
}
As we see here we first import plugin and then call the method setup
, which is wired in this plugin. The plugin options themselves are already described in Github, so we just need to specify them and customize them for ourselves.
Also, do not forget that in the configuration files strictly not recommended make settings not related to the plugin.
Let’s take a look at our directory structure now:
.
└── lua
├── base
│ ├── other.lua
│ ├── search.lua
│ └── tabs.lua
├── keys
│ ├── alias.lua
│ ├── main.lua
│ └── plugins.lua
└── plugins
├── <ваши файлы для конфигурации плагинов>
└── packer_install.lua
Here is a list of all the plugins that I use:
plugin | Description |
---|---|
nvim-lspconfig | A plugin for Neovim to support LSP, it allows you to enable auto-completion, syntax checking and much more |
fidget.nvim | Plugin to show LSP loading process |
trouble.nvim | Allows you to conveniently show all errors and warnings found by LSP |
nvim-cmp | Allows you to enable auto-completion in Neovim |
lspkind-nvim | Adds icons for autocomplete items |
symbols-outline.nvim | Adds a menu with objects from LSP to quickly switch between functions, methods and classes |
nvim-lsp-installer | Allows you to install LSP servers quickly and painlessly |
gitsigns.nvim | Git integration in Neovim |
gruvbox-material | Theme for Neovim |
nord-vim | Another theme in Neovim |
lualine.nvim | Adds a statusline at the bottom |
neo-tree.nvim | Adds explorer for Neovim |
commentary | Convenient code commenting in Neovim |
nvim-treesitter | Syntax highlighting in Neovim |
nvim-autopairs | Auto-complete any brackets, quotes, and so on |
telescope.nvim | Fast and convenient search |
bufferline.nvim | Statusline with buffers at the top |
vim test | Testing directly from Neovim (Mocha, Jest, JUnit) |
Hotkeys for plugins
Finally, we need to talk about hotkeys for plugins. My file looks like this:
require('keys/plugins')
vim.g.mapleader=" " -- Используем Space, как клавишу для альтернативных хотекеев
-- LSP (все горячие клавиши начинаются с g), кроме ховера
nm('K', '<cmd>lua vim.lsp.buf.hover()<CR>' ) -- Ховер для объекта
nm('gf', '<cmd>lua vim.lsp.buf.formatting()<CR>') -- Форматировать документ
nm('ga', '<cmd>lua vim.lsp.buf.code_action()<CR>') -- Действия с кодом
nm('gR', '<cmd>lua vim.lsp.buf.rename()<CR>') -- Переименовать объект
-- Отркыть NvimTree
nm('<leader>v', '<cmd>NeoTreeRevealToggle<CR>')
-- Telescope
nm('gd', '<cmd>Telescope lsp_definitions<CR>') -- Объявления в LSP
nm('<leader>p', '<cmd>Telescope oldfiles<CR>') -- Просмотр недавних файлов
nm('<leader>o', '<cmd>Telescope git_files<CR>') -- Поиск файлов
nm('<leader>b', '<cmd>Telescope git_branches<CR>') -- Ветки в Git
nm('<leader>f', '<cmd>Telescope live_grep<CR>') -- Поиск строки
nm('<leader>q', '<cmd>Telescope buffers<CR>') -- Буфферы
-- Git
nm('<leader>gp', '<cmd>Gitsigns preview_hunk<CR>')
nm('<leader>gb', '<cmd>Gitsigns blame_line<CR>')
-- SymbolsOutline
nm('<leader>s', '<cmd>SymbolsOutline<CR>') -- Структура для файла
-- BufferLine
nm('ç', '<cmd>bd<CR>') -- Закрыть буффер
nm('≤', '<cmd>BufferLineCyclePrev<CR>') -- Перейти в предыдущий буффер
nm('≥', '<cmd>BufferLineCycleNext<CR>') -- Перейти в следующий буффер
nm('˘', '<cmd>BufferLineMoveNext<CR>') -- Закрыть буффер слева
nm('¯', '<cmd>BufferLineMovePrev<CR>') -- Закрыть буффер справа
-- Formatter
nm('<leader>l', '<cmd>Format<CR>')
-- Trouble
nm('<leader>x', '<cmd>TroubleToggle<CR>') -- Открыть меню с проблемами LSP
nm('gr', '<cmd>Trouble lsp_references<CR>') -- Референсы в LSP
As you can see here from the unusual only the first line. Neovim has a dedicated button for alternative keyboard shortcuts, I have this <Space>
. Wherever it is written <leader>
it’s enough just to keep in mind that this is Space.
The full configuration can be seen in this repository
That’s all for today, if you were interested in reading this article, you can look and public in telegram, there are a lot of interesting things about development. Have a good day!