воскресенье, 29 апреля 2012 г.

vim: автоматическое переключение раскладки клавиатуры при входе и выходе из режима ввода

Если нужно писать в vim что-то на русском языке, то одной из самых раздражающих неприятностей является необходимость постоянного ручного переключения раскладки клавиатуры на латинскую при выходе из режима ввода и обратно на русскую - при входе в режим ввода (даже одна эта фраза уже раздражает). Оказывается, можно очень просто автоматизировать этот процесс, во всяком случае в среде под управлением X server. Волшебным средством автоматизации будет использование внешней программы, которая умеет возвращать значение текущей раскладки и устанавливать новую. При поиске такого волшебного средства я опробовал xte из пакета программ xautomation и xdotool. Эти программы можно установить прямо из репозитория моего дистрибутива (что, конечно же, плюс), но они не умеют выполнять первую задачу, поэтому на роль волшебного средства не годятся. Поискав еще немного я нашел то, что надо: программу xkb-switch. Я не буду описывать ее опции, они достаточно просты, а сразу приведу код, который нужно добавить в .vimrc, чтобы автоматическое переключение на латиницу при выходе из режима ввода заработало:
fun<SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch')'\n''''g')
    if a:mode == 0
        if cur_layout != 'us'
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun

if executable('xkb-switch')
    autocmd InsertEnter * call <SID>xkb_switch(1)
    autocmd InsertLeave * call <SID>xkb_switch(0)
endif
Все интересное происходит в функции xkb_switch(mode), которая вызывается с разными аргументами mode при выходе из режима ввода (значение mode=0) и входе в режим ввода (значение mode=1). В первом случае мы вызываем системную команду xkb-switch -s us, которая принудительно переводит раскладку клавиатуры в латиницу, запоминая какая раскладка была на выходе из режима ввода в буферной переменной b:xkb_layout. Во втором случае (при входе в режим ввода) проверяется, существует ли переменная b:xkb_layout, и если это так, то вызывается xkb-switch c опцией -s, в которую передается значение этой переменной. Это позволяет восстановить раскладку клавиатуры, установленную в данном буфере перед выходом из режима ввода. Я еще раз хочу подчеркнуть: в данном буфере! То есть раскладка устанавливается для каждого буфера такой, которой она была при выходе из режима ввода именно в нем, а это очень удобно.

Update. Небольшое дополнение для тех, кто пользуется режимом выделения текста (Select mode). Это очень удобный режим, когда текст сначала выделяется, а затем поверх него пишется новый текст (аналог классического режима выделения текста в офисных редакторах). Перейти в него из нормального режима vim можно, набрав gh (нормальный режим выделения), gH (строчный режим выделения) или g<C-h> (блочный режим выделения). Кроме того, можно переключаться между визуальным режимом и режимом выделения текста с помощью <C-g>, а также настроить vim таким образом, чтобы выделение текста мышью переводило его в режим выделения текста (вместо визуального режима по умолчанию).

Поскольку я иногда применяю режим выделения текста, то быстро обнаружил, что при его использовании, автоматическое переключение раскладки ведет себя неподобающе, а именно: если ожидается переключение на кириллицу, то первый введенный символ остается латинским, а следующие, как и ожидалось, печатаются кириллицей. Это легко объясняется тем, как работает режим выделения текста: вначале vim проверяет, является ли вводимый символ печатным, затем, если это так, этот символ вводится в начале выделенного текста, а уже после этого vim переходит в режим ввода. Очевидно, что первый введенный символ останется латинским, так как событие InsertEnter в момент его ввода еще не наступило.

Для того, чтобы исправить это недоразумение, я добавил новую функцию xkb_mappings_load(), которая переопределяет вышеперечисленные команды gh, gH, g<C-h> и <C-g>, с помощью маппингов, которые вызывают функцию xkb_switch() с нужным аргументом mode. Сразу хочу отметить, что проблема останется для тех, кто использует мышь для перехода в режим выделения текста (вы можете добавить аналогичный маппинг для этого случая сами), а также в случае выхода из режима выделения с помощью клавиши <Esc> (дело в том, что xkb_switch() может перевести раскладку в кириллицу, выход же из режима выделения текста с помощью <Esc> минует режим ввода, а следовательно мы можем оказаться в нормальном режиме с кириллической раскладкой).
fun<SID>xkb_mappings_load()
    for hcmd in ['gh''gH''g^H']
        exe "nnoremap <buffer> <silent> ".hcmd.
                    \ " :call <SID>xkb_switch(1)<CR>".hcmd
    endfor
    xnoremap <buffer> <silent> <C-g> :<C-u>call <SID>xkb_switch(1)<CR>gv<C-g>
    snoremap <buffer> <silent> <C-g> <C-g>:<C-u>call <SID>xkb_switch(0)<CR>gv
    let b:xkb_mappings_loaded = 1
endfun

fun<SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch')'\n''''g')
    if a:mode == 0
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun
Обратите внимание: ^H в первой строке функции xkb_mappings_load() не является записью двух символов ^ и H! Это один символ, который соответствует <Bs> в таблице ASCII. Для того, чтобы ввести его в vim, следует в режиме ввода набрать <C-v>008.

Функция xkb_mappings_load() вызывается из xkb_switch() только в случае необходимости, то есть когда при выходе из режима ввода раскладка клавиатуры не является латинской (точнее us), и только для текущего буфера. Почему я просто не замапил эти команды в глобальной области, например рядом с автокомандами InsertEnter и InsertLeave? Все просто: маппинги <C-g> в визуальном режиме с использованием вызова команд в нормальном режиме не даются даром: заметили использование команды gv внутри маппингов, которая говорит: выдели-ка потерянное выделение заново? Потеря и восстановление области выделения приводят к перерисовке текста на экране. Поскольку редактировать русские тексты приходится очень нечасто, то глобальное определение маппингов для <C-g> ухудшило бы юзабилити vim без необходимости.

Update 2. По каким-то причинам вызов функции system() из autocmd InsertEnter может иногда приводить к появлению мусора в строке ввода если vim стартовал в режиме ввода (например был запущен командой vim -c start или, при наличии плагинов c.vim или perl-support.vim, пользователь открыл для редактирования новый C/C++ или Perl файл и соответствующий плагин вставил хедер с переводом vim в режим ввода). Ниже приводится вариант функции xkb_switch(), в котором system() не вызывается при первом переводе буфера в режим ввода, а также добавлен комментарий с упоминанием этого неприятного феномена.
fun<SID>xkb_switch(mode)
    if a:mode == 0
        " by some reason calling system() function may produce garbage on
        " display if vim started in Insert mode (e.g. when c-support,
        " perl-support etc. apply file header for a new file), so definition
        " of cur_layout was moved inside if- and else- blocks from top level
        let cur_layout = substitute(system('xkb-switch')'\n''''g')
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout')
            let cur_layout = substitute(system('xkb-switch')'\n''''g')
            if b:xkb_layout != cur_layout
                call system('xkb-switch -s '.b:xkb_layout)
            endif
        endif
    endif
endfun
Update 3. См. важное обновление здесь.