Vimクロケット

私から翻訳者は絶対にいませんが、この記事をすり抜けることはできませんでした。それはクールな波を放射し、その中の禅の集中がロールオーバーするからです。 したがって、歓迎します。



はじめに



私は最近VimGolfと呼ばれる面白いゲームを発見しました。 このゲームの目的は、可能な限り少ない数のキーストロークでテキストをあるフォームから別のフォームに変換することです。 このサイトでさまざまなパズルをプレイしている間、私は興味がありました。どのようなテキスト編集の習慣がありますか? Vimでテキストを操作する方法をよりよく理解し、ワークフローで非効率な瞬間を見つけることができるかどうかを確認したかったのです。 私はテキストエディターで膨大な時間を費やしているため、わずかな不規則性さえ排除することで、生産性が大幅に向上します。 この投稿では、分析と、Vimを使用する際のキーストローク数の削減方法について説明します。 このゲームをVim Croquetと呼びました。



データ収集



私は分析をデータ収集から始めました。 コンピューターでのテキスト編集は常にVimを使用して行われるため、45日間はscriptoutフラグを使用してキーストロークを記録しました。 便宜上、ログにクリックを記録するエイリアスを作成しました。



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





その後、取得したデータを解析する必要がありましたが、それほど簡単ではありませんでした。 Vimはモーダルエディターであり、1つのコマンドが異なるモードで複数の異なる意味を持つことができます。 さらに、コマンドはコンテキスト依存であり、vimバッファー内で実行される場所によって動作が異なる場合があります。 たとえば、通常モードのcibコマンドは、コマンドが角かっこ内で実行されるとユーザーを編集モードにしますが、角かっこ外で実行されるとユーザーを通常モードのままにします。 cibが編集モードで実行された場合、完全に異なる動作をします- 「cib」文字を現在のバッファーに書き込みます。



antlerparsecなどの産業用ライブラリーや、 vimprintに特化したvimプロジェクトなど、vimコマンドを解析するためのいくつかの候補を見ました。 少し考えてから、私は自分のツールを書くことにしました。 非常に複雑なパーサーの研究に多くの時間を費やすことは、このタスクには不合理であると思われました。



収集したキーストロークを個々のvimコマンドに分割するために、haskellで湿ったレクサーを作成しました。 レクサーは、 モノイドを使用して、ログから通常モードのコマンドを抽出し、さらに分析します。 レクサーのソースは次のとおりです。



 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 sr = 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~
      
      





レクサーは標準入力から読み取り、処理されたコマンドを標準出力に送信します。 上記の例では、生データは2行目にあり、処理結果は次のようになります。 各行は、対応するシーケンスで実行される通常モードコマンドのグループを表します。 字句解析器は、ラベル`Mを使用してバッファーに移動して通常モードで開始し、編集モードでここにテキストを入力し、行をコピー/貼り付けして、コマンドyyp $ bで行の最後の単語の先頭に移動したことを正しく判断しました。 その後、彼は追加のテキストを入力し、最終的に行の先頭に移動して、最初の文字を大文字のコマンド0〜に置き換えました。



キー使用マップ



誓約されたデータを処理した後、 Patrick Wiedによる素晴らしいヒートマップキーボードプロジェクトを分岐し、レクサーの出力を読み取るための独自のカスタムレイヤーを追加しました。 このプロジェクトでは、ESC、Ctrl、Cmdなどのほとんどのメタ文字が定義されていなかったため、JavaScriptでデータローダーを記述し、他のいくつかの変更を加える必要がありました。 vimで使用されているメタ文字をUnicodeに変換し、キーボードに投影しました。 これが500,000に近いコマンドの数で得られたものです(色の強度はキーの使用頻度を示します)。







結果のマップは、Ctrlキーが最も頻繁に使用されることを示しています。vimの多数のナビゲーションコマンドに使用しています。 たとえば、 ControlPの場合は^ p 、または^ j ^ kを介したオープンバッファのループ。



マップを分析するときに目を引いたもう1つの機能は、 ^ E ^ Yを頻繁に使用することでした 毎日これらのコマンドを使用してコードを上下に移動しますが、これらのコマンドを使用した垂直方向の移動は非効率的です。 これらのコマンドのいずれかが実行されるたびに、カーソルは一度に数行だけ移動します。 ^ U ^ Dコマンドを使用する方が効率的です。 カーソルを画面の半分に移動します。



コマンドの使用頻度



キー使用マップは、個々のキーがどのように使用されるかについての良いアイデアを提供しますが、異なるキーシーケンスの使用方法についてもっと知りたいと思いました。 レクサー出力の行を頻度でソートし、1行を使用して最も使用される通常モードコマンドを確認しました。



 $ 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が8位になったことは私にとって驚くべきことでした。 この事実を検討した後、私はテキストを編集する私のアプローチの深刻な非効率性に気付きました。 実際、私の.vimrcでは 、テキストのブロックを自動的に折りたたむように指示されています。 しかし、この構成の問題点は、テキスト全体をほとんどすぐに展開したため、これは意味がありませんでした。 そのため、 zRを頻繁に使用する必要性をなくすために、この設定を構成から単に削除しました



チームの難易度



私が見たかった別の最適化は、通常モードのコマンドの複雑さです。 毎日使用しているコマンドが多すぎるのにキーストロークが必要なコマンドを見つけることができるかどうかを知りたいと思いました。 このようなコマンドは、実行を高速化するショートカットに置き換えることができます。 コマンドの複雑さの尺度として、次の短い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コマンドが選択され 、45日間に246回実行されました。 このコマンドは、11回の不器用なキーストロークを使用して実行され、現在のバッファー全体をレジスターzにコピーします。 通常、このコマンドを使用して、あるバッファーの内容全体を別のバッファーに移動します。 もちろん、設定に新しいショートカットを追加しました



 nnoremap <leader>ya ggvG$"zy
      
      





結論



私のvimクロケットの試合では、vimのキーストローク数を減らすための3つの最適化が特定されました。



これらの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
      
      






All Articles