Про Vim " Миграция на Neovim (Lua)

https://oir.mobi/uploads/posts/2019-12/thumbs/1575937316_10-17.jpg

Введение

Теоретически если вы решили пересесть с классического Vim на более современный его клон — Neovim — вам делать ничего особенного не надо. В файле ~/.config/nvim/init.vim прописать source ~/.vimrc ну и скачать или скопировать словари. Идея в том, что Neovim должен поддерживать все конфигурации Vim по умолчанию. Однако, если у вас установлено множество плагинов и разных к ним расширений, то с высокой вероятностью конфигурация загрузится с ошибками, предупреждениями и другими, не очень желательными нюансами. Да и вообще вся фишка, вся разница Neovim заключается в том, что он поддерживает настройки и плагины написанные на Lua вместо vimscript.

Lua — более современный интерпретируемый язык, на нем удобнее писать, его проще читать. А еще, считается, что работает интерпретатор на порядок быстрее родного языка. На счет порядка я бы засомневался, но действительно тяжелые плагины работают вроде как расторопнее и глаже. Впрочем и классический Vim известен не тем, что он медленный или глючный, так что тут спор скорее софистический. А вот с первыми тремя утверждениями я абсолютно согласен.

Более того для Neovim именно на Lua в последнее время выбор современных плагинов и расширений, что уж тут говорить, куда богаче. Вопрос даже не в том лучше ли эти аналоги, а в скорее в свежести, динамике развития и в целом в оптимизме сообщества. Очень похоже на то, что не сегодня так завтра Neovim повторит судьбу предшественника и займет свое место в распространенных дистрибутивах Linux в качестве стандартной замены устаревшему Vim. Заменит полностью? Ну, я бы не был так категоричен в этом вопросе, но вероятность такая существует.

С другой стороны, в философии Neovim нет вот этого старого аскетизма и предполагаемой сверхнадежности. Для работы в монопольном режиме в консоли сетевого устройства всё-таки наличие Neovim не предполагается. И разработчики подспудно рассчитывают на некий девайс, на котором можно обнаружить как комплект современных сред исполнения, так и подключение к интернету. Таким образом для настройки Neovim не стоит задача сохранить работоспособность в урезанном и минмалистском окружении.

В связи с этим в сети нынче найдется дюжина — не меньше, качественных сборных солянок — готовых настроек, установив которые вы из коробки получаете комбайн готовый к большинству сценариев использования. По сути это такие отдельные продукты с собственными брендами, со своим сообществом фанатов и постоянных пользователей. И если вы не любитель самостоятельно копаться в тонких настройках программ, то один из брендов скорее всего вам подойдет на 90%.

Однако, как вы понимаете, я отношусь к тем гражданам, которым остальные 10% нужны так что спасу нет. И если вы один из таких людей, которые готовы научить Neovim чему-то очень личному или нестандартному, то остальная часть статьи именно для вас.

Сброс

Поскольку изложенный материал это такой эксперимент над собой, о том насколько имеет смысл пройти весь путь от голого редактора до полноценной IDE, то я поступлю просто — начну с нуля. Карта того в каком направлении идти у меня уже есть — я просто повторю весь путь сделанный с оригинальным vimscript, но теперь под Lua.

Перед сбросом, однако, нужно немного подготовиться. Во-первых, я должен прикрепить список статей посвященных Vim ранее.

Вводные к циклу

Горячие клавиши

Режим вставки

Файлы и плагины

Встроенное

Клиент БД

Форматирование

PHP LSP

JDT LS

Во-вторых, я добавлю в окружение настройки привычных мне инструментов — tmux, zsh + oh-my-zh, какие-то алиасы на GitHub. Там в принципе почти всё стандартно, так что можете смело качать и их.

Pro Vim dotfiles

В-третьих, надо оговориться, что подход к упорядочению файлов конфигурации в Lua сильно отличается от подхода в vimscript. Тут всё по классике: модули, иерархия, вложенность. То есть сразу отказываемся от линейной структуры и раскладываем всё по полочкам.

Главным файлом — точкой входа при этом является ~/.config/nvim/init.lua. Создаем таковой если его нет и удаляем предыдущий ~/.config/nvim/init.vim, если он у вас был.

Основные настройки

Чтобы в будущем было проще управлять настройками рекомендуется поместить их в специальную директорию lua и развести основные настройки и настройки плагинов по разным директориям.

$ cd ~/.config/nvim
$ mkdir lua
$ mkdir lua/core lua/plugins

Можно разделить по смыслу и основные настройки по неким категориям.

$ cd lua/core
$ touch options.lua keymaps.lua colorscheme.lua

Это я у кого-то взял. Так делать вовсе необязательно, но выглядит разумно. Далее просто вставляем в ~/.config/nvim/init.lua (далее я буду ссылаться к нему как к «главному файлу») ссылки на эти файлы согласно синтаксиса и парадигмы Lua, где точки в строках интерпретируются как директории, а в конце добавляется расширение .lua.

require("core.options")
require("core.keymaps")
require("core.colorscheme")

Для программистов надо отметить, что синтаксис у Lua достаточно своеобразный и очень вариабельный, можно что-то одно делать десятью разными способами. Что иногда напрягает, когда что-то просто отказывается работать без видимой причины. А иногда помогает, когда что-то работает несмотря на небольшие опечатки. Скажем так, еще более либеральный JavaScript. Каждый файл при этом представляет из себя не макрос, а функцию с результатом исполнения. Для тех кому не чужд vimscript можно почитать здесь о том как быстро найти общий язык с Lua.

Options

В первую очередь я перенесу глобальные настройки в options.lua, так что бы дальше работать не в голом редакторе.

local opt = vim.opt

-- line number
opt.relativenumber = true
opt.number = true

-- tabs & indentation
opt.tabstop = 4 -- 4 spaces for tabs (prettier default)
opt.shiftwidth = 4 -- 4 spaces for indent width
opt.expandtab = true -- expand tab to spaces
opt.autoindent = true -- copy indent from current line when starting new one

-- line wrapping
opt.wrap = true -- disable line wrapping
opt.linebreak = true

-- search settings
opt.ignorecase = true -- ignore case when searching
opt.smartcase = true -- if you include mixed case in your search, assumes you want case-sensitive

-- cursor line
opt.cursorline = true -- highlight the current cursor line

-- appearance

-- turn on termguicolors for nightfly colorscheme to work
-- (have to use iterm2 or any other true color terminal)
opt.termguicolors = true
opt.background = "dark" -- colorschemes that can be light or dark will be made dark
opt.signcolumn = "yes" -- show sign column so that text doesn't shift

-- backspace
opt.backspace = "indent,eol,start" -- allow backspace on indent, end of line or insert mode start position

-- clipboard
opt.clipboard:append("unnamedplus") -- use system clipboard as default register

-- split windows
opt.splitright = true -- split vertical window to the right
opt.splitbelow = true -- split horizontal window to the bottom

opt.iskeyword:append("-") -- consider string-string as whole word

-- spelling
opt.spelllang = { "en_us", "ru" } -- Словари рус eng
opt.spell = true

-- redundancy
opt.undofile = true --  keep undo history between sessions
opt.undodir = "~/.vim/undo/" -- keep undo files out of file dir
opt.directory = "~/.vim/swp/" -- keep unsaved changes away from file dir
opt.backupdir = "~/.vim/backup/" -- backups also should not go to git

Пояснения к настройкам даны как в комментариях, так и в первой статье цикла. Не будем на них долго останавливаться. Добавлю лишь то что в Lua принято сокращать команды путем создания переменных псевдонимов тех или иных объектов. В данном случае это первая строчка в скрипте. А также программистам не лишним будет понимать, что оператор присвоения здесь на самом деле сокращение для функции vim.api.nvim_win_set_option. О чем можно прочитать в мануале, на GitHub или на страничке проекта Lua-guide.

Рестартуем или ресурсим.

:so $MYVIMRC

Colorscheme

В файл colorscheme.lua положим следующее:

-- Color control
vim.g.sonokai_style = "andromeda"
vim.g.sonokai_better_performance = 1

vim.g.sonokai_transparent_background = 1
vim.g.sonokai_diagnostic_text_highlight = 1

-- set colorscheme with protected call
-- in case it isn't installed
local status, _ = pcall(vim.cmd, "colorscheme sonokai")
if not status then
	print("Colorscheme not found, defaulting to builtin")
    vim.cmd([[colorscheme desert]])
	return
end

Почему нельзя было просто выполнить установку схемы? Да можно было. Этот кусок скорее для демонстрации. Здесь показано, например, что есть вариант вызвать команду в «безопасном» режиме и обработать отсутствие соответствующей цветовой схемы. Причем второй безусловный вызов встроенной схемы colorscheme desert выполнен в виде родной vimscript команды. То есть, если нет простого способа выполнить команду из Lua, то можно вот так воспользоваться синтаксисом с двумя квадратными скобками, который ко всему еще и многострочный.

Надо также понимать, что это всё реально конфигурация, а не прямое выполнение команд. Здесь мы задаем что будет потом выполнено при обращении к этой функции. То есть сперва конфигурация полностью формируется, а затем выполняется при вызове require.

Можно отдельно вызвать функцию из файла командой :luafile или :source:

:luafile ~/.config/nvim/lua/core/colorscheme.lua

Keymaps

Ну и наконец следует вернуть привычные клавосочетания.

-- set leader key to space
vim.g.mapleader = " "

local keymap = vim.keymap -- for conciseness

local cmd = vim.cmd

-- Русский язык

cmd("set keymap=russian-jcukenwin")
cmd("set iminsert=0")
cmd("set imsearch=0")

---------------------
-- General Keymaps
---------------------

-- soft wrap remap
local expr_opts = { noremap = true, expr = true, silent = true }
keymap.set({ "n", "v" }, "j", "v:count == 0 ? 'gj' : 'j'", expr_opts)
keymap.set({ "n", "v" }, "k", "v:count == 0 ? 'gk' : 'k'", expr_opts)
keymap.set({ "n", "v" }, "", "v:count == 0 ? 'gj' : 'j'", expr_opts)
keymap.set({ "n", "v" }, "", "v:count == 0 ? 'gk' : 'k'", expr_opts)

-- use jk to exit insert mode
keymap.set({ "v", "i" }, "\\'", "")
keymap.set({ "v", "i" }, "\\э", "")

-- unbind ins toggle
keymap.set("i", "", "")

-- yank and paste clipboard
--keymap.set({ "n", "v", "x", "t" }, "y", '"+y')
--keymap.set({ "n", "v", "x", "t" }, "Y", '"*y')
--keymap.set({ "n", "t" }, "p", '"+p')
--keymap.set({ "n", "t" }, "P", '"*p')

-- do not yank on replace or delete
--keymap.set({ "v", "x" }, "p", '"_d"+p')
--keymap.set({ "v", "x" }, "P", '"_d"*p')
--keymap.set({ "n", "v", "x", "t" }, "d", '"_d')

-- copy default reg to/from system/mouse clipboard
keymap.set({"n", "v", "x"}, 'y', ':let @+=@"')
keymap.set({"n", "v", "x"}, 'p', ':let @"=@+')
keymap.set({"n", "v", "x"}, 'Y', ':let @*=@"')
keymap.set({"n", "v", "x"}, 'P', ':let @"=@*')


-- clear search highlights
keymap.set("n", "nh", ":nohl")

-- delete single character without copying into register
keymap.set("n", "x", '"_x')

-- increment/decrement numbers
keymap.set("n", "+", "") -- increment
keymap.set("n", "-", "") -- decrement

-- navigate windows
keymap.set("n", "", "j")
keymap.set("n", "", "h")
keymap.set("n", "", "k")
keymap.set("n", "", "l")

-- move out of terminal
keymap.set("t", "", "j")
keymap.set("t", "", "k")
keymap.set("t", "", "h")
keymap.set("t", "", "l")

-- move line or v-block
keymap.set("i", "", "m .+1==gi")
keymap.set("i", "", "m .-2==gi")
keymap.set("x", "J", ":m '>+1gv-gv", { noremap = true, silent = true })
keymap.set("x", "K", ":m '<-2gv-gv", { noremap = true, silent = true })

-- stay in indent mode
keymap.set("v", ">", ">gv", { noremap = true, silent = true })
keymap.set("v", "<", "wv", "v") -- split window vertically
keymap.set("n", "wh", "s") -- split window horizontally
keymap.set("n", "we", "=") -- make split windows equal width & height
keymap.set("n", "wx", ":close") -- close current split window

-- tabs
keymap.set("n", "to", ":tabnew") -- open new tab
keymap.set("n", "tx", ":tabclose") -- close current tab
keymap.set("n", "tn", ":tabn") --  go to next tab
keymap.set("n", "tp", ":tabp") --  go to previous tab

-- buffers
keymap.set("n", "bo", ":new") -- open new tab
keymap.set("n", "bd", ":bdelete") -- close current tab
keymap.set("n", "bn", ":bn") --  go to next tab
keymap.set("n", "bp", ":bp") --  go to previous tab

-- shift arrow like gui
keymap.set("n", "", "v")
keymap.set("n", "", "v")
keymap.set("n", "", "v")
keymap.set("n", "", "v")
keymap.set("v", "", "")
keymap.set("v", "", "")
keymap.set("v", "", "")
keymap.set("v", "", "")
keymap.set("i", "", "v")
keymap.set("i", "", "v")
keymap.set("i", "", "v")
keymap.set("i", "", "v")

-- copy paste like gui
keymap.set("v", "", '"+yi')
keymap.set("v", "", '"+di')
keymap.set("i", "", '"+pi')
keymap.set("i", "", '"+pi', { noremap = true, silent = true })
keymap.set("i", "", "ui", { noremap = true, silent = true })
keymap.set("i", "", "ui", { noremap = true, silent = true })
keymap.set({ "i", "v", "x", "t" }, "", "ggVG", { noremap = true, silent = true })

----------------------
-- Plugin Keybinds
----------------------

Здесь я намеренно оставил некоторые комментарии. В конце заготовка для сочетаний плагинов. Обратите внимание, что теперь их можно поместить в одно место, в независимости от того где и когда подключается сам плагин. Также, если обратить внимание на секцию использования регистров, то тут я отключил популярный способ работы с системным буфером обмена и привел свою находку.

Отличается этот подход тем, что мы не трогаем поведение стандартных клавиш, а дополняем их операциями перемещения регистра в буфер и обратно. Таким образом не нарушаются движения связанные с использованием клавиш y и p, а работа с внешним буфером идет как дополнительная команда. На мой вкус такой подход оказался более интуитивным, хотя добавляется еще одно незначительное действие. Также оно позволяет затем сколько угодно раз перезаписывать регистры без затирания системного буфера при вставке в несколько мест одного и того же с перезаписью.

Пожалуй на сегодня хватит. Далее плагины.

© Habrahabr.ru