- PVSM.RU - https://www.pvsm.ru -
В этой статье я хочу рассказать о некоторых особенностях VimL, зачастую неочевидных, которые надо знать человеку, желающему написать хорошее дополнение для Vim. Для понимания статьи требуется знание vimscript и рекомендуется наличие как минимум одного написанного дополнения. Людям, не желающим написать своё собственное дополнение статья будет, по большей части, бесполезна.
Начнём с банальных и общеизвестных вещей: перепривязки. В Vim есть два основных семейства команд: *map и *noremap. Первая позволяет переопределять свою правую часть, вторая нет. В дополнениях должна использоваться только вторая, так как неизвестно, какие привязки имеются у пользователя. Классический пример:
noremap : ;
noremap ; :
сломает
nmap <F4> :PluginToggle<CR>
, но не
nnoremap <F4> :PluginToggle<CR>
. Другие команды, о которых надо помнить:
*map/*noremap, *abbrev/*noreabbrev, *menu/*noremenunormal/normal!call feedkeys(string)/call feedkeys(string, 'n')
Также можно отметить наличие настройки 'remap' [1], которая заставляет все команды вида *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>'
nmap <Esc> <Esc>
уже страдает этим.
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' [3]. Как правило, наиболее правильным путём справиться с ней является просто отказ от загрузки, возможно, с выводом сообщения:
if &compatible
finish
endif
Второй по разрушительности влияния на дополнения является настройка 'cpoptions' [4]: с помощью неё можно [5] запретить перенос части команды на следующую строку [6], изменить [7] поведение [8] команд [9], создающих [10] привязки, изменить [11] поведение [12] регулярных выражений и сделать другие нехорошие вещи. Обычно с этим частично справляются с помощью
let s:saved_cpo=&cpo
set cpo&vim
<...>
let &cpo=s:saved_cpo
unlet s:saved_cpo
, но с изменением поведения регулярных выражений вы сделать ничего не сможете. Хорошо ещё, что оно не затрагивает функции (я имею ввиду встроенные функции вроде match*() [13], substitute() [14], а не пользовательские).
Ещё «забавной» настройкой является 'ignorecase' [15]. Впрочем, в отличие от настроек совместимости, с ней достаточно легко справиться, следуя простым правилам:
# (принимает регистр во внимание) или ? (соответственно, игнорирует его). Справка [16]./ [21] и ? [22], в том числе когда команда поиска является частью диапозона [23], следует явно указывать C или c [24]. :tag name придётся превратить в :tag /VC^name$ если вам есть дело до регистра тёгов. i или I.* [29] и # [30] вы ничего сделать не можете. Можно использовать вместо этого
let @/='VC<'.escape(expand('<cword>'), '/').'>’
call histadd('/', @/)
normal! n
, а можно просто забить и забыть. Никогда не видел использование этого действия в дополнениях.
M, m, v или V [33] при поиске с помощью / [21] и ? [22], в том числе когда команда поиска является частью диапозона [23], а также испольозовать :sm [26] или :sno [27] вместо :s [25].g на прямо противоположное. Единственный способ справиться с ней — использовать
let saved_gdefault=&gdefault
set nogdefault
" Do sm// commands here
let &gdefault=saved_gdefault
При использовании expand() [35], glob() [36] или globpath() [37] следует указывать единицу в качестве первого из необязательных аргументов, в противном случае будут приняты во внимание настройки 'wildignore' [38] и 'suffixes' [39], что может исключить некоторые файлы из выдачи. Впрочем, иногда это, наоборот, полезно.
./) или каталога, в котором находится текущий (../).execute 'normal! A'.text
для ввода текста, используйте setline() [47] или append() [48] вместо этого. Если нужно добавление именно в конец строки, придётся использовать
let lines=split(text, "n", 1)
let lines[0]=getline('.').lines[0]
call setline('.', remove(lines, 0))
if !empty(lines)
call append('.', lines)
endif
. Перемещение курсора также придётся считать вручную. Вставка текста внутрь, как можно догадаться, добавит ещё несколько строк кода. Пример, не дёргающий курсор, можно посмотреть здесь [49].
shell* настройки, а, согласно документации, некоторые настройки не изменяются автоматически с изменением 'shell' [53], если их изменяли явно ранее, так что лучше этого не делать, так как восстановить всё «как было» у вас не получится./ [21] и ? [22], в том числе когда команда поиска является частью диапозона [23]. Используйте search() [56], которой можно явно задать поведение или стандартное «сохранил, изменил, сделал, восстановил».Также нельзя не отметить, что :setlocal [57] не бросает исключение если вы пытаетесь изменить настройку, являющуюся исключительно глобальной. Таким образом, перед изменением любой настройки с её помощью будет не лишним заглянуть в документацию и затем либо отказаться от использования глобальных настроек, либо восстанавливать их по событию BufLeave [58], устанавливая своё значение по событию BufEnter [59].
glob('{*,.*}', 1, 1) вы в нагрузку получите специальные каталоги . (текущий каталог) и .. (каталог, в котором находится текущий), которые во множестве случаев надо просто проигнорировать.Значение символов новой строки и вертикальной черты, а также возможность использования комментариев сильно зависит от контекста:
execute "function Abc()n DoSomethingnendfunction"
, здесь новая строка всегда разделяет две команды.
%s/ns*\//
endfunction не на отдельной строке. Парсер после команды function просто тупо жуёт все строки принадлежащие функции и сохраняет их в массив, пока не встретит команду endfunction, находящуюся на новой строке.Создание правильных (Buf|File)(Read|Write)Cmd гораздо сложнее, чем кажется на первый взгляд. Дело в том, что vim не предоставляет ни автоматического определения кодировки или способа переноса строк, ни какой‐либо лёгкой поддержки ++opt. Если вы посмотрите на стандартное дополнение, читающее сжатые с помощью gzip файлы, то увидите, что оно использует сохранение расжатого содержимого во временный файл и затем :read [62], чтобы этот файл прочитать. Это избавляет дополнение от необходимости использовать настройки 'fileformats' [63] и 'fileencodings' [64] для угадывания способа переноса строк и кодировки, но открыть сжатый файл в кодировке KOI8-R, если fileencodings=utf8,cp1251 у вас нормально не получится, в отличие от сжатого файла в кодировке CP1251. Если вы не хотите, чтобы такое случалось с вашим дополнением, к вашим услугам есть v:cmdarg [65]. Данная переменная всегда содержит данные пользователем ++настройки в кратком виде, так что поддерживать ++enc и ++encoding вам не придётся. Пример можно найти здесь [66] (чтение, следующая функция — запись) (правда, здесь игнорируются 'fileformats' [63] и 'fileencodings' [67]), но в некоторых случаях v:cmdarg [65] можно просто соединить с :read [62] с помощью :execute [68]. Учтите, что :read [62] игнорирует ++настройки при чтении вывода команд оболочки.
Человеку, только прочитавшему список команд может показаться, что команда :echoerr [69] есть хороший способ отобразить ошибку. На самом деле это не так: нет никакого способа сделать так, чтобы отображённая ошибка не прервала выполнение программы. Можно сделать так, чтобы данная команда гарантировано прервала выполнение программы, но если вместо
try
echoerr 'Error'
endtry
вы напишете просто
echoerr 'Error'
" some code here
готовьтесь к тому, что вам придётся отлаживать странные проблемы, связанные с тем, что «some code» то исполняется, то нет — в зависимости от того, поместили ли вы код внутрь блока :try. Учитывая, что внутрь блока :try нельзя поместить только код, который никогда не будет выполнен, а пользователь может иметь не одну причину поместить именно туда именно ваш код, написав :echoerr вы просто зря усложнили себе жизнь.
Если нужно отобразить ошибку, не прерывая программу, используйте
echohl ErrorMsg
echomsg 'Error'
echohl None
. Если надо прервать выполнение, есть :throw [70]. И только если вас совсем не устраивает сообщение об ошибке, выдаваемое :throw, есть
try
echoerr 'Error'
endtry
. Просто :echoerr нет, вы должны его забыть.
Работа с файлами и выводом команд, могущими содержать нули или не оканчиваться на новую строку, в Vim может доставить вам немало «приятных» минут, если вам надо поддерживать её корректность. Вот несколько фактов:
shell (или от наличия у пользователя сборки, поддерживающей другие языки программирования).'b'. При этом рекомендуется использовать значение настройки 'shellredir' [73], чтобы иметь бо́льшие шансы заработать на другом компьютере. Пример [66].enew
call setline('.', "anb")
yank
put
call setreg('"', getreg('"'), getregtype('"'))
put
(смотреть здесь [74], чтобы понять, почему я взял "n"). Вот что вы увидите:
a^@b
a^@b
a
b
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. Никогда не выбрасывает ошибку и никогда не приводит аргументы к другому типу.Соответственно рекомендуемые правила использования:
is#, is?, isnot# или isnot? (разница описана в разделе «Настройки/Игнорирование регистра»).==#, ==?, !=# или !=?.==, is, != и isnot следует забыть вовсе.Есть и другой набор правил, которых придерживаюсь я и которые позволяют сократить количество символов для набора:
==.is.==# для строк мною не применяется поскольку я не хочу думать, нужно ли (будет) мне использовать число в качестве «специального аргумента» (как 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().
Автор: ZyXI
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/vim/13463
Ссылки в тексте:
[1] 'remap': http://vimdoc.sourceforge.net/htmldoc/options.html#'remap'
[2] документирована: http://vimdoc.sourceforge.net/htmldoc/map.html#:map-<expr>
[3] 'compatible': http://vimdoc.sourceforge.net/htmldoc/options.html#'compatible'
[4] 'cpoptions': http://vimdoc.sourceforge.net/htmldoc/options.html#'cpoptions'
[5] можно: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-C
[6] перенос части команды на следующую строку: http://vimdoc.sourceforge.net/htmldoc/repeat.html#line-continuation
[7] изменить: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-B
[8] поведение: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-k
[9] команд: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-<
[10] создающих: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-b
[11] изменить: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-%5C
[12] поведение: http://vimdoc.sourceforge.net/htmldoc/options.html#cpo-l
[13] match*(): http://vimdoc.sourceforge.net/htmldoc/eval.html#match%28%29
[14] substitute(): http://vimdoc.sourceforge.net/htmldoc/eval.html#substitute%28%29
[15] 'ignorecase': http://vimdoc.sourceforge.net/htmldoc/options.html#'ignorecase'
[16] Справка: http://vimdoc.sourceforge.net/htmldoc/eval.html#expr4
[17] matchend(): http://vimdoc.sourceforge.net/htmldoc/eval.html#matchend%28%29
[18] matchlist(): http://vimdoc.sourceforge.net/htmldoc/eval.html#matchlist%28%29
[19] matchstr(): http://vimdoc.sourceforge.net/htmldoc/eval.html#matchstr%28%29
[20] :tag: http://vimdoc.sourceforge.net/htmldoc/tagsrch.html#:tag
[21] /: http://vimdoc.sourceforge.net/htmldoc/p[́attern.html#/
[22] ?: http://vimdoc.sourceforge.net/htmldoc/pattern.html#?
[23] команда поиска является частью диапозона: http://vimdoc.sourceforge.net/htmldoc/cmdline.html#:/
[24] C или c: http://vimdoc.sourceforge.net/htmldoc/pattern.html#/%CC
[25] :s: http://vimdoc.sourceforge.net/htmldoc/change.html#:s
[26] :sm: http://vimdoc.sourceforge.net/htmldoc/change.html#:sm
[27] :sno: http://vimdoc.sourceforge.net/htmldoc/change.html#:sno
[28] флаг: http://vimdoc.sourceforge.net/htmldoc/change.html#:s_flags
[29] *: http://vimdoc.sourceforge.net/htmldoc/pattern.html#star
[30] #: http://vimdoc.sourceforge.net/htmldoc/pattern.html##
[31] :syntax: http://vimdoc.sourceforge.net/htmldoc/syntax.txt#:syntax
[32] 'magic': http://vimdoc.sourceforge.net/htmldoc/options.html#'magic'
[33] M, m, v или V: http://vimdoc.sourceforge.net/htmldoc/p[́attern.html#/%CCm
[34] 'gdefault': http://vimdoc.sourceforge.net/htmldoc/options.html#'gdefault'
[35] expand(): http://vimdoc.sourceforge.net/htmldoc/eval.html#expand%28%29
[36] glob(): http://vimdoc.sourceforge.net/htmldoc/eval.html#glob%28%29
[37] globpath(): http://vimdoc.sourceforge.net/htmldoc/eval.html#globpath%28%29
[38] 'wildignore': http://vimdoc.sourceforge.net/htmldoc/options.html#'wildignore'
[39] 'suffixes': http://vimdoc.sourceforge.net/htmldoc/options.html#'suffixes'
[40] 'hidden': http://vimdoc.sourceforge.net/htmldoc/options.html#'hidden'
[41] 'bufhidden': http://vimdoc.sourceforge.net/htmldoc/options.html#'bufhidden'
[42] 'autochdir': http://vimdoc.sourceforge.net/htmldoc/options.html#'autochdir'
[43] 'cdpath': http://vimdoc.sourceforge.net/htmldoc/options.html#'cdpath'
[44] :cd: http://vimdoc.sourceforge.net/htmldoc/editing.html#:cd
[45] :lcd: http://vimdoc.sourceforge.net/htmldoc/editing.html#:lcd
[46] 'revins': http://vimdoc.sourceforge.net/htmldoc/options.html#'revins'
[47] setline(): http://vimdoc.sourceforge.net/htmldoc/eval.html#setline%28%29
[48] append(): http://vimdoc.sourceforge.net/htmldoc/eval.html#append%28%29
[49] здесь: http://translit3.hg.sourceforge.net/hgweb/translit3/translit3/file/b41346e7d2d311a137ca93848adbd59d01bddf08/plugin/translit3.vim#l2265
[50] 'selection': http://vimdoc.sourceforge.net/htmldoc/options.html#'selection'
[51] 'selectmode': http://vimdoc.sourceforge.net/htmldoc/options.html#'selectmode'
[52] 'virtualedit': http://vimdoc.sourceforge.net/htmldoc/options.html#'virtualedit'
[53] 'shell': http://vimdoc.sourceforge.net/htmldoc/options.html#'shell'
[54] 'startofline': http://vimdoc.sourceforge.net/htmldoc/options.html#'startofline'
[55] 'wrapscan': http://vimdoc.sourceforge.net/htmldoc/options.html#'wrapscan'
[56] search(): http://vimdoc.sourceforge.net/htmldoc/eval.html#search%28%29
[57] :setlocal: http://vimdoc.sourceforge.net/htmldoc/options.html#:setlocal
[58] BufLeave: http://vimdoc.sourceforge.net/htmldoc/autocmd.html#BufLeave
[59] BufEnter: http://vimdoc.sourceforge.net/htmldoc/autocmd.html#BufEnter
[60] пример: https://bitbucket.org/ZyX_I/frawor/src/1ca697df77cc56e0aa7ee7801dde2f8f582eb089/plugin/frawor/os.vim#cl-175
[61] :echo: http://vimdoc.sourceforge.net/htmldoc/eval.html#:echo
[62] :read: http://vimdoc.sourceforge.net/htmldoc/insert.html#:read
[63] 'fileformats': http://vimdoc.sourceforge.net/htmldoc/options.html#'fileformats'
[64] 'fileencodings': http://vimdoc.sourceforge.net/htmldoc/options.html#'fileencodings'
[65] v:cmdarg: http://vimdoc.sourceforge.net/htmldoc/eval.html#v:cmdarg
[66] здесь: https://bitbucket.org/ZyX_I/aurum/src/0fbb21274851bc013c9b7e0d072e9a44a4def96d/autoload/aurum/lineutils.vim#cl-53
[67] 'fileencodings': http://vimdoc.sourceforge.net/htmldoc/options.html#'fileencodings
[68] :execute: http://vimdoc.sourceforge.net/htmldoc/eval.html#:execute
[69] :echoerr: http://vimdoc.sourceforge.net/htmldoc/eval.html#:echoerr
[70] :throw: http://vimdoc.sourceforge.net/htmldoc/eval.html#:throw
[71] system(): http://vimdoc.sourceforge.net/htmldoc/eval.html#system%28%29
[72] readfile(): http://vimdoc.sourceforge.net/htmldoc/eval.html#readfile%28%29
[73] 'shellredir': http://vimdoc.sourceforge.net/htmldoc/options.html#'shellredir'
[74] смотреть здесь: http://vimdoc.sourceforge.net/htmldoc/pattern.txt#NL-used-for-Nul
[75] 'binary': http://vimdoc.sourceforge.net/htmldoc/options.html#'binary'
Нажмите здесь для печати.