Pull to refresh

Некоторые особенности VimL

Reading time13 min
Views7.4K

В этой статье я хочу рассказать о некоторых особенностях VimL, зачастую неочевидных, которые надо знать человеку, желающему написать хорошее дополнение для Vim. Для понимания статьи требуется знание vimscript и рекомендуется наличие как минимум одного написанного дополнения. Людям, не желающим написать своё собственное дополнение статья будет, по большей части, бесполезна.

Привязки

Перепривязки


Начнём с банальных и общеизвестных вещей: перепривязки. В Vim есть два основных семейства команд: *map и *noremap. Первая позволяет переопределять свою правую часть, вторая нет. В дополнениях должна использоваться только вторая, так как неизвестно, какие привязки имеются у пользователя. Классический пример:
noremap : ;
noremap ; :
сломает
nmap <F4> :PluginToggle<CR>
, но не
nnoremap <F4> :PluginToggle<CR>
. Другие команды, о которых надо помнить:
  • *map/*noremap, *abbrev/*noreabbrev, *menu/*noremenu
  • normal/normal!
  • call feedkeys(string)/call feedkeys(string, 'n')

Также можно отметить наличие настройки 'remap', которая заставляет все команды вида *map вести себя так же, как и их *noremap эквиваленты. Вы не сможете использовать
nnoremap <Plug>PluginAction :DoAction<CR>
if !hasmapto('<Plug>PluginAction')
    nmap <Leader>a <Plug>PluginAction
endif
для предоставления пользователям возможности переопределения lhs и также поддерживать пользователей, включивших эту настройку, вместо этого вам придётся привести все определения привязок к виду
execute 'nnoremap '.get(g:, 'plugin_action_key', '<Leader>a').' :DoAction<CR>'
или более совместимому со старыми версиями Vim
if !exists('g:plugin_action_key')
    let g:plugin_action_key='<Leader>a'
endif
execute 'nnoremap '.g:plugin_action_key.' :DoAction<CR>'

Специальные символы


  • Использование <Esc> в качестве левой части привязки легко сделает невозможным применение стрелок и функциональных клавиш. Даже
    nmap <Esc> <Esc>
    уже страдает этим.
  • Наличие байта 0x80 в правой части привязки‐выражения может её испортить. Ошибка известна и документирована.

Файловые (<script>) привязки


Vim предоставляет возможность сделать допустимыми переопределение правой части привязок только теми привязками, которые определены в том же файле. Оставляя в стороне ограниченную полезность (точнее, отсутствие таковой) в случае, когда переопределение левой части привязки пользователем делается с помощью
nnoremap <Plug>PluginAction :DoAction<CR>
if !hasmapto('<Plug>PluginAction')
    nmap <Leader>a <Plug>PluginAction
endif
или
execute 'nnoremap '.get(g:, 'plugin_action_key', '<Leader>a').' :DoAction<CR>'
есть ещё одно соображение, почему этот способ не следует использовать: для команд
nnoremap <script> lhs rhs
и
nnoremap          lhs rhs
вызов maparg('lhs', 'n', 0, 1) вернёт один и тот же словарь. То есть, если другой разработчик хочет, скажем, временно сделать другую привязку с тем же lhs, а потом восстановить старую, то файловая привязка восстановится некорректно.

Настройки

В Vim есть множество настроек, которые легко испортят вам жизнь:

Настройки совместимости


Самой разрушительной настройкой является 'compatible'. Как правило, наиболее правильным путём справиться с ней является просто отказ от загрузки, возможно, с выводом сообщения:
if &compatible
    finish
endif
Второй по разрушительности влияния на дополнения является настройка 'cpoptions': с помощью неё можно запретить перенос части команды на следующую строку, изменить поведение команд, создающих привязки, изменить поведение регулярных выражений и сделать другие нехорошие вещи. Обычно с этим частично справляются с помощью
let s:saved_cpo=&cpo
set cpo&vim
<...>
let &cpo=s:saved_cpo
unlet s:saved_cpo
, но с изменением поведения регулярных выражений вы сделать ничего не сможете. Хорошо ещё, что оно не затрагивает функции (я имею ввиду встроенные функции вроде match*(), substitute(), а не пользовательские).

Игнорирование регистра


Ещё «забавной» настройкой является 'ignorecase'. Впрочем, в отличие от настроек совместимости, с ней достаточно легко справиться, следуя простым правилам:
  • При сравнении строк всегда используйте суффикс # (принимает регистр во внимание) или ? (соответственно, игнорирует его). Справка.
  • В функциях match(), matchend(), matchlist(), matchstr() и substitute(), при использовании регулярных выражений в команде :tag, а также при поиске с помощью / и ?, в том числе когда команда поиска является частью диапозона, следует явно указывать \C или \c.
    Вызовы :tag name придётся превратить в :tag /\V\C\^name\$ если вам есть дело до регистра тёгов.
  • В командах :s, :sm и :sno нужно указывать флаг i или I.
  • С действиями * и # вы ничего сделать не можете. Можно использовать вместо этого
    let @/='\V\C\<'.escape(expand('<cword>'), '\/').'\>’
    call histadd('/', @/)
    normal! n
    , а можно просто забить и забыть. Никогда не видел использование этого действия в дополнениях.
  • Одна хорошая новость: с командой :syntax ничего делать не надо.

Магия и повторения


  • Настройка 'magic' также влияет на регулярные выражения, но, к счастью, игнорируется в большинстве случаев. Всё, что надо делать, это указывать один из модификаторов \M, \m, \v или \V при поиске с помощью / и ?, в том числе когда команда поиска является частью диапозона, а также испольозовать :sm или :sno вместо :s.
  • Ещё одна настройка, влияющая на :s — это 'gdefault'. Она изменяет поведение флага g на прямо противоположное. Единственный способ справиться с ней — использовать
    let saved_gdefault=&gdefault
    set nogdefault
    " Do sm// commands here
    let &gdefault=saved_gdefault

Дикие файлы


При использовании expand(), glob() или globpath() следует указывать единицу в качестве первого из необязательных аргументов, в противном случае будут приняты во внимание настройки 'wildignore' и 'suffixes', что может исключить некоторые файлы из выдачи. Впрочем, иногда это, наоборот, полезно.

Другие настройки


  • Если вы привыкли жить с включённой 'hidden', помните, что она не стоит по умолчанию. Это может повлиять на способность вашего дополнения переключится на другой буфер. Для предотвращения этого можно использовать 'bufhidden', установленую в значение «hide».
  • 'autochdir' меняет текущий каталог, когда пользователь переходит в другой буфер. Иногда это неожиданно и ломает ваше дополнение.
  • 'cdpath' изменяет поведение команд :cd и :lcd, за исключение случаев, когда каталог задан абсолютным путём либо путём относительно текущего каталога (с явным ./) или каталога, в котором находится текущий (../).
  • 'revins' изменяет направление ввода. Из‐за неё вы не должны использовать
    execute 'normal! A'.text
    для ввода текста, используйте setline() или append() вместо этого. Если нужно добавление именно в конец строки, придётся использовать
    let lines=split(text, "\n", 1)
    let lines[0]=getline('.').lines[0]
    call setline('.', remove(lines, 0))
    if !empty(lines)
        call append('.', lines)
    endif
    . Перемещение курсора также придётся считать вручную. Вставка текста внутрь, как можно догадаться, добавит ещё несколько строк кода. Пример, не дёргающий курсор, можно посмотреть здесь.
  • 'selection', 'selectmode' и 'virtualedit' могут также испортить вам жизнь. Вряд ли здесь можно сделать что‐то лучшее, чем стандартное «сохранить настройку, установить своё значение, сделать дело, восстановить её обратно».
  • Изменение 'shell' также может легко повлиять на прочие shell* настройки, а, согласно документации, некоторые настройки не изменяются автоматически с изменением 'shell', если их изменяли явно ранее, так что лучше этого не делать, так как восстановить всё «как было» у вас не получится.
  • 'startofline' изменяет позицию курсора при совершении некоторых действий. Вам придётся явно задавать её после них, если это имеет значение.
  • 'wrapscan' изменяет поведение команд поиска / и ?, в том числе когда команда поиска является частью диапозона. Используйте search(), которой можно явно задать поведение или стандартное «сохранил, изменил, сделал, восстановил».

Локальные настройки


Также нельзя не отметить, что :setlocal не бросает исключение если вы пытаетесь изменить настройку, являющуюся исключительно глобальной. Таким образом, перед изменением любой настройки с её помощью будет не лишним заглянуть в документацию и затем либо отказаться от использования глобальных настроек, либо восстанавливать их по событию BufLeave, устанавливая своё значение по событию BufEnter.

Странные имена файлов

  • Функции expand(), glob() или globpath() на *nix системах не возвращают имена файлов, начинающиеся с точки. А при использовании конструкций вида glob('{*,.*}', 1, 1) вы в нагрузку получите специальные каталоги . (текущий каталог) и .. (каталог, в котором находится текущий), которые во множестве случаев надо просто проигнорировать.
  • За исключением globpath(), функции expand() и glob() теперь поддерживают второй дополнительный аргумент, заставляющий их возвращать результат в виде списка. Используйте его на *nix системах, потому что POSIX позволяет иметь новую строку в имени файла. Если vim достаточно стар, чтобы эти функции не имели дополнительного аргумента, а поддерживать такие файлы хочется (к сведению, «в дикой природе» я их никогда не видел), то вот пример, как надо извратиться, чтобы это сделать.

Разбиение на строки и комментарии

Значение символов новой строки и вертикальной черты, а также возможность использования комментариев сильно зависит от контекста:
  1. Если встроенная команда принимает в качестве аргумента выражение (например, :echo), то двойной штрих считается началом строкового литерала, новая строка и вертикальная черта отделяют две команды (но не в том случае, если они находятся внутри строкового литерала). Вы не сможете добиться такого эффекта для своих команд.
  2. Но не в том случае, если это привязка/сокращение‐выражение, они на самом деле не принимают выражения с точки зрения парсера.
  3. И не в том случае, если идёт чтение из файла/создание функции с помощью
    execute "function Abc()\n DoSomething\nendfunction"
    , здесь новая строка всегда разделяет две команды.
  4. Если команда считает вертикальную черту частью своих аргументов, то символ новой строки будет также частью её аргументов (за исключением случаев, описанных выше).
  5. Обратная косая черта в начале новой строки обозначает перенос части команды. Работает в точности будто перед исполнением файла мы пользуемся командой
    %s/\n\s*\\//
  6. Но только если происходит исполнение файла. Нигде более перенос применить не получится.
  7. Команды, создающие привязки/сокращения/меню, особенны: двойной штрих считается их частью, однако не начинает строковый литерал, а вертикальная черта, тем не менее, прерывает команды. Вы также не сможете добиться такого эффекта для своих команд.
  8. Вы не можете расположить endfunction не на отдельной строке. Парсер после команды function просто тупо жуёт все строки принадлежащие функции и сохраняет их в массив, пока не встретит команду endfunction, находящуюся на новой строке.

*Cmd

Создание правильных (Buf|File)(Read|Write)Cmd гораздо сложнее, чем кажется на первый взгляд. Дело в том, что vim не предоставляет ни автоматического определения кодировки или способа переноса строк, ни какой‐либо лёгкой поддержки ++opt. Если вы посмотрите на стандартное дополнение, читающее сжатые с помощью gzip файлы, то увидите, что оно использует сохранение расжатого содержимого во временный файл и затем :read, чтобы этот файл прочитать. Это избавляет дополнение от необходимости использовать настройки 'fileformats' и 'fileencodings' для угадывания способа переноса строк и кодировки, но открыть сжатый файл в кодировке KOI8-R, если fileencodings=utf8,cp1251 у вас нормально не получится, в отличие от сжатого файла в кодировке CP1251. Если вы не хотите, чтобы такое случалось с вашим дополнением, к вашим услугам есть v:cmdarg. Данная переменная всегда содержит данные пользователем ++настройки в кратком виде, так что поддерживать ++enc и ++encoding вам не придётся. Пример можно найти здесь (чтение, следующая функция — запись) (правда, здесь игнорируются 'fileformats' и 'fileencodings'), но в некоторых случаях v:cmdarg можно просто соединить с :read с помощью :execute. Учтите, что :read игнорирует ++настройки при чтении вывода команд оболочки.

Нефатальная ошибка

Человеку, только прочитавшему список команд может показаться, что команда :echoerr есть хороший способ отобразить ошибку. На самом деле это не так: нет никакого способа сделать так, чтобы отображённая ошибка не прервала выполнение программы. Можно сделать так, чтобы данная команда гарантировано прервала выполнение программы, но если вместо
try
    echoerr 'Error'
endtry
вы напишете просто
echoerr 'Error'
" some code here
готовьтесь к тому, что вам придётся отлаживать странные проблемы, связанные с тем, что «some code» то исполняется, то нет — в зависимости от того, поместили ли вы код внутрь блока :try. Учитывая, что внутрь блока :try нельзя поместить только код, который никогда не будет выполнен, а пользователь может иметь не одну причину поместить именно туда именно ваш код, написав :echoerr вы просто зря усложнили себе жизнь.
Если нужно отобразить ошибку, не прерывая программу, используйте
echohl ErrorMsg
echomsg 'Error'
echohl None
. Если надо прервать выполнение, есть :throw. И только если вас совсем не устраивает сообщение об ошибке, выдаваемое :throw, есть
try
    echoerr 'Error'
endtry
. Просто :echoerr нет, вы должны его забыть.

Не совсем текстовые файлы

Работа с файлами и выводом команд, могущими содержать нули или не оканчиваться на новую строку, в Vim может доставить вам немало «приятных» минут, если вам надо поддерживать её корректность. Вот несколько фактов:
  • В VimL не существует абсолютно никакого способа запустить команду не из оболочки. Это значит, что работа вашего дополнения будет всегда зависить от корректности настроек, начинающихся на shell (или от наличия у пользователя сборки, поддерживающей другие языки программирования).
  • system() официально не поддерживает наличие новой строки в своих аргументах.
  • Единственный способ получить вывод команды полностью и неизменённым на чистом VimL — это запись вывода команды во времменый файл и чтение его с помощью readfile() с дополнительным аргументом 'b'. При этом рекомендуется использовать значение настройки 'shellredir', чтобы иметь бо́льшие шансы заработать на другом компьютере. Пример.
  • Не существует никакого способа записать нулевой байт в регистр, за исключением записи его в буфер и копирования оттуда. Можете провести эксперимент:
    enew
    call setline('.', "a\nb")
    yank
    put
    call setreg('"', getreg('"'), getregtype('"'))
    put
    (смотреть здесь, чтобы понять, почему я взял "\n"). Вот что вы увидите:
    a^@b
    a^@b
    a
    b
  • :read, казалось бы, правильно читает нули, но вы никогда не сможете сказать, была ли в конце вывода новая строка при её использовании. И вам придётся установить 'binary', чтобы :read не считала себя слишком умной и не жевала \r перед \n.

Неравенство

В Vim есть шесть операторов, позволяющих проверить равенство и столько же операторов, проверяющих неравенство:
  • ==, ==?, ==# (неравентсво: !=*). Скалярные типы сравнивают как есть, приводя в случае необходимости (если аргументы имеют разный тип) строку к целому, а целое к числу с плавающей точкой. При сравнении строки с числом с плавающей точкой строка будет сначала приведена к целому, затем целое к числу с плавающей точкой. Поэтому "42"==42.0, но "42.1"!=42.1, а "42.1"==42.0.
    Нескалярные типы данный оператор обходит рекурсивно.
    Сравнение ссылки на функцию, списка и словаря с чем‐либо другого типа выбрасывает ошибку (превращающуюся в исключение в блоке :try).
  • is, is?, is# (неравество: isnot*). При сравнении скалярных типов действует так же, как и type(a)==type(b) && a==b (с соответствующим суффиксом, естественно). При сравнении нескалярных типов (ссылка на функцию — тоже скалярный тип) сначала проверяет тип, затем идентичность (то есть, ссылаются ли аргументы на один и тот же объект в памяти), аналогично оператору is в Python. Никогда не выбрасывает ошибку и никогда не приводит аргументы к другому типу.

Соответственно рекомендуемые правила использования:
  1. При любых сравнениях скалярных типов следует использовать is#, is?, isnot# или isnot? (разница описана в разделе «Настройки/Игнорирование регистра»).
  2. При любых сравнениях нескалярных типов, если необходимо выяснить равенство значения, а не идентичность, следует использовать ==#, ==?, !=# или !=?.
  3. Про операторы ==, is, != и isnot следует забыть вовсе.

Есть и другой набор правил, которых придерживаюсь я и которые позволяют сократить количество символов для набора:
  1. Если один из аргументов — числовая постоянная, а другой далее в коде используется только как число, можно использовать ==.
  2. Если один из аргументов — числовая постоянная, можно использовать is.
  3. Далее использовать правила из предыдущего списка. ==# для строк мною не применяется поскольку я не хочу думать, нужно ли (будет) мне использовать число в качестве «специального аргумента» (как 0 вместо None).

Функции

Одной из интересных особенностей Vim является его работа с переменными, ссылающимися на функции. Еслы вы уже слышали, что можно получить такую переменую с помощью
let Func=function("tr")
, то, наверное, также знаете, что имя переменной начинается с большой буквы, потому что иначе Vim покажет ошибку. Но есть другой, менее известный, факт, из‐за которого вы не должны никогда присваивать переменной ссылку на функцию: если бы кто‐то где‐то определил функцию «Func», то Vim тоже показал бы ошибку. Есть только два безопасных способа использовать ссылку на функцию: передать её в качестве аргумента и использовать сложные структуры: словарь или список:
let d={}
let d.func=function("tr")
, а также
function Apply(func, list)
    return call(a:func, a:list, {})
endfunction
echo Apply(function("tr"), ["abc", "a", "d"])
полностью безопасны, даже не смотря на возможность определения функции a:func().

Спецсимволы!

Вы видели и вам показалось удобным использование символа % вместо имени файла, например, здесь:
nnoremap <F4> :!python %<CR>
? Это ещё одна фишка Vim, которая могла бы быть удобной… если бы нормально работала. Стоит в имени файла оказаться пробелу/штриху/доллару (*sh) и вместо имени файла интерпретатор в данной привязке может получить что угодно. Vim имеет множество модификаторов к текущему имени файла вроде %:t[ail] (оставляет только последную часть имени), но модификатора, %:E[scape], который бы прогонял имя файла через shellescape(), среди них нет. Поэтому вам придётся всё экранировать самому, при этом не забывая о контексте вызова: при использовании system() shellescape() должна быть вызвана с одним аргументом либо с нулём вместо второго, а при использовании :!, :read !, :write ! и прочих восклицательных знаков — с двумя аргументами и единицей в качестве второго. Примеры:
nnoremap <F4> :execute '!python' shellescape(@%, 1)<CR>
nnoremap <F5> :call system('javac '.shellescape(expand('%')))<CR>

Особенности регулярных выражений

  • [^\na] — знакомая запись? В регулярных выражениях VimL она означает совершенно не то, что вы думаете: данная коллекция соответствует любому символу, а также новой строке. Дело в том, что данная запись означает то же, что и менее обескураживающая \_[^a]: то есть, добавление новой строки к коллекции, в данном случае, состоящей из всех символов, кроме a.
  • substitute(str, reg, '\=expr', flags) может заменить регулярное выражение на результат вычисления expr, а может — на =expr. Рекурсивное использование \= невозможно, но в случае, если вы всё‐таки попытаетесь его выполнить, Vim не выбросит ошибку. То же самое касается substitute() внутри :s.
  • ^ означает «начало строки» только в начале регулярного выражения, группы (\(\), \%(\)) либо ветки (\|). То же самое касается $, но речь уже идёт не о начале, а о конце.
  • Более неприятной вещью является то, что при применении упомянутых выше атомов к строке, а не к буферу (то есть, с помощью match*(), substitute() или =~*) они изменяют своё значение на «начало/конец текста». Точнее они не изменяют своё значение, просто всё выражение считается одной строкой. Что изменяет своё значение — это \n, теперь этот атом соответствует не «концу строки», а «символу новой строки». Разница есть: как обсуждалось выше, в буфере и ряде функций символ новой строки заменяет нулевой байт. Внутри Vim «конец строки» — реальный нулевой байт в конце char*.

Автонедополнение

Vim имеет множество вариантов автодополнения, которые вы можете использовать в своих командах. Кроме одного: файлы и каталоги. Конечно, если заглянуть в справку, можно увидеть в списке -complete=dir и -complete=file, однако эти аргументы имеют с автодополнением мало общего:
command -complete=dir -nargs=1 -bar Echo :echo [<f-args>]
command               -nargs=1 EchoN :echo [<f-args>]
:Echo a b c
E172: Разрешено использовать только одно имя файла
:EchoN a b c
['a b c']
:Echo *
E77: Слишком много имён файлов
:Echo $HOME
['/home/zyx']
:Echo `date`
['Пт. сент. 28 17:17:47 MSK 2012']
. Согласитесь, немного не то, что вы ожидали, настраивая автодополнение. Особенно, если команда должна принимать именно шаблоны. Более того, это не описано в документации. И не происходит с другими вариантами автодополнения.
Tags:
Hubs:
+22
Comments3

Articles