История одного плагина
Все началось с того, что у меня перестал работать 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. Поскольку для любого, кто ранее программировал, не должно быть ничего сложного на данном этапе, код я опущу; добавлю лишь, что обработчик нужно вешать с атрибутом
autocmd TextChangedI call ...
Prompt
Поскольку подсказка находится на той же строке, что и пользовательский ввод, нужно каким-то образом зафиксировать её. Для этих целей можно было бы очистить значение опции backspace, которая отвечает за поведение таких клавиш, как . В частности, меня интересовали только eol и start. Eol разрешает удаление символа конца строки и соответственно слияние строк, start же разрешает удаление только того текста, что был введен после начала режима вставки. Выходило достаточно удобно и просто: я вставляю подсказку «Files> », например, затем начинаю вводить текст и при удалении текста, подсказка оставалась на месте. Я, правда, не учел один момент — для работы такого плагина нужно достаточно много логики и выход в нормальный режим был обыденной практикой. Любой маппинг мог запросто начать новую «сессию» и текст, что был введен ранее, переставал удаляться. Мне всего-лишь нужно было нажать
inoremap :call ...
Пришлось создать маппинг для
inoremap :call finder#backspace()
Появилось какое-то странное мерцание курсора, которое со временем стало жутко раздражать. Прошло немало времени, прежде чем я понял, что виною тому — переход в режим ввода команд (command-line mode), который мы обычно инициируем, нажимая : . В этот самый момент курсор, что находится над текстом, исчезает. Эффект мерцания тем сильнее, чем «тяжелее» вызываемая функция. Были попытки повесить обработчик на событие TextChangedI, который проверял текущую позицию курсора, и если курсор находился в опасной близости от подсказки, то нужно было всего-лишь забиндить
map {lhs} {rhs}
Где {rhs} — валидное выражение (: help expression-syntax), результат которого вставляется в буфер. Особые клавиши, такие как
inoremap finder#canGoLeft() ? "\" : ""
inoremap finder#canGoLeft() ? "\" : ""
inoremap col(".") == col("$") ? "" : "\"
inoremap finder#canGoLeft() ? "\" : ""
Выход
Для того чтобы выйти из буфера можно забиндить
Здесь могла быть ваша шутка, дорогой пользователь IDE.
Возможно, я что-то упустил, но не зря на различных ресурсах советуют не менять поведение этой клавиши. Если
inoremap =expr
Из документации следует, что это — 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. Просто попробуйте поискать что-нибудь в буфере и следите за курсором, пока печатаете. Если с изменением поведения
В третий раз проблема настигла, когда пришлось делать превью. Курсор мигал в тот момент, когда менялась позиция курсора в одном из буферов и происходила отрисовка экрана. Боюсь, тут без извращений в духе: «подсвечивать синтаксисом символ под курсором» не обойтись и я решил оставить подобные махинации до лучших времен.
Заметаем следы
Так как мы постоянно меняем содержимое буфера, 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("\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.
Комментарии (1)
13 декабря 2016 в 05:31
0↑
↓
От полного перехода на (n)vim меня как раз останавливает vim script. (E)lisp как-то чище и роднее как таковой. Прочитал, вроде не все так страшно, осталось найти время на дальнейшее углубленное вкуривание. Сейчас пользуюсь Evil mode, есть большие вопросы к производительности (говорят, правда, что если тот же функциональный набор плагов повесить на вим, будет так же лагать)