- PVSM.RU - https://www.pvsm.ru -
Переводчик из меня совершенно никакой, но я просто не мог пройти мимо этой статьи, ибо она излучает волны крутости, а концентрация дзена в ней зашкаливает. Поэтому welcome.
Недавно я обнаружил интересную игру под названием VimGolf [1]. Цель этой игры заключается в том, чтобы преобразовать кусок текста из одной формы в другую наименьшим возможным количеством нажатий клавиш. Пока я играл на этом сайте с разными пазлами, мне стало любопытно — а какие привычки редактирования текста есть у меня? Мне захотелось лучше понять способы манипулирования текстом в Vim и проверить, смогу ли я найти неэффективные моменты в моем рабочем процессе. Я провожу огромное количество времени в моем текстовом редакторе, поэтому устранение даже незначительных шероховатостей может привести к значительному увеличению производительности. В этом посте я расскажу о своем анализе и о том, как я уменьшил количество нажатий клавиш при использовании Vim. Я назвал эту игру Vim-крокет.
Я начал мой анализ со сбора данных. Редактирование текста на моем компьютере всегда происходит с помощью Vim, так что в течении 45 дней я логировал любое нажание клавиши в нем с помощью флага scriptout. Для удобства я сделал alias для записи нажатий в лог:
alias vim='vim -w ~/.vimlog "$@"'
После этого необходимо было распарсить полученные данные, но это оказалось не так легко. Vim это модальный редактор, в котором одна команда может иметь несколько различных значений в разных режимах. Помимо этого команды зависят от контекста, когда их поведение может отличаться в зависимости от того, где внутри буфера vim они исполняются. Например, команда cib в нормальном режиме переведет пользователя в режим редактирования, если команда выполняется внутри скобок, но оставит пользователя в нормальном режиме, если она выполнена вне скобок. Если же cib будет выполнена в режиме редактирования, то она будет иметь совершенно другое поведение — запишет символы «cib» в текущий буфер.
Я рассмотрел несколько кандидатов для парсинга команд vim, включая промышленные библиотеки, такие как antler [2] и parsec [3], а также специализирующийся на vim проект vimprint [4]. После некоторых раздумий, я решил написать собственный инструмент, т.к. трата большого количества времени на изучение достаточно сложных парсеров казалось необоснованным для этой задачи.
Я написал сыроватый лексер на haskell'е для разбиения собранных мной нажатий клавиш на индивидуальные команды vim. Мой лексер использует monoids [5] для извлечения команд нормального режима из лога для дальнейшего анализа. Вот исходник лексера:
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),("195130194128195131194189`",n),
("194128195189`",n),("194128kbESC",n),
("194128kb",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 [6] за авторством Patrick Wied [7], и добавил в него собственный кастомный слой для чтения вывода лексера. Этот проект не определял большинство мета-символов, например, ESC, Ctrl и Cmd, поэтому мне было необходимо написать загрузчик данных на JavaScript и внести некоторые другие модификации. Я транслировал мета-символы, используемые в vim, в юникод и спроецировал их на клавиатуру. Вот что у меня получилось на количестве команд, близком к 500 000 (интенсивность цвета указывает на частоту использования клавиш).
На полученной карте видно, что чаще всего используется клавиша Ctrl — я использую ее для многочисленных команд перемещения в vim. Например, ^p для ControlP [8], или цикл по открытым буферам через ^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'ов, которые бы ускорили их выполнение. В качестве меры сложности команд я использовал энтропию [9], которую измерял следующим коротким скриптом на 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:
Эти 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
Автор: erthalion
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/vim/54075
Ссылки в тексте:
[1] VimGolf: http://vimgolf.com/
[2] antler: http://www.antlr.org/
[3] parsec: http://legacy.cs.uu.nl/daan/parsec.html
[4] vimprint: https://github.com/nelstrom/vimprint
[5] monoids: http://en.wikipedia.org/wiki/Monoid
[6] heatmap-keyboard: http://www.patrick-wied.at/projects/heatmap-keyboard/
[7] Patrick Wied: http://www.patrick-wied.at/
[8] ControlP: http://kien.github.io/ctrlp.vim/
[9] энтропию: http://en.wikipedia.org/wiki/Information_theory#Entropy
[10] Источник: http://habrahabr.ru/post/211108/
Нажмите здесь для печати.