Все началось с того, что у меня перестал работать tagbar. Плагин падал с ошибкой, якобы текущая моя версия Exuberant Ctags вовсе не Exuberant. Покопавшись немного в исходниках, я понял, что последняя внешняя команда завершалась с ошибкой, а v:shell_error выдавал -1, что говорит о том, судя по документации vim'a, что "the command could not be executed". Я не стал копать дальше и установил fzf. Fzf, как и ctrlp, позволяет проводить нечеткий поиск по файлам, тегам, буферам, ..., но в отличии от последнего, работает гораздо шустрее, однако, не без минусов. Приложение работает напрямую с терминалом и каждый раз затирает мне историю вводимых команд. Это также означает, что мы не можем отобразить результаты поиска в буфере (neovim, судя по некоторым скринкастам, может), например, справа от основного буфера, когда ищем нужный тег. В отличие от sublime, fzf не придает больший вес имени файла, из — за чего я часто получал в топе вовсе не те результаты, которые ожидал увидеть. Ко всему прочему, отсутствие полной свободы в настройке цветовой схемы, что в общем-то не слишком важно для обычного пользователя, но только не для меня, с моим повышенным вниманием к мелочам. Под свободой я понимаю, как минимум, разграничение цвета для обычного (нормального) текста и строки запроса.
Всё это подтолкнуло меня к написанию своего плагина, внешний вид которого напоминает стандартный просмотрщик директорий — netrw. Я опишу проблемы, с которыми сталкивался, и пути их решения, полагая, что этот опыт может быть кому-то полезен.
Vim script language
Прежде всего хотел бы провести небольшую экскурсию для тех, кто делает первые шаги в vim script. Переменные имеют префиксы, некоторые из них вы уже видели и писали самостоятельно. Обычно настройка плагина происходит с помощью глобальных переменных с префиксом g:. При написании своего плагина уместно использовать префикс s:, который делает переменные доступными только в пределах скрипта. Чтобы обратиться к аргументу функции, используют префикс a:. Переменные без префикса локальны для функции, в которой они были объявлены. Полный перечень префиксов можно посмотреть командой :help internal-variables.
Для управления буфера существуют две очень простых функции: getline и setline. С их помощью можно вставить результаты поиска в буфер или получить значение запроса. Я не буду останавливаться на описании каждой функции, поскольку из названия, зачастую, и так понятно, что она делает. Почти любое ключевое слово из этой статьи можно искать в документации, поэтому :help getline или :help setline, а для полной картины советую посмотреть :help function-list со списком всех функций, сгруппированных по разделам.
События
Vim предоставляет множество событий из коробки, однако, при написании собственного плагина, может возникнуть необходимость в создании своих собственных событий. К счастью, делается это очень просто.
// слушаем событие CustomEvent
autocmd User CustomEvent call ...
// если подписчиков нет, можно получить "No matching autocommands", так что возбуждаем событие только при наличии слушателей
if(exists("#User#CustomEvent"))
doautocmd User CustomEvent
endif
Автозагрузка
Всем функциям в моём плагине я задаю префикс finder#. Это встроенный механизм автозагрузки vim, который ищет в runtimepath нужный файл с таким же именем. Функция finder#call должна быть расположена в файле runtimepath/finder.vim, функция finder#files#index — в файле runtimepath/finder/files.vim. Затем, нужно добавить плагин в runtimepath.
set runtimepath+=path/to/plugin
Но лучше для этих целей использовать менеджер плагинов, например, vim-plug.
Составные команды
Часто возникает ситуация, когда команду нужно комбинировать из разных кусочков или просто вставить значение переменной. Для этих целей, в vim существует команда execute, которую часто удобно использовать с функцией printf.
execute printf('syntax match finderPrompt /^\%%%il.\{%i\}/', b:queryLine, len(b:prompt))
Начнем
Итак, всё, что нам нужно — это строка запроса и результат поиска. За пользовательский ввод в vim отвечает функция input, но, насколько мне известно, она не позволяет разместить строку ввода наверху, а это довольно важно, если речь идет о поиске по тегам, поскольку теги удобнее отображать в том порядке, в каком они представлены в файле. Более того, со временем я решил сделать похожую шапку, какую показывает netrw. Строку ввода нужно было реализовывать в буфере, тут и появляются первые трудности.
Запрос
Чтобы получить значение запроса, нам нужно знать строку, на которой находится поле ввода и смещение относительно подсказки, а также задать обработчик для события TextChangedI. Поскольку для любого, кто ранее программировал, не должно быть ничего сложного на данном этапе, код я опущу; добавлю лишь, что обработчик нужно вешать с атрибутом <buffer>.
autocmd TextChangedI <buffer> call ...
Prompt
Поскольку подсказка находится на той же строке, что и пользовательский ввод, нужно каким-то образом зафиксировать её. Для этих целей можно было бы очистить значение опции backspace, которая отвечает за поведение таких клавиш, как <BS> и <Del>. В частности, меня интересовали только eol и start. Eol разрешает удаление символа конца строки и соответственно слияние строк, start же разрешает удаление только того текста, что был введен после начала режима вставки. Выходило достаточно удобно и просто: я вставляю подсказку "Files> ", например, затем начинаю вводить текст и при удалении текста, подсказка оставалась на месте. Я, правда, не учел один момент — для работы такого плагина нужно достаточно много логики и выход в нормальный режим был обыденной практикой. Любой маппинг мог запросто начать новую "сессию" и текст, что был введен ранее, переставал удаляться. Мне всего-лишь нужно было нажать <Esc>, например:
inoremap <C-j> <Esc>:call ...
Пришлось создать маппинг для <BS> и удалять текст вручную.
inoremap <buffer><BS> <Esc>:call finder#backspace()<CR>
Появилось какое-то странное мерцание курсора, которое со временем стало жутко раздражать. Прошло немало времени, прежде чем я понял, что виною тому — переход в режим ввода команд (command-line mode), который мы обычно инициируем, нажимая :. В этот самый момент курсор, что находится над текстом, исчезает. Эффект мерцания тем сильнее, чем "тяжелее" вызываемая функция. Были попытки повесить обработчик на событие TextChangedI, который проверял текущую позицию курсора, и если курсор находился в опасной близости от подсказки, то нужно было всего-лишь забиндить <BS> делать ничего. К сожалению, иногда 1 символ всё же удалялся. Спустя какое-то время было найдено решение — атрибут <expr>.
map {lhs} {rhs}
Где {rhs} — валидное выражение (:help expression-syntax), результат которого вставляется в буфер. Особые клавиши, такие как <BS> или <C-h> должны обрамляться двойными кавычками и экранироваться символом \ (:help expr-quote).
inoremap <expr><buffer><BS> finder#canGoLeft() ? "\<BS>" : ""
inoremap <expr><buffer><C-h> finder#canGoLeft() ? "\<BS>" : ""
inoremap <expr><buffer><Del> col(".") == col("$") ? "" : "\<Del>"
inoremap <expr><buffer><Left> finder#canGoLeft() ? "\<Left>" : ""
Выход
Для того чтобы выйти из буфера можно забиндить <Esc>. Неприятный момент состоит в том, что некоторые комбинации клавиш начинаются с той же последовательности символов, что и <Esc>. Если войти в режим ввода и нажать <C-v>, затем любую из стрелочек, то можно увидеть ^[ в качестве префикса. Например, для стрелки "влево" терминал посылает ^[OD vim'у. Поэтому, когда нажимается любая стрелка или <S-Tab>, то vim выполнит действие, назначенное <Esc>, затем попытается интерпретировать остальные символы: для стрелки влево это будет вставка пустой строки вверху (O) и заглавного литерала "D" на этой же строке. Опция esckeys указывает на то, стоит ли ожидать поступления новых символов, если последовательность начинается с <Esc>, то есть с ^[, в режиме ввода. Казалось бы, то, что надо, но работает только в том случае, если мы не меняем поведение <Esc>.
Здесь могла быть ваша шутка, дорогой пользователь IDE.
Возможно, я что-то упустил, но не зря на различных ресурсах советуют не менять поведение этой клавиши. Если <S-Tab> не так и важен, то стрелочки неплохо бы забиндить на выбор следующего/предыдущего вхождения. Поэтому, вместо <Esc>, используем событие InsertLeave. Это влечет за собой новые проблемы. Как вызвать функцию, не выходя из режима ввода? Читая документацию, я наткнулся на один интересный момент — комбинацию <Ctrl-c>, которая выходит из режима вставки, не вызывая событие InsertLeave, но, что довольно странно, если <C-c> присутствует в маппинге, это не работает и InsertLeave таки всплывает. Бороздя по просторам интернета, было найдено решение, общий вид которого:
inoremap <BS> <C-r>=expr<CR>
Из документации следует, что это — expression register. Это именно то, что мне нужно было, за исключением того, что результат выражения вставлялся в буфер. Собственно на этом и построен весь плагин, поскольку все телодвижения происходят в режиме вставки. Дабы не возвращать в каждой функции пустую строку (если этого не сделать, функция вернет 0), я решил использовать посредника, который вызывает нужную функцию.
function! finder#call(fn, ...)
call call(a:fn, a:000)
return ""
endfunction
Пространство имен a: отвечает за доступ к аргументам функции, а переменная a:000 содержит список необязательных параметров. Поскольку теперь стало возможным писать логику приложения не выходя из режима ввода, можно было бы воспользоваться опцией backspace. Однако, как я позже узнал, сброс значения этой опции приводил в негодование delimitMate, из — за чего тот не мог нормально функционировать, и я решил оставить эти попытки.
Бэкенд
Всего-то ничего и у нас уже есть пачка бесполезных пикселей. Самое время добавить немного жизни нашему буферу. Так как vim script сложно назвать быстрым языком или языком, на котором приятно писать что-то сложное, я решил бэкенд написать на D. Поскольку нечеткий поиск мне лень реализовывать не нужен, это будет поиск с учетом точного вхождения, и я решил, что буду посимвольно сравнивать исходную строку с запросом пользователя, посчитав, что так будет гораздо быстрее, нежели использование регулярных выражений. Учитывая то, что у меня фактически было 4 режима: ^query, query, query$, ^query$, код выглядел немного не привлекательным. Увидев, что я написал, появилось желание всё удалить и производить поиск регулярками. Через время я понял, что написанное можно сделать стандартными средствами Unix и решил вернуться к использованию grep, мысли о котором у меня появлялись с самого начала, но которые я отбрасывал ввиду наличия "сложной" логики. Сложность же была в том, что мне нужно было искать по имени файла, сортировать по длине пути файла и выводить не исходную строку, а её индекс. Стоит отметить, что Unix'овый grep оказался раза в 4 быстрее std.regex, что в D.
Чтобы получить имя файла, можно воспользоваться программой basename, но, к сожалению, она не читает стандартный поток ввода и работает только непосредственно с параметрами. Можно также воспользоваться и sed 's!.*/!!', которая обрежет все до последнего /. Подойдет и встроенная функция vim'a — fnamemodify.
Сортировку я решил делать средствами vim'a, поскольку проще в плане реализации и создании собственных расширений. За сортировку отвечает функция sort, для которой потребуется написать comparator.
- Чтобы вывести индекс, можно воспользоваться флагом -n в grep, который выводит номер строки, формат которой n:line и распарсить которую не составляет труда.
Мерцание курсора
Вообще, это довольно ненавистная мною вещь. Мерцание курсора можно увидеть, выставив опцию incsearch. Просто попробуйте поискать что-нибудь в буфере и следите за курсором, пока печатаете. Если с изменением поведения <BS> всё ясно, то писать <expr> повсюду, как оказалось, нельзя. Этот флаг запрещает изменение каких-либо строк, отличных от той, на которой находится курсор. Поэтому для остальной логики используется вышеупомянутый expression register, который подобно :, убирает курсор с текущей позиции на время выполнения выражения. Поскольку поиск по нескольким тысячам файлам занимает какое-то время, возникает эффект мигания курсора при печатании каждого символа. Должен сказать, что неблокируемый vim подоспел весьма вовремя, а конкретно функция timer_start. Когда буфер стал отрисовываться асинхронно, проблема ушла. Не лучшее решение, должен сказать, но ничего более подходящего не нашел. Это единственная причина, почему плагин требует vim 8-ой версии.
В третий раз проблема настигла, когда пришлось делать превью. Курсор мигал в тот момент, когда менялась позиция курсора в одном из буферов и происходила отрисовка экрана. Боюсь, тут без извращений в духе: "подсвечивать синтаксисом символ под курсором" не обойтись и я решил оставить подобные махинации до лучших времен.
Заметаем следы
Так как мы постоянно меняем содержимое буфера, tabline сообщит нам, что буфер изменен, а работа в режиме ввода будет сопровождаться соответствующей надписью слева внизу. Не знаю как вам, но мне нравится минималистичный дизайн, и подобные вещи я хотел бы убрать. Также, было бы неплохо скрывать ruler и statusline. Чтобы vim не отслеживал изменения в буфере, можно использовать опцию buftype.
setlocal buftype=nofile
Со статусной строкой, линейкой и надписью -- INSERT --
немного сложнее, поскольку опции, которые отвечают за их отображение, глобальны, а значит, нам нужно восстанавливать прежнее значение при выходе из буфера. Для этого удобно слушать событие OptionSet.
set noshowmode
set laststatus=0
set rulerformat=%0(%)
redraw
Вместо rulerformat можно было бы использовать noruler, но последний требует перерисовки экрана с предварительной очисткой (redraw!), что вызывает неприятный для глаза эффект.
Синтаксис
Здесь я хотел бы резюмировать наиболее важные моменты относительно синтаксиса, которые сыграли важную роль в работе плагина.
Элемент | Пример | Описание |
---|---|---|
\c | \c.* | Игнорировать регистр при поиске. |
.{-} | .{-}p | "Не жадный" аналог .* |
\zs, \ze | .{-}\zsfoo\ze.* | Начало и конец вхождения соответственно. |
\@=, \@<= | \(hidden\)\@<=text | Так называемые zero-width atoms — вырезают предыдущий атом из вхождения. |
\%l | \%1l | Поиск на определенной строке. |
\& | p1\&p2\&.. | Оператор конъюнкции. |
Basename
Поскольку мы работаем с именем файла, нам нужно задать регион, ограничивающий подсветку вхождения.
\(^\|\%(\/\)\@<=\)[^\/]\+$
config/foobar.php
foobar.php
Теперь необходимо подсветить нужные символы. Для этих целей можно воспользоваться оператором конъюнкции \& (:help branch).
\(^\|\%(\/\)\@<=\)[^\/]\+$\&.\{-}f
config/foobar.php
foobar.php
Совет: так как это обычный pattern (:help pattern), то можно тестировать всё в отдельном буфере, нажав /.
Комментарии
В fzf есть полезная визуальная фича — наличие текста, который не влияет на результаты поиска, то есть комментарии. Поначалу я хотел использовать какой-то невидимый Unicode символ для обозначения начала комментария (пробел, по понятным причинам, не подходит), но позже наткнулся на полезное свойство для группы синтаксиса — conceal. Если вкратце, conceal скрывает любой текст, оставляя его в буфере. За поведение conceal отвечают две опции: conceallevel и concealcursor. При определенной настройке текст может и не скрываться, так что советую с ними ознакомиться. В моём плагине строки имеют следующий вид:
text#finderendline...
где ... — необязательный комментарий, а #finderendline — скрывается. Пример скрытого текста:
syntax match hidden /pattern/ conceal
Прокрутка
Работа плагина в режиме ввода доставляет немало проблем, одна из которых — прокрутка. Поскольку курсор нужен в месте ввода запроса, двигать его, чтобы подсветить нужную строку, мы не можем. Для того, чтобы перемещаться по результатам поиска, можно использовать синтаксис, создав соответствующую группу. Ordinary atom \%l подходит как нельзя лучше. К примеру, \^%2l.*$ выделит вторую строку.
Мой экран вмещает 63 строки текста, и, так как вхождений может быть намного больше, возникает вопрос, как добраться до 64-ой и последующих строк. Поскольку в видимой части экрана всегда должны находиться шапка и строка запроса, при приближении к концу экрана, мы будем вырезать (помещать во временный массив) первое (второе, третье, ...) вхождение до тех пор, пока не дойдем до конца. При движении вверх — всё с точностью до наоборот.
Резюме
Наличием данной статьи vim как бы намекает — нужно было использовать input, однако, когда всё уже позади, я рад, что пошёл нестандартным путем, и получил столь ценный опыт. На этом всё, информацию по установке, использованию и созданию собственных расширений можно найти в репозитории.
Полезные мелочи
Получив определенную базу после написания плагина, захотелось немного упростить себе жизнь.
Выход из режима вставки
Те, кто читал мою предыдущую статью, знают, что ранее я использовал sublime для редактирования текста и кода. Есть существенные отличия между sublime и vim в том, как они обрабатывают комбинации клавиш. Если sublime, при вводе комбинации, вставляет текст без задержки, то vim сперва ожидает определенное время, и лишь после вставляет нужный символ, если комбинация "обрывается". С самого начала использования vim-mode в целом и vim'а в частности, я использовал df для выхода из режима вставки. Это настолько вошло в привычку, что любые попытки переучивания на jj, например, не давали успеха. Каждый раз, печатая d и символ, отличный от f, я наблюдал неприятный рывок. Я решил повторить поведение из sublime.
let g:lastInsertedChar = ''
function! LeaveInsertMode()
let reltime = reltime()
let timePressed = reltime[0] * 1000 + reltime[1] / 1000
if(g:lastInsertedChar == 'd' && v:char == 'f' && timePressed - g:lastTimePressed < 500)
let v:char = ''
call feedkeys("\<Esc>x")
endif
let g:lastInsertedChar = v:char
let g:lastTimePressed = timePressed
endfunction
autocmd InsertCharPre * call LeaveInsertMode()
Может это и не лучший код, но свою задачу он выполняет. Суть в следующем: после нажатия d, у нас есть пол секунды, чтобы нажать f. Если последнее истинно, f не печатается, а d удаляется из буфера. После чего редактор переходит в нормальный режим.
Read-only files
Остальным незначительным дополнением будет запрет на редактирование определенных файлов.
function! PHPVendorFiles()
let path = expand("%:p")
if(stridx(path, "/vendor/") != -1)
setlocal nomodifiable
endif
endfunction
autocmd Filetype php call PHPVendorFiles()
Данный код запрещает редактирование .php файла, если он находится в директории vendor.
Постскриптум
Список изменений моего окружения с момента публикации первой статьи.
- Перешел на XTerm, который по ощущению, раза в 2 быстрее gnome-terminal.
- Airline удален за ненадобностью.
- NERD Tree удален в пользу стандартного netrw.
- Vundle удален в пользу многопоточного vim-plug.
- CtrlP удален в пользу Finder.
- Tagbar
сломалсяудален в пользу Finder.
Комментарии (14)
psFitz
13.12.2016 21:07Сейчас все вещи из видео делаются быстрее в нормальной IDE.
daMage
13.12.2016 22:42Поясните, что вы имеете ввиду. Если есть что-то быстрее, чем
- Нажать триггер
- Ввести запрос
- Нажать ентер
то с радостью добавлю это в свой плагин.
psFitz
14.12.2016 17:36Например открытие файла по пути
Phpstorm > ctrl + shift + n будет быстрее, чем то, что я увидел на видео.daMage
15.12.2016 00:38Я намеренно отказался от горячих клавиш, так как на видео не видно, что нажимается. Ввод команды не только нагляднее, но и предоставляет базовый howto. О возможности забиндить команду на комбинацию клавиш не знает разве что совсем юный vim'ер.
ATwn
13.12.2016 23:50Весьма занимательно. Наверняка работать удобнее чем с :vimgrep. Возьму плагин и статью себе на заметку.
brooth
15.12.2016 09:55Зависимость от Vim8 — это фейл. Большинство давно уехало на Neovim.
daMage
15.12.2016 10:20Причина зависимости от vim 8 указана в статье. В коде всего пара вызовов timer_start, которые очень легко убрать, но что делать с курсором в такое случае — я не знаю. А где вы прослеживаете предпочтения сообщества?
brooth
15.12.2016 10:36+1В основном reddit, github. Активность на https://github.com/neovim/neovim. То же количесто звезд — 21К у неовим против 8К у вим. Такие монстры как Shougo переписыват свои плагины под nvim.
сорри, промазал окошком) это ответ на вопрос выше
vanburg
От полного перехода на (n)vim меня как раз останавливает vim script. (E)lisp как-то чище и роднее как таковой. Прочитал, вроде не все так страшно, осталось найти время на дальнейшее углубленное вкуривание. Сейчас пользуюсь Evil mode, есть большие вопросы к производительности (говорят, правда, что если тот же функциональный набор плагов повесить на вим, будет так же лагать)
poxu
Автор, отличная статья, спасибо!
Я тоже последнюю неделю интенсивно использую emacs из за org-mode. Думаю не перейти ли совсем. Так-то это, конечно, невозможно, но с появлением evil-mode (нормального текстового редактора) всё меняется.
Как же вы будете теперь писать статьи про Dwarf fortress? ;)
brooth
VimScript не самый лучший язык, префиксы переменных и функций главный wtf per minute, но писать плагины и тем более конфиги на нем в полне можно. А вот elisp для меня, это какое то безумие. Везде эти скобки, какие то названия типа setq, конструкции типа (+ 1 2). Скажу честно, дальше бездумного копи-паста я не осилил.
daMage
В Lisp оператор пишется слева, а операнды — справа. Всего лишь дело привычки. Зато писать выражения вида:
намного приятнее)
daMage
А что вы имеете ввиду под производительностью? Скорость запуска редактора или выполнение каких-то операций? Последнее как-то не замечаешь, а для первого придумали autoload.
vanburg
Даже такие базовые вещи как построчный скролл при синтакс хайлайте тупят. Конкретно для ruby очень подбешивает. Плагинов по минимуму.