- PVSM.RU - https://www.pvsm.ru -

Vim-крокет

Переводчик из меня совершенно никакой, но я просто не мог пройти мимо этой статьи, ибо она излучает волны крутости, а концентрация дзена в ней зашкаливает. Поэтому 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 (интенсивность цвета указывает на частоту использования клавиш).

Vim крокет

На полученной карте видно, что чаще всего используется клавиша 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:

  • Использование команд навигации ^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

Автор: 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/