Пользователь
0,0
рейтинг
2 февраля 2014 в 13:52

Разработка → Vim-крокет перевод

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

Введение


Недавно я обнаружил интересную игру под названием VimGolf. Цель этой игры заключается в том, чтобы преобразовать кусок текста из одной формы в другую наименьшим возможным количеством нажатий клавиш. Пока я играл на этом сайте с разными пазлами, мне стало любопытно — а какие привычки редактирования текста есть у меня? Мне захотелось лучше понять способы манипулирования текстом в Vim и проверить, смогу ли я найти неэффективные моменты в моем рабочем процессе. Я провожу огромное количество времени в моем текстовом редакторе, поэтому устранение даже незначительных шероховатостей может привести к значительному увеличению производительности. В этом посте я расскажу о своем анализе и о том, как я уменьшил количество нажатий клавиш при использовании Vim. Я назвал эту игру Vim-крокет.

Сбор данных


Я начал мой анализ со сбора данных. Редактирование текста на моем компьютере всегда происходит с помощью Vim, так что в течении 45 дней я логировал любое нажание клавиши в нем с помощью флага scriptout. Для удобства я сделал alias для записи нажатий в лог:

alias vim='vim -w ~/.vimlog "$@"'

После этого необходимо было распарсить полученные данные, но это оказалось не так легко. Vim это модальный редактор, в котором одна команда может иметь несколько различных значений в разных режимах. Помимо этого команды зависят от контекста, когда их поведение может отличаться в зависимости от того, где внутри буфера vim они исполняются. Например, команда cib в нормальном режиме переведет пользователя в режим редактирования, если команда выполняется внутри скобок, но оставит пользователя в нормальном режиме, если она выполнена вне скобок. Если же cib будет выполнена в режиме редактирования, то она будет иметь совершенно другое поведение — запишет символы «cib» в текущий буфер.

Я рассмотрел несколько кандидатов для парсинга команд vim, включая промышленные библиотеки, такие как antler и parsec, а также специализирующийся на vim проект vimprint. После некоторых раздумий, я решил написать собственный инструмент, т.к. трата большого количества времени на изучение достаточно сложных парсеров казалось необоснованным для этой задачи.

Я написал сыроватый лексер на haskell'е для разбиения собранных мной нажатий клавиш на индивидуальные команды vim. Мой лексер использует monoids для извлечения команд нормального режима из лога для дальнейшего анализа. Вот исходник лексера:

import qualified Data.ByteString.Lazy.Char8 as LC
import qualified Data.List as DL
import qualified Data.List.Split as LS
import Data.Monoid
import System.IO

main = hSetEncoding stdout utf8 >> 
       LC.getContents >>= mapM_ putStrLn . process

process =   affixStrip 
          . startsWith 
          . splitOnMode
          . modeSub
          . capStrings 
          . split mark 
          . preprocess

subs = appEndo . mconcat . map (Endo . sub)

sub (s,r) lst@(x:xs)
    | s `DL.isPrefixOf` lst = sub'
    | otherwise = x:sub (s,r) xs
    where
        sub' = r ++ sub (s,r) (drop (length s) lst)
sub (_,_) [] = []

preprocess =   subs meta 
             . DL.intercalate " "
             . DL.words
             . DL.unwords
             . DL.lines 
             . LC.unpack

splitOnMode = DL.concat $ map (\el -> split mode el)

startsWith = filter (\el -> mark `DL.isPrefixOf` el && el /= mark)

modeSub = map (subs mtsl)

split s r = filter (/= "") $ s `LS.splitOn` r

affixStrip =   clean 
             . concat 
             . map (\el -> split mark el)

capStrings = map (\el -> mark ++ el ++ mark)

clean = filter (not . DL.isInfixOf "[M")

(mark, mode, n) = ("-(*)-","-(!)-", "")
meta = [("\"",n),("\\",n),("\195\130\194\128\195\131\194\189`",n),
        ("\194\128\195\189`",n),("\194\128kb\ESC",n), 
        ("\194\128kb",n),("[>0;95;c",n), ("[>0;95;0c",n),
        ("\ESC",mark),("\ETX",mark),("\r",mark)]
mtsl = [(":",mode),("A",mode), ("a",mode), ("I",mode), ("i",mode),
        ("O",mode),("o",mode),("v", mode),("/",mode),("\ENQ","⌃e"),
        ("\DLE","⌃p"),("\NAK","⌃u"),("\EOT","⌃d"),("\ACK","⌃f"),
        ("\STX","⌃f"),("\EM","⌃y"),("\SI","⌃o"),("\SYN","⌃v"),
        ("\DC2","⌃r")]

А вот пример данных до и после обработки:

cut -c 1-42 ~/.vimlog | tee >(cat -v;echo) | ./lexer
`Mihere's some text^Cyyp$bimore ^C0~A.^C:w^M:q

`M
yyp$b
0~

Лексер читает из стандартного потока ввода и отправляет обработанные команды в стандартный вывод. В примере выше примере необработанные данные расположены во второй строке, а результат обработки — на следующих. Каждая строка представляет собой группы команд нормального режима, выполненные в соответствующей последовательности. Лексер корректно определил, что я начал в нормальном режиме, перейдя в некоторый буфер с помощью метки `M, затем ввел here's some text в режиме редактирования, после чего скопировал/вставил строку и перешел на начало последнего слова в строке с помощью команды yyp$b. Затем ввел дополнительный текст и в итоге перешел в начало строки, заменив первый символ на прописной командой 0~.

Карта использования клавиш


После обработки залогированных данных, я форкнул замечательный проект heatmap-keyboard за авторством Patrick Wied, и добавил в него собственный кастомный слой для чтения вывода лексера. Этот проект не определял большинство мета-символов, например, ESC, Ctrl и Cmd, поэтому мне было необходимо написать загрузчик данных на JavaScript и внести некоторые другие модификации. Я транслировал мета-символы, используемые в vim, в юникод и спроецировал их на клавиатуру. Вот что у меня получилось на количестве команд, близком к 500 000 (интенсивность цвета указывает на частоту использования клавиш).



На полученной карте видно, что чаще всего используется клавиша Ctrl — я использую ее для многочисленных команд перемещения в vim. Например, ^p для ControlP, или цикл по открытым буферам через ^j ^k.

Другая особенность, которая бросилась в глаза при анализе карты — это частое использование ^E ^Y. Я повседневно использую эти команды для навигации вверх/вниз по коду, хотя вертикальное перемещение с помощью них неэффективно. Каждый раз, когда одна из этих команды исполняется, курсор перемещается только на несколько строк за раз. Более эффективно было бы использовать команды ^U ^D, т.к. они смещают курсор на половину экрана.

Частота использования команд


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

$ sort normal_cmds.txt | uniq -c | sort -nr | head -10 | \
    awk '{print NR,$0}' | column -t

1   2542    j
2   2188    k
3   1927    jj
4   1610    p
5   1602    ⌃j
6   1118    Y
7   987     ⌃e
8   977     zR
9   812     P
10  799     ⌃y

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

Сложность команд


Другая оптимизация, на которую я хотел взглянуть — это сложность команд нормального режима. Мне было любопытно увидеть, смогу ли я найти команды, которые использую повседневно, но которые требуют излишне большого количества нажатий клавиш. Такие команды можно было бы заменить с помощью shortcut'ов, которые бы ускорили их выполнение. В качестве меры сложности команд я использовал энтропию, которую измерял следующим коротким скриптом на Python:

#!/usr/bin/env python
import sys
from codecs import getreader, getwriter
from collections import Counter
from operator import itemgetter
from math import log, log1p

sys.stdin = getreader('utf-8')(sys.stdin)
sys.stdout = getwriter('utf-8')(sys.stdout)

def H(vec, correct=True):
    """Calculate the Shannon Entropy of a vector
    """
    n = float(len(vec))
    c = Counter(vec)
    h = sum(((-freq / n) * log(freq / n, 2)) for freq in c.values())

    # impose a penality to correct for size
    if all([correct is True, n > 0]):
        h = h / log1p(n)

    return h

def main():
    k = 1
    lines = (_.strip() for _ in sys.stdin)
    hs = ((st, H(list(st))) for st in lines)
    srt_hs = sorted(hs, key=itemgetter(1), reverse=True)
    for n, i in enumerate(srt_hs[:k], 1):
        fmt_st = u'{r}\t{s}\t{h:.4f}'.format(r=n, s=i[0], h=i[1])
        print fmt_st

if __name__ == '__main__':
    main()

Скрипт читает из стандартного потока ввода и выдает команды с наибольшей энтропией. Я использовал вывод лексера в качестве данных для расчета энтропии:

$ sort normal_cmds.txt | uniq -c | sort -nr | sed "s/^[ \t]*//" | \
    awk 'BEGIN{OFS="\t";}{if ($1>100) print $1,$2}' | \
    cut -f2 | ./entropy.py

1 ggvG$"zy 1.2516

Я отбираю команды, которые выполнялись более 100 раз, а затем нахожу среди них команду с наибольшей энтропией. В результате анализа была выделена команда ggvG$''zy, которая выполнялась 246 раз за 45 дней. Команда выполняется с помощью 11 достаточно неуклюжих нажатий клавиш и копирует весь текущий буфер в регистр z. Я обычно использую это команду для перемещения всего содержимого одного буфера в другой. Конечно, добавил в свой конфиг новый shortcut

nnoremap <leader>ya ggvG$"zy

Выводы


Мой матч в vim-крокет определил 3 оптимизации для уменьшения количества нажатий клавиш в vim:
  • Использование команд навигации ^U ^D вместо ^E ^Y
  • Предотвращение автоматического сворачивания текста в буфере для избежания zR
  • Создание shortcut'а для многословной команды ggvG$''zy

Эти 3 простых изменения спасли меня от тысяч ненужных нажатий клавиш каждый месяц.

Части кода, которые я представил выше, немного изолированы и могут быть сложны для использования. Чтобы сделать шаги моего анализа понятнее, я привожу Makefile, который показывает, как код, содержащийся в моей статье, совмещается в единое целое.

SHELL           := /bin/bash
LOG             := ~/.vimlog
CMDS            := normal_cmds.txt
FRQS            := frequencies.txt
ENTS            := entropy.txt
LEXER_SRC       := lexer.hs
LEXER_OBJS      := lexer.{o,hi}
LEXER_BIN       := lexer
H               := entropy.py
UTF             := iconv -f iso-8859-1 -t utf-8

.PRECIOUS: $(LOG)
.PHONY: all entropy clean distclean

all: $(LEXER_BIN) $(CMDS) $(FRQS) entropy

$(LEXER_BIN): $(LEXER_SRC)
    ghc --make $^

$(CMDS): $(LEXER_BIN)
    cat $(LOG) | $(UTF) | ./$^ > $@

$(FRQS): $(H) $(LOG) $(CMDS)
    sort $(CMDS) | uniq -c | sort -nr | sed "s/^[ \t]*//" | \
      awk 'BEGIN{OFS="\t";}{if ($$1>100) print NR,$$1,$$2}' > $@

entropy: $(H) $(FRQS)
    cut -f3 $(FRQS) | ./$(H)

clean:
    @- $(RM) $(LEXER_OBJS) $(LEXER_BIN) $(CMDS) $(FRQS) $(ENTS)

distclean: clean
Перевод: Seth Brown
erthalion @erthalion
карма
25,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (46)

  • +6
    Вот в этой команде: ggvG$«zy вместо кавычек должен стоять знак дюйма: ggvG$"zy. Тогда действительно получится команда копирования всего буфера в регистр z

    пс. это, видать, ошибка хабровского парсера.
    • 0
      Да, она — спасибо, что заметили =)
  • 0
    Если у вас клавиатура со стандартной раскладкой, попробуйте повесить Ctrl на клавишу Caps Lock. С таким положением Ctrl сочетания с ним становятся в разы удобнее.
    • 0
      Это всё равно не отменяет мускулисткого мизинца вимовода.
      • 0
        Зато делает этот мизинец не таким кривым.
        • 0
          У меня на капсе стоит английская раскладка, а на shift-caps — русская. Вакантное место уже занято.

          Я видел психов, которые пробел перевешивали на Alt-Gr, а на пробел вешали контрл. Вот у них с мизинцами, наверное, всё хорошо.
          • 0
            Хитмап в топике показывает, что у автора капс не используется вообще. Возможно это артефакт анализа.

            Вы так часто переключаете раскладку в пределах одного окна (предполагая, что состояние раскладки запоминается пооконно)?
            • 0
              Ага. Любые разговоры, а ля Habrahabr — это постоянное переключение раскладки. То Caps надо латиницей написать, то Shift, то Alt. Не говоря уже про более серьёзные разговоры в районе python, системного администрирования, linux и т.д.
              • 0
                Ну да, я мог бы и сам догадаться, 10534 комментария требуют изрядного количества переключений =)
            • +1
              Автор оригинала не подвержен проблеме смены раскладок и кодировок текста :)
          • 0
            А для меня самая удобная клавиша переключения языков — правый Alt. Правда у меня MS Natural клавиатура и печатаю слепой печатью, так что правым большим пальцем нажимать очень удобно.
          • 0
            А то интересная идея…
      • +1
        Сравнили… Есть ещё емаксеры. Правда, нормальные быстро покупают педали, чтобы не перекачивать мизинец.
        • +2
          Под Emacs надо ctrl не только на caps, но и на return. Серьезно, с таким конфигом в Emacs очень удобно и быстро работать. В bash/zsh с таким контролом тоже очень удобно, тем более там из коробки емаксовые хоткеи: ⌃-n, ⌃-p, ⌃-m и т.д., всё на контроле по-умолчанию. Для OS X keyremap4macbook хак может накрутить такие хоткеи для всей системы, ⌃-m жать удобнее, чем return (который у нас теперь ctrl).

          P.S. Вот чтобы не превращать OS X в Emacs я сейчас привыкаю к виму, не шучу =)
      • 0
        Зачем нужен мизинец жать на контрол? Надо жать костяшкой ладони, куда мизинец приходит.
        Делаю так с глубокого детства, в связи с чем не понимаю этой повальной мании капслокизации :)
        • +3
          попытался так нажать ctrl-z, остался в недоумении.
          • 0
            Видимо с детства просто нужно тренировать…
            • 0
              Не, если серьёзно, то как нажать ctrl-z, придерживая ctrl краем ладони?
              • 0
                Большим пальцем, разве что.
          • 0
            Z жмётся большим пальцем.
            хотя я могу и безымянным, но большим удобнее.
            • 0
              Попробуйте это сделать на Macbook или любой другой клавиатуре Apple
              • 0
                Да, клава у маков — говно. Многократно в этом убеждался :)
        • 0
          Зависит от клавиатуры. Например, на MS Ergo 4k так нажать Ctrl чертовски сложно, из-за подушек для запястий.
          • 0
            если вы используете клавиатуру, заточенную для машинописи, а не для вашей работы — это ваша проблема. :)
            с обычными клавами (A-Shape мой выбор) это получается прекрасно и очень удобно.
            • 0
              Для моей работы она вполне подходит, а вот плоские клавиатуры мне неудобны.
              • 0
                A4Tech KB(s)-8 совсем не плоская.
                Плоские я не могу использовать, так как автоматически включается моторика ноутбука (с другой раскладкой)
                • 0
                  Такая?:


                  Она плоская, насколько я вижу. Все кнопки в одной плоскости.
                  • 0
                    нет, такая:


                    высокие кнопки, долгий ход, подставка под кисти, слэш правее шифта и энтер сапогом.
                    при этом разделение уровня кнопок по этажам ощутимое и A-Shape.
                    • 0
                      Все кнопки в одной плоскости, увы.
                      • 0
                        Не понимаю, о какой плоскости идёт речь.
                        • 0
                          Не понимаю, о какой плоскости идёт речь.

                          О плоскости в ее геометрическом понимании, конечно.

                          Для сравнения взгляните на ergo 4000 (к сожалению, в бюджетном диапазоне клавиатур эта — единственная с такой компоновкой):



                          Вот у нее две левая и правая половины находятся в разных плоскостях. Центральная часть выше. В вашей клавиатуре такого нет, у нее все кнопки расположены в одной плоскости.
                          • 0
                            А я «ломаные» клавиатуры вообще не перевариваю — руки зажимаются жёстко на своей половине. а я середину расшариваю.
    • 0
      Спасибо за наводку, из-за этого мне было удобнее и быстрее нажимать esc чем ^+[.
      В OS X это делается просто: Apple Menu -> System Preferences -> Keyboard -> Keyboard Tab -> Modifier Keys
  • 0
    Кто-нибудь всерьез думает, что самое главное при программировании это скорость набора?
    Однако…
    • 0
      При чём тут программирование). Вы не понимаете сути оптимизации).
      • –1
        Да неужели? :)))
        А мне всегда казалось, что лучше оптимизировать скорость соображалки, чем скорость пальцев.
        Но если уж совсем просто, то лучше чуть посидеть на ладошках. Такой термин знаком? ;)
        • +1
          Вы играете на музыкальном инструменте? Хоть каком нибудь. Если да, то легче будет согласиться, что иногда «узкое место» именно в пальцах)
          Оптимизация «соображалки», конечно, важна и нужна, но не только её необходимо развивать.
          • 0
            Аналогия с музыкальным инструментом не годится :)
            Вот в соответствии с законом парных случаев сегодня опубликовано в Джо Армстронг об инструментах разработчика
            Есть два тип программистов. Одни пишут 10 строк в день и уверенно двигаются к конечному продукту. Другие — пишут 100 или даже 1000 строк в день но топчутся на месте. Они никода не получат продукт, потому-что не понимают какую задачу решают. Они пишут тонны кода, чтобы это понять. Это создает иллюзию занятости и неплохо оплачивается.
            • 0
              Безусловно, необходимо больше думать, чем набивать строки в редакторе. Но часто мысль развивается в процессе созерцания и создания кода, и в этом случае чем меньше препятствий для воплощения мысли в виде текста, тем лучше.
            • 0
              Не спешите отрицать. И вообще, пожалуйста, не спешите).
    • 0
      Кто-то всерьез думает, что при наборе главное — не убить пальцы. Меньше нажатий — меньше нагрузка.
    • 0
      Не главное, но я многократно упирался в скорость набора.
      • +1
        Ну хорошо, пусть скорость набора важна.
        Кто ж спорит, что шоткаты — это супер, и мышью не надо возить и целиться, и отрабатывают они быстрее.

        Но на самом деле меня порадовала «методика», описанная в этой статье.
        У меня больше к ней вопросов.
        Притянутая за уши шенноновская энтропия — это за гранью добра и зла.
        Чтобы не показаться голословным, попробуйте скрипт на двух строках «test» и «testtest».
        Кому лень, привожу результаты — 0.932002401839 и 0.68267941997, соответственно.
        Что труднее набрать?
        Ох уж это наукообразие…
        • 0
          Да, testtest сложнее, так как буквосочетание «tt» редкое!
          При попытке набрать у меня стабильно выходит «testest», за счет микромоторики и мышечной памяти.
          • 0
            Да про буквосочетания я и вообще не говорю :)))
            Например, любая строка, состоящая из любого количества повторений одного символа ('tt', 'tttttt', 'ttttttttt' и т.д.) по результатам этой функции будет иметь «энтропию» ноль :)
            Зато круто — для «оптимизации» использовал информационную энтропию.
            Так сказать, стоял на плечах титанов.
            Только надо понимать что используешь и как оно устроено…
            Вот просто с налёта я бы мерой сложности установил бы (длину пути пальцев, которую нужно пройти, + количество нажатий), требующихся для набора той или иной волшебной команды.
  • 0
    {ответ выше}

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.