Чем особенно хорош Vim/Neovim? Тем, что твой инструментарий — это не только редактор (который сам по себе сильно расширяем плагинами и имеет богатый базовый функционал и очень гибок в области кастомизации), но и всё ваше рабочее окружение, со всем юникс-вейным прилагающимся инструментарием из gnu/coreutils и не только. Можно не уходя из редактора взять любую программу или интерпретатор ЯП и использовать его прямо в редакторе.


Предисловие


Этот пост писался на скорую руку для приватного круга лиц, но я решил что его вполне можно запостить и на Хабр. Для кого-то может стать вдохновением, кому-то поможет лучше понять филосовию Vim, а кто-то приметит для себя пару трюков. Сразу на всякий случай оговорюсь, что не следует ожидать, что я стану кому-то что-то доказывать в комментариях, например убеждать что вам нужно определённо бросить вашу разжиревшую IDE и начать пользоваться Vim-ом, мне это совершенно не интересно.


К делу


Вот к примеру возьмём такой кусок кода (из конфига Haskell проекта), список зависимостей пакета (пример в вакууме):


  build-depends:
      X11
    , base
    , directory
    , extra
    , GLFW-b
    , safe
    , aeson
    , containers
    , data-default
    , text
    , process
    , time
    , dbus

Что мы хотим?


  1. Отсортировать зависимости по алфавиту, по-возрастанию
  2. Отсортировать регистро-независимо (X11 и GLFW-b не должны уходить вверх над всем)
  3. Восстановить запятые (aeson уйдёт в самый вверх и у него уже не должно быть запятой слева, а вот у X11 должна добавиться запятая слева)
  4. Восстановить отступы (чтобы можно было и в другом конфиге с другим уровнем вложенности просто достать команду из истории и переиспользовать её, или вообще забиндить команду на хоткей в конфиге Vim-а)

Решение


В первую очередь выделим (визуальным выделением) список зависимостей кроме первой строки build-depends. Можно конечно просто нажать V (визуальный режим с построчным выделением) и через jk или стрелочками вверх-вниз выделить нужные строки. В своём случае я это делаю одним взмахом руки с помощью кастомного хоткея для визуального режима:


xn iz <esc>[zV]z

Находясь например в середине списка зависимостей я просто жму viz и уменя выделены все зависимости, т.к. выделен весь fold, который в свою очередь — текущий блок вложенности (т.к. foldmethod у меня задан как indent). Но можно и вручную набирать последовательно [zV]z без кастомного хоткея ([z прыгает в начало fold-а, а ]z в конец), но т.к. для меня такая операция часто-употребляемая, то я укоротил её до viz — тут нет модификаторов вроде шифта и прожимается на рефлексах в одно мгновение (наиболее близкий стандартный аналог — vip для выделения блока до ближайших пустых строк).


Далее жмётся : (двоеточие) для перехода в командный режим для выполнения команды относительно текущего визуального выделения. По сути обычный командный режим, но с дописанными сразу маркерами выделения, т.е. будет выглядеть как :'<,'> где '<,'> — это диапазон выделения, где '< — первая строка визуального выделения, а '> — последняя.


После нажимаем ! (восклицательный знак) на клавиатуре, это будет означать что всё, что идёт дальше — это shell/bash (в зависимости от настроек) команда. Будет выглядеть как :'<,'>!. На самом деле после выделения можно сразу нажать ! и получим тот же результат — :'<,'>!.


Данная операция перенаправит выделенные строки в STDIN команды и заменит выделенные строки на STDOUT выхлоп от этой команды. Для примера можно использовать команду sort, чисто для проверки, результат пока не тот, что нам нужен — '<,'>!sort и жмём Enter, получим:


  build-depends:
    , aeson
    , base
    , containers
    , data-default
    , dbus
    , directory
    , extra
    , GLFW-b
    , process
    , safe
    , text
    , time
      X11

Способ с coreutils и вообще башем


Восстановим предыдущее выделение (можно нажать gv для восстановления последнего выделения) и нажмём ! и далее стрелку вверх — это восстановит из истории последнюю команду, таким образом нам не надо писать заново, просто достаём из истории предыдущую команду и изменяем её. Для более комфортного редактирования команды можно нажать Ctrl+f — это откроет доп. окно с нормальлным стандартным редактированием команды, со всеми возможностями Vim-а, кстати там будут видны и все предудщие команды из истории в качестве отдельных строк, которые также можно выбрать, отредактировать и выполнить.


Как тут правильно поступить — можно выдумать находу, мой же поинт такой: сначала удаляем запятые, сортируем без них (регистро-независимо), потом возвращаем запятые, кроме самой первой строки.


Сначала удаляем запятые (а у первой строки доп. отступ, чтобы у всех строк был одинаковый отступ) используя команду sed с регулярным выражением ([, ] — запятая или пробел, а затем ещё пробел, \(\w\) экранированные скобки для выделения блока для подстановки, чтобы он потом был доступен как \1, \w — первый буквенный символ, в замене мы востанавливаем буквенный символ подстановкой \1):


:'<,'>!sed 's/[, ] \(\w\)/\1/'

Получим следующее:


  build-depends:
    X11
    base
    directory
    extra
    GLFW-b
    safe
    aeson
    containers
    data-default
    text
    process
    time
    dbus

Далее пайпим (через символ | — это фича баша) в команду сортировки sort передавая ключ -f для регистро-нечувствительности:


:'<,'>!sed 's/[, ] \(\w\)/\1/' | sort -f

Получаем:


  build-depends:
    aeson
    base
    containers
    data-default
    dbus
    directory
    extra
    GLFW-b
    process
    safe
    text
    time
    X11

Почти готово! Осталось только добавить запятые, а первой строке — пару пробелов. Воспользуемся всё тем же sed, в синтаксисе его операций можно указывать строки и диапазоны строк (как и в самом Vim-е, синтаксис такой же, ну или почти такой же). Префикс 1 будет означать первую строку, 2,$ означает диапазон со 2-ой строки и до конца ($, как и ^ означает начало файла, по аналогии с такими же символами в регулярных выражениях, которые означают конец и начало строки). Будем использовать \w чтобы скипнуть отступ и сразу выделить первый буквенный символ: 1s/\w/ &/ — тут мы делаем замену для первой строки, восстанавливаем первый буквенный символ через & (по аналогии с \1, только & означает всё, что попало под регулярку целиком, в то время как \1 означает первый блок, завёрнутый в круглые экранированные скобки), добавив перед ним пару пробелов. Для остальных строк вместо двух пробелов добавим запятую + пробел следом: 2,$s/\w/, &/, целиком команда будет такой: sed -e '1s/\w/ &/' -e '2,$s/\w/, &/', — -e мы используем чтобы отделить 2 операции друг от друга. В Vim вся операция целиком будет выглядеть как:


:'<,'>!sed 's/[, ] \([^, ]\)/\1/' | sort -f | sed -e '1s/\w/  &/' -e '2,$s/\w/, &/'

Применяем и получаем:


  build-depends:
      aeson
    , base
    , containers
    , data-default
    , dbus
    , directory
    , extra
    , GLFW-b
    , process
    , safe
    , text
    , time
    , X11

Готово! Писать второй раз её уже не нужно, просто набираем первые несколько символов, напр: :'<,'>!se (фактически нужно нажать лишь !se), — и стрелкой вверх достаём нужную команду из истории. Так или иначе рекомендую почаще тренироваться писать такие штуки сходу. Таким образом вы как прокачаете навыки повседневной работы в bash, так и в самом Vim-е, т.к. по сути вы делаете тоже самое.


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


Используя сторонний ЯП


Вместо запуска чего-то из coreutils можно запустить интерпретатор какого-нибудь удобного для вас ЯП, мне вот нравится такие штуки делать через Perl6 (он же недавно переименовался в Raku):


:'<,'>!perl6 -e 'my @x=lines.map(*.subst(/<[,\s]>\s(\w)/,{$0})).sort(*.lc); @x.shift.subst(/\w/,{q/  /~$_}).say; .subst(/\w/,{q/, /~$_}).say for @x'

Да хоть на жопоскрипте (node.js):


:'<,'>!node -e 'let fs=require("fs"), x=fs.readFileSync(process.stdin.fd).toString().replace(/\n$/,'').split(/\n/).map(x=>x.replace(/[, ] (\w)/,"$1")).sort((a,b)=>a.toLowerCase().localeCompare(b.toLowerCase())); console.log(x.shift().replace(/(\w)/,"  $1")); process.stdout.write(x.map(x=>x.replace(/(\w)/,", $1")).join("\n"))'

Такое можно проделать и на VimL/Vimscript внутри самого Vim, без вызова внешних команд. Но этот пост не об этом.


Естественно, как уже можно было догадаться, вы можете легко сохранить свой скрипт в отдельный файл, или даже скомпилировать собственную программу, которая что-то берёт на вход в STDIN и выдаёт что-то обработанное в STDOUT и использовать это в Vim просто вызывая (что, опять же, можно назначить на хоткей):


:'<,'>!~/my-program-or-script

Таким образом, когда вы пишете код в Vim, в вашем распоряжении не только сам Vim, но и всё ваше рабочее окружение.


Один из самых простых примеров, отпреттифаить JSON-файл:


:%!jq

Всего несколько нажатий клавиш, зачем переизобретать AST-парсер и преттифаер для JSON для любого нового редактора/IDE/whatever, когда можно просто взять и пропустить файл через jq никуда не уходя из Vim-а? Я уж не говорю о том, что вы можете через jq таким образом обработать ваш большой JSON файл, никуда не уходя из Vim-а, найти например нужный ключ в дереве, отсортировать, оставить только нужные данные, etc.