Создание сценариев для редактора Vim: Часть 2. Пользовательские функции

Создаем фундаментальные строительные блоки для задач автоматизации

Пользовательские функции являются важнейшим инструментом разбиения приложения на удобные для поддержки компоненты. Такое разбиение необходимо, чтобы справляться со сложными реальными задачами программирования. В этой статье (второй в серии) рассказывается о том, как в Vimscript создавать и использовать новые функции, а также приводится ряд практических примеров.

Дэмиан Конвей, руководитель и ведущий преподаватель, Thoughtstream

author photo - damian conwayДэмиан Конвей является адъюнкт-профессором компьютерных наук в университете Монаш, Австралия, а также главой международной компании Thoughtstream, занимающейся обучением в сфере ИТ. Он ежедневно пользуется редактором vi на протяжении четверти века и теперь уже почти не надеется, что сможет побороть эту пагубную привычку.



01.03.2011

О Vimscript и этой серии статей

Язык Vimscript – это мощный язык создания сценариев, позволяющий изменять и расширять функциональность редактора Vim. С его помощью можно создавать новые инструменты, упрощать выполнение типовых задач и даже перерабатывать существующие возможности редактора. В этой серии статей предполагается некоторое знакомство читателя с редактором Vim. Также прочитайте статью "Создание сценариев для редактора Vim, часть 1", в которой рассказывается о переменных, значениях и выражениях, - знаниях, необходимых для создания функций.

Пользовательские функции

Спросите программистов на Haskell или Scheme, и они скажут вам, что функции являются самой важной частью любого серьезного языка программирования. Спросите программистов C или Perl, и они скажут вам то же самое.

Функции предоставляют серьезному программисту две важные возможности:

  1. Они позволяют разделять вычислительные задачи на небольшие обозримые части
  2. Они позволяют назначать частям задачи логичные и понятные имена, чтобы с ними было удобно работать.

Язык Vimscript – это серьезный язык программирования, поэтому он, конечно же, поддерживает создание пользовательских функций. В действительности, возможно, он имеет даже лучшую поддержку пользовательских функций, чем Scheme, C или Perl. В этой статье изучаются различные возможности функций Vimscipt и показывается, как с их помощью создавать хорошо сопровождаемый код, расширяющий встроенную функциональность редактора Vim.

Объявление функций

В Vimscript функции определяются с помощью ключевого слова function, после которого указывается имя функции, список параметров (который является обязательным, даже если функция не принимает аргументов). На следующей строке начинается тело функции, которое продолжается до ключевого слова endfunction. Пример:

Листинг 1. Правильно структурированная функция
functionExpurgateText (text)
    let expurgated_text = a:text

    for expletive in [ 'cagal', 'frak', 'gorram', 'mebs', 'zarking']
        let expurgated_text
        \   = substitute(expurgated_text, expletive, '[DELETED]', 'g')
    endfor

    return expurgated_text
endfunction

Возвращаемое значение функции указывается с помощью инструкции return. Можно указывать столько раздельных инструкций return, сколько вам нужно. Можно вообще не указывать этой инструкции, если функция используется как процедура и не имеет полезного возвращаемого значения. Тем не менее функции Vimscript всегда возвращают некоторое значение, поэтому если инструкция return не указана, функция автоматически возвращает ноль.

Имена функций в Vimscript должны начинаться с заглавной буквы:

Листинг 2. Имена функций начинаются с заглавной буквы
function SaveBackup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call SaveBackup()<CR>

В этом примере определяется функция, которая инкрементирует значение находящейся в текущем буфере переменной b:backup_count (а если она еще не существует, инициализирует ее значением 1). Далее функция считывает все строки текущего файла (getline(1,'$')) и вызывает встроенную функцию writefile(), которая записывает их на диск. Вторым аргументом функции writefile() является имя файла, в который будет осуществляться запись; в данном случае оно определяется конкатенацией имени открытого файла (bufname('%')) и нового значения счетчика. Возвращаемым значением является удачное/неудачное значение вызова writefile(). В конце листинга вызов новой функции, создающей нумерованную резервную копию текущего файла, с помощью nmap назначается сочетанию клавиш CTRL-B.

Имена функций Vimscript могут и не начинаться с заглавной буквы, если их объявлять, явно указывая префикс области видимости (так же, как и переменные, о чем было рассказано в части 1). Чаще всего используется префикс s:, делающий функцию локальной по отношению к текущему файлу сценария. Если область видимости функции указана таким образом, ее имя не обязательно должно начинаться с заглавной буквы и может быть любым корректным идентификатором. Однако функции с явно указанной областью видимости также всегда должны вызываться с префиксами области видимости. Пример:

Листинг 3. Вызов функции с указанием префикса области видимости
" Функция доступна в текущем файле сценария...
function s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call s:save_backup()<CR>

Повторно объявляемые функции

Объявления функций в Vimscript являются инструкциями времени выполнения, поэтому если сценарий загружается дважды, все объявления функций в этом сценарии также будут выполнены дважды, а значит, соответствующие функции будут пересозданы.

Повторное объявление функций считается фатальной ошибкой (для предотвращения коллизий - ситуаций, когда два разных сценария по ошибке объявляют функции с одним и тем же именем). Это затрудняет создание функций в сценариях, спроектированных для многократной загрузки, таких как специальные сценарии подсветки синтаксиса.

Поэтому в Vimscript предусмотрен модификатор (function!), указывающий, что объявление этой функции можно сколь угодно часто и безопасно перезагружать:

Листинг 4. Указываем, что объявление функции можно безопасно перезагружать
function! s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

Для функций, определенных с этим ключевым словом, не выполняются проверки повторного объявления, поэтому лучше всего использовать этот прием для функций с явно указанной областью видимости (так как в этом случае указание области видимости гарантирует, что функция не будет конфликтовать с какой-либо функцией из другого сценария).

Вызов функций

Чтобы вызвать функцию и использовать её возвращаемое значение в каком-либо выражении, просто укажите ее имя вместе с заключенным в скобки списком аргументов функции:

Листинг 5. Использование возвращаемого функцией значения
"Очищаем текущую строку...
let success = setline('.', ExpurgateText(getline('.')) )

Заметьте, однако, что в отличие от C или Perl Vimscript не позволяет отбрасывать возвращаемое значение функции, не используя его. Поэтому, если вы намерены использовать функцию как процедуру или подпрограмму и хотите игнорировать возвращаемое значение, необходимо предварить вызов функции командой call:

Листинг 6. Использование функции без использования возвращаемого значения
"Делаем контрольную копию текста....
call SaveBackup()

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

Список параметров

Vimscript позволяет определять как явные параметры, так и списки параметров переменной длины, и даже позволяет их комбинировать.

При объявлении функции можно указать до 20 явно именованных параметров. Внутри функции соответствующие значения аргументов для текущего вызова можно получить, указав префикс a: и имя параметра:

Листинг 7. Получение значений аргументов внутри функции
function PrintDetails(name, title, email)
    echo 'Name:   '  a:title  a:name
    echo 'Contact:'  a:email
endfunction

Если вы не знаете, сколько именно аргументов понадобится передавать в функцию, то вместо именованных параметров можно с помощью круглых скобок (...) указать для нее переменное число параметров. В этом случае функцию можно вызывать с любым количеством аргументов; эти аргументы собираются в одной переменной–массиве с именем a:000. К отдельным аргументам также можно обращаться по имени как к позиционным параметрам: a:1, a:2, a:3 и т.д. Количество аргументов хранится в переменной a:0. Пример:

Листинг 8. Определяем функцию с переменным количеством аргументов
function Average(...)
    let sum = 0.0

    for nextval in a:000"a:000 - это список аргументов
        let sum += nextval
    endfor

    return sum / a:0"a:0 - это количество аргументов
endfunction

Заметьте, что в этом примере переменная sum должна быть явно проинициализирована числом с плавающей точкой, иначе все последующие вычисления будут выполняться с использованием целочисленной арифметики.

Совместное использование именованных аргументов и переменного количества аргументов

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

Например, предположим, мы хотим создать функцию CommentBlock(), которая принимает строку и форматирует ее в соответствующий блок комментария для различных языков программирования. Такой функции всегда необходимо передавать строку, которую необходимо отформатировать, поэтому этот аргумент должен быть именованным. Но мы также хотели бы сделать необязательные аргументы для последовательности, начинающей комментарий, «обрамляющего» символа и ширины комментария (задав им разумные значения по умолчанию). Вызов функции мог бы выглядеть так:

Листинг 9. Простой вызов функции CommentBlock
call CommentBlock("This is a comment")

Такой вызов должен возвращать следующий многострочный комментарий:

Листинг 10. Возвращаемое значение функции CommentBlock
//*******************
// This is a comment
//*******************

Если же задать дополнительные аргументы, то они будут задавать новые значения последовательности, начинающей комментарий, обрамляющего символа и ширины комментария. Поэтому следующий вызов:

Листинг 11. Более сложный вызов функции CommentBlock
call CommentBlock("This is a comment", '#', '=', 40)

вернет строку:

Листинг 12. Возвращаемое значение функции CommentBlock
#========================================
# This is a comment
#========================================

Такую функцию можно реализовать, например, так:

Листинг 13. Реализация функции CommentBlock
function CommentBlock(comment, ...)
    "Если имеется хотя бы 1 необязательный аргумент, то первый аргумент
    "задает начало комментария...
    let introducer =  a:0 >= 1  ?  a:1  :  "//"

    "Если имеется 2 или больше необязательных аргумента, то второй аргумент
    "задает обрамляющий символ...
    let box_char   =  a:0 >= 2  ?  a:2  :  "*"

    "Если имеется 3 или больше необязательных аргумента, то третий аргумент
    "задает ширину комментария...
    let width      =  a:0 >= 3  ?  a:3  :  strlen(a:comment) + 2

    " Конструируем рамку и помещаем в нее текст комментария...
    return introducer . repeat(box_char,width) . "\<CR>"
    \    . introducer . " " . a:comment        . "\<CR>"
    \    . introducer . repeat(box_char,width) . "\<CR>"
endfunction

Если имеется хотя бы один необязательный аргумент (a:0 >= 1), то строковой переменной начала комментария присваивается значение первого аргумента (т. е., a:1); иначе ей назначается значение по умолчанию "//". Аналогично, если имеется два или более необязательных аргумента (a:0 >= 2), то переменной box_char присваивается значение второго аргумента (a:2), иначе ей задается значение по умолчанию "*". Если передано три или более необязательных аргумента, то переменной width - ширине комментария - присваивается значение третьего аргумента. Если же ширина комментария не указана явно, она вычисляется автоматически исходя из длины строки комментария (strlen(a:comment)+2).

В конце, определив значения всех параметров, функция создает блок комментария: верхняя и нижняя строка формируются из строковой переменной начала комментария, за которой следует соответствующее количество обрамляющих символов (repeat(box_char,width)), а между этими строками помещается сам текст комментария.

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

Листинг 14. Назначаем клавиши для вызова функции в режиме вставки
"Комментарий в C++/Java/PHP...
imap <silent>  ///  <C-R>=CommentBlock(input("Enter comment: "))<CR>

"Комментарий в Ada/Applescript/Eiffel...
imap <silent>  ---  <C-R>=CommentBlock(input("Enter comment: "),'--')<CR>

"Комментарий в Perl/Python/Shell...
imap <silent>  ###  <C-R>=CommentBlock(input("Enter comment: "),'#','#')<CR>

При нажатии любого из этих сочетаний клавиш вызывается встроенная функция input(), которая предлагает пользователю ввести текст комментария. Затем вызывается функция CommentBlock(), которая конвертирует этот текст в блок комментария. Предшествующая ей последовательность <C-R>= вставляет возвращенную функцией строку.

Обратите внимание, что для первого сочетания клавиш функция вызывается лишь с одним аргументом, поэтому в этом случае для создания комментария будет использоваться //. Для второго и третьего сочетания клавиш в вызов функции в качестве второго аргумента, обозначающего строку начала комментария, передаются соответственно -- и #. Для последнего сочетания клавиш также передается и третий аргумент, чтобы "обрамляющий" символ совпадал с символом начала комментария.


Функции и интервалы строк

Любые стандартные команды Vim, в том числе call—, можно вызывать, предварительно указав интервал строк, для которых следует ее выполнить. Команда будет вызвана по одному разу для каждой строки из указанного интервала:

"Удаляем все строки, начиная с текущей строки (.) до конца файла ($)...
:.,$delete

"Заменяем "foo" на "bar" в строках с 1-й по 10-ю
:1,10s/foo/bar/

"Выравниваем по центру все строки, начиная с 5-й строки выше текущей по 5-ю строку ниже текущей...
:-5,+5center

Узнать больше о возможностях работы с интервалами строк можно, набрав :help cmdline-ranges в любом сеансе Vim.

В случае указания интервала для команды call запрошенная функция будет вызвана для каждой строки из интервала. Чтобы увидеть, почему это может быть полезным, давайте попробуем написать функцию, которая конвертировала бы обыкновенные символы амперсанда на текущей строке в &amp; - представление амперсанда в XML, но которая также была бы достаточно умной, чтобы игнорировать амперсанды, уже являющиеся частью какой-либо другой сущности XML. Такую функцию можно реализовать следующим образом:

Листинг 15. Функция конвертации амперсандов
function DeAmperfy()
    "Считываем текущую строку...
    let curr_line   = getline('.')

    "Заменяем простые амперсанды...
    let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')

    "Обновляем текущую строку...
    call setline('.', replacement)
endfunction

В первой строке функции DeAmperfy() из буфера редактора считывается текущая строка (getline('.')). Во второй строке выполняется поиск в строке всех символов &, за которыми не следует идентификатор или двоеточие. Это делается с помощью отрицающего шаблона просмотра вперед '&\(\w\+;\)\@!' (подробнее см. :help \@!). Далее с помощью вызова функции substitute() все "простые" амперсанды заменяются XML-представлениями &amp;. Наконец, третья строка функции DeAmperfy() выполняет фактическое обновление текущей строки.

Если вызвать эту функцию из командной строки:

:call DeAmperfy()

то замена будет произведена только в текущей строке. Однако если перед call указать интервал строк:

:1,$call DeAmperfy()

то функция будет вызвана по одному разу для каждой строки в интервале (в данном случае, для всех строк в файле).

Передача интервала строк внутрь функции

Повторный вызов функции для каждой строки является поведением по умолчанию, удобным в большинстве случаев. Однако иногда бывает предпочтительнее задать интервал строк и затем один раз вызвать функцию, которая должна сама обработать переданный ей интервал. В Vimscript легко можно сделать и так. Для этого после объявления функции следует указать специальный модификатор (range):

Листинг 16. Работа с интервалом строк внутри функции
function DeAmperfyAll() range"Проходим по всем строкам из интервала...
    for linenum in range(a:firstline, a:lastline)
        "Заменяем свободные амперсанды (как в DeAmperfy())...
        let curr_line   = getline(linenum)
        let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')
        call setline(linenum, replacement)
    endfor

    "Отчитываемся о том, что было сделано...
    if a:lastline > a:firstline
        echo "DeAmperfied" (a:lastline - a:firstline + 1) "lines"
    endif
endfunction

Так как для функции DeAmperfyAll() указан модификатор range, то каждый раз при ее вызове с указанием интервала строк, например:

:1,$call DeAmperfyAll()

функция будет вызываться только один раз, и в нее будут передаваться верхняя и нижняя границы указанного интервала с помощью специальных аргументов a:firstline и a:lastline. Если интервал строк не указан, то аргументам a:firstline и a:lastline присваивается номер текущей строки.

Сначала функция строит список номеров строк, которые следует обработать: (range(a:firstline, a:lastline)). Обратите внимание, что здесь вызывается встроенная функция range(), которая не имеет ничего общего с использованием модификатора range в объявлении функции. Функция range() просто конструирует список, она очень похожа на функцию range() в Python или оператор .. в Haskell или Perl.

Определив список строк, которые следует обработать, функция с помощью цикла for проходит по строкам из этого списка:

for linenum in range(a:firstline, a:lastline)

и соответствующим образом обновляет каждую из них (точно так же, как это делалось в DeAmperfy()).

И в конце, если в интервал входит более чем одна строка (т.е. если a:lastline > a:firstline), функция отчитывается в том, сколько строк было обработано.

Интервалы строк в визуальном режиме

Особенно полезным приемом работы с функцией, которая может оперировать над интервалами строк, является ее вызов из визуального режима (подробнее о нем узнать можно в справке :help Visual-mode).

Например, с помощью следующего вызова можно конвертировать амперсанды в параграфе, внутри которого в данный момент находится курсор:

Vip:call DeAmperfyAll()

Нажатие клавиши V в нормальном режиме переключает редактор в визуальный режим. Затем сочетание клавиш ip подсвечивает текущий параграф. Далее, клавиша : переключает редактор в командный режим и автоматически задает для выполнения команды интервал строк, который был только что выбран в визуальном режиме. В данном случае интервал строк задается для функции DeAmperfyAll().

Заметьте, что того же самого можно было бы добиться и вызовом:

Vip:call DeAmperfy()

Единственным отличием здесь является то, что функция DeAmperfy() будет вызываться многократно: по одному разу для каждой строки, подсвеченной в визуальном режиме с помощью команды Vip.


Функция, помогающая писать код

Большинство пользовательских функций в Vimscript требуют очень мало параметров, а часто не требуют их вообще. Это связано с тем, что обычно они получают данные напрямую из буфера редактора и контекстной информации (такой как позиция курсора, размер текущего параграфа, размер текущего окна или содержимое текущей строки).

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

Листинг 16. Нарушение выравнивания в операторах присваивания
let applicants_name = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative = 'sister'
let fathers_occupation = 'Sith'

При добавлении каждой новой инструкции нужно поддерживать выравнивание операторов присваивания. Делать это каждый раз вручную довольно утомительно:

Листинг 17. Операторы присваивания, выровненные вручную
let applicants_name     = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative    = 'sister'
let fathers_occupation  = 'Sith'

Чтобы эта рутинная работа нагоняла меньше скуки, можно назначить на определенное сочетание клавиш (например, ;=) вызов функции, которая в текущем блоке кода будет искать строки с операторами присваивания, и автоматически выравнивать эти операторы. Например, так:

Листинг 18. Функция, выравнивающая операторы присваивания
function AlignAssignments ()
    "Шаблоны, необходимые для нахождения операторов присваивания...
    let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
    let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

    "Находим блок кода, с которым будем работать (непустые строки с одинаковым отступом)
    let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
    let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
    let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
    if lastline < 0
        let lastline = line('$')
    endif

    "Находим позицию в строке, по которой следует выравнивать операторы присваивания...
    let max_align_col = 0
    let max_op_width  = 0
    for linetext in getline(firstline, lastline)
        "В этой строке имеется оператор присваивания?
        let left_width = match(linetext, '\s*' . ASSIGN_OP)

        "Если оператор имеется, отслеживаем максимальные позицию в строке и 
        "ширину оператора присваивания...
        if left_width >= 0
            let max_align_col = max([max_align_col, left_width])

            let op_width      = strlen(matchstr(linetext, ASSIGN_OP))
            let max_op_width  = max([max_op_width, op_width+1])
         endif
    endfor

    "Код, необходимый для переформатирования строк таким образом, 
    "чтобы выровнять операторы присваивания...
    let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
    \                                    max_op_width,  submatch(2))'

    " Переформатируем строки с операторами присваивания...
    for linenum in range(firstline, lastline)
        let oldline = getline(linenum)
        let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
        call setline(linenum, newline)
    endfor
endfunction

nmap <silent>  ;=  :call AlignAssignments()<CR>

Функция AlignAssignments() сначала задает два регулярных выражения (подробнее о синтаксисе регулярных выражений в Vim см. :help pattern):

let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

Шаблон ASSIGN_OP распознает все стандартные операторы присваивания: =, +=, -=, *=, т.д., однако аккуратно игнорирует другие операторы, содержащие =, такие, как == и =~. Если в вашем любимом языке имеются другие операторы присваивания (например .=, ||= или ^=), то можно расширить регулярное выражение ASSIGN_OP, чтобы оно распознавало и эти операторы. Аналогично, можно переопределить ASSIGN_OP, чтобы оно распознавало и другие элементы, которые следует выравнивать, такие как символы начала комментария или маркеры столбцов, и выравнивало эти символы тоже.

Шаблон ASSIGN_LINE ищет с начала строки (^), минимальное количество символов (.\{-}), за которыми следуют один или несколько пробельных символов (\s*), а затем оператор присваивания.

Обратите внимание, что начальный подшаблон минимального количества символов и подшаблон оператора указываются в скобках: \(...\). Подстроки, подошедшие под эти два компонента регулярного выражения, позднее будут извлечены вызовами встроенной функции submatch(), а именно: вызовом submatch(1) будет получено все находящееся перед оператором, а вызовом submatch(2) будет извлечен сам оператор.

Далее в AlignAssignments() определяется интервал строк, с которым надо будет работать:

let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
    let lastline = line('$')
endif

В предыдущих примерах интервал строк для функций задавался либо явно, либо посредством выделения блока текста в визуальном режиме, однако здесь функция определяет для себя интервал строк самостоятельно. Для этого с помощью вызова встроенной функции matchstr() функция определяет, с каких пробельных символов ('^\s*') начинается текущая строка (getline('.')). Затем в переменной indent_pat конструируется новое регулярное выражение, которое ищет точно такие же пробельные символы в начале непустой строки (для выбора непустых строк в конце выражения мы указали '\S').

Далее в AlignAssignments() вызывается встроенная функция search(), которая ищет от текущей строки к началу файла (с помощью флагов 'bnW') и определяет первую строку, не имеющую в точности такого же отступа. Добавив 1 к номеру найденной строки, мы получаем начало интересующего нас интервала, а именно первую из последовательности строк с точно таким же отступом, как и у текущей строки.

Затем второй вызов search() ищет от текущей строки к концу файла ('nW') и определяет параметр lastline: номер последней строки из последовательности строк с точно таким же отступом. Во втором случае поиск может дойти до конца файла и не найти строки с отличным отступом, тогда search() вернет -1. Чтобы корректно обработать эту ситуацию, далее следует инструкция if, в которой lastline задается равным номеру последней строки файла (т.е. номеру строки, который вернет line('$')).

В результате AlignAssignments() узнает интервал строк, которые окружают текущую строку и имеют точно такой же отступ. На основании этой информации выравниваются только операторы присваивания, находящиеся в одной области видимости в одном блоке кода. Конечно же, это не будет работать, если отступы вашего кода не отражают области видимости; в таком случае вы полностью заслужили, чтобы с форматированием кода произошла катастрофа.

В первом цикле for определяется позиция в строке, по которой следует выравнивать операторы присваивания. Для этого все строки из выбранного интервала (полученные вызовом getline(firstline, lastline)) проверяются на наличие оператора присваивания (перед которым, возможно, находятся пробельные символы):

let left_width = match(linetext, '\s*' . ASSIGN_OP)

Если в строке отсутствует оператор присваивания, то встроенная функция match() не сможет ничего найти и вернет -1. В этом случае цикл просто переходит к следующей строке. Если же оператор имеется, то функция match() вернет (положительный) индекс, обозначающий, где в строке находится этот оператор. Далее в инструкции if с помощью встроенной функции max() определяется максимальная (т.е. самая правая) позиция оператора среди всех найденных к данному моменту операторов присваивания:

let max_align_col = max([max_align_col, left_width])

В двух оставшихся строках инструкции if сначала с помощью встроенной функции matchstr() извлекается сам оператор, а затем с помощью встроенной функции strlen() определяется его длина (это будет 1 для "=" или 2 для '+=', '-=', и т.д.) Далее в переменной max_op_width отслеживается максимальная ширина, необходимая для выравнивания различных операторов присваивания в данном интервале:

let op_width     = strlen(matchstr(linetext, ASSIGN_OP))
let max_op_width = max([max_op_width, op_width+1])

Когда интервал строк и позиция, по которой следует производить выравнивание, определены, остается только пройти по строкам из интервала и соответствующим образом их переформатировать. Для выполнения переформатирования используется встроенная функция printf() Эта очень полезная функция, но она неудачно названа. Она делает не то же самое, что функция printf в C, Perl или PHP. Фактически она выполняет то, что в этих языках делает функция sprintf. В Vimscript printf не распечатывает отформатированную версию строки с данными, а формирует и возвращает эту отформатированную строку.

В идеале в AlignAssignments() для переформатирования каждой строки использовалась бы встроенная функция substitute(), которая бы заменяла все, что находится перед оператором выравнивания, на тот же текст, преобразованный функцией printf. К сожалению, в substitute() в качестве заменяющего значения можно передавать только строку, а не вызов функции.

Поэтому, чтобы воспользоваться printf() для переформатирования, нужно использовать специальную форму замещения текста: "\=expr". Последовательность символов \= в начале заменяющей строки сообщает substitute(), что следует вычислить значение следующего за ней выражения и подставить полученный результат. Заметьте, что это похоже на механизм <C-R>= в режиме вставки, за исключением того, что данное магическое поведение доступно только при замене строк функцией substitute() и в стандартной команде Vim :s/.../.../.

В нашем примере заменяющее выражение одинаково для всех строк, поэтому оно помещается перед началом второго цикла for в переменную FORMATTER:

let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
\                                    max_op_width,  submatch(2))'

Когда это выражение вычисляется в substitute(), функция printf() выравнивает слева (с помощью %-*s) все, что находится левее оператора (submatch(1)) и помещает результат в поле шириной max_align_col символов. Затем она задает выравнивание справа (с помощью %*s) для самого оператора (submatch(2)) и помещает его во второе поле, шириной max_op_width символов. За более подробной информацией о том, как параметры - и * модифицируют спецификаторы форматирования %s обращайтесь к справке - :help printf().

Имея такую строку форматирования, можно во втором цикле for пройти по всему интервалу строк:

for linenum in range(firstline, lastline)
    let oldline = getline(linenum)

В этом цикле используется функция substitute(), которая заменяет текст, находящийся включительно до оператора присваивания (находя его с помощью шаблона ASSIGN_LINE) результатом вызова функции printf() (в переменной FORMATTER):

    let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
    call setline(linenum, newline)
endfor

После того как цикл for пройдет по всем строкам, все находящиеся в них операторы присваивания будут корректно выровнены. Теперь остается только назначить сочетание клавиш для вызова функции AlignAssignments(), например:

nmap <silent>  ;=  :call AlignAssignments()<CR>

Заглядывая вперед

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

Vimscript позволяет определять функции как с фиксированным, так и с переменным количеством аргументов. Также функции могут либо автоматически, либо под управлением пользователя взаимодействовать с интервалами строк из буфера редактора. Функции могут внутри себя вызывать встроенные функции Vim (например, search() или substitute() соответственно для поиска и замены текста), а также могут напрямую получать информацию о состоянии редактора (например, о том, на какой строке находится курсор, с помощью line('.')) или работать с текстовым буфером (с помощью getline() и setline()).

Несомненно, это очень мощный инструментарий, но наши возможности программного манипулирования редактором всегда ограничены тем, насколько ясно и точно мы можем представлять данные, с которыми работает наш код. Пока в этой серии статей, мы ограничивались использованием скалярных значений (чисел, строк и булевых переменных). В следующих двух статьях мы научимся использовать намного более мощные и удобные структуры данных: упорядоченные списки и словари произвольного доступа.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

  • Участвуйте в жизни сообщества developerWorks (EN) - создав свой личный профиль и домашнюю страницу, вы можете приспособить developerWorks к своим интересам и взаимодействовать с другими пользователями developerWorks.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=629977
ArticleTitle=Создание сценариев для редактора Vim: Часть 2. Пользовательские функции
publish-date=03012011