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 

ссылка на оригинал статьи http://habrahabr.ru/post/211108/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *