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 luathis 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.optso 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.git 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.luathey 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.luawe 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_keymaphowever, 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:

  1. nm – creates a hotkey in normal mode

  2. im – creates a hotkey in input mode

  3. vm – creates a hotkey in visual mode

  4. tm – 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 PackerSyncnow 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 usefollowed 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 configwhich 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.luaif 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:

  1. We always try to separate the configuration from the list of plugins.

  2. 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.luawe’re just through require() specify the path to the file.

How to set up plugins

For example, let’s take a plugin lualinelet’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!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *