Vimのterminalでパイプを使う

この記事はVim Advent Calendar 2021の9日目の記事です。

Vimにterminal機能が追加されてずいぶん経ちましたが、普段はtmux上でVimを使っていたので実際のところ使用頻度はそんなに高くありませんでした。
たまに使った時は出力がVimの中に閉じているため、検索したり編集したりはtmuxのペイン分割と比べてやりやすいなと思うことがあったくらいです。

ただ、最近になって似たようなコマンドを何度も実行することが増えてきて、その度にシェルの履歴から探してきてはコマンドライン引数を変更して実行するのが煩わしくなってきました。
例えば以下のようなコマンドです。

go test \
    # 1. 詳細な出力が欲しい(-v)
    # 2. カバレッジが取りたい(-covermode, -coverprofile, -coverpkg)
    # 3. 実行するテストを指定したい(-run)
    # 4. goldenファイルを更新したい(-golden)
    # 5. テストの分析がしたい時は出力を自作コマンドに流し込む

1〜4までは:executeを使って組み立てたcmdを実行するようにすれば良いので簡単です。

:execute 'terminal ' . cmd

ところが5で出力を流すのに、パイプ(|)をどのように使えば良いのかわかりませんでした。
いくつか試してみたところ、bash(やzsh)の-cオプションに""で実行したいコマンドを渡してあげれば良いことがわかりました。

" grepが効かない(`|`以降もvimコマンドの引数として扱われる)
:terminal vim -h | grep vimrc

" エラー
:terminal bash -c 'vim -h | grep vimrc'

" OK
:terminal bash -c "vim -h | grep vimrc"

これを自作コマンドにしておくと簡単に実行できます。

当初は柔軟に実行できる形にしようと思っていましたが、組み合わせはある程度固定化されていたので3、4パターンほどコマンドとして定義しておき、必要に応じて修正するような使い方をしています。
シェルスクリプトMakefileにしておくのも手かと思いますが、管理の手間などを考えると自分にとってはvimrcに書いておくのが一番楽でした。

curlとjqを組み合わせても良さそうなので、必要になったら同じようにしてやってみようと思っています。

vim-lspのCallHierarchyをツリーっぽく表示する

リファクタリングしたりコードを調べたりする時、呼び出し元を探すのにLspReferencesLspCallHierarchyIncomingを使っていた。
ただ、どちらも1階層分しか表示してくれず、呼び出し元が遠いと影響範囲が把握しにくかったのでquickfixに結果をマージして表示するコマンドを作ってみた。

command! AppendCallTree call s:append_tree(':LspCallHierarchyIncoming')
command! AppendRefTree call s:append_tree(':LspReferences')

augroup AppendTree
    autocmd!
augroup END

function! s:append_tree(cmd) abort
    autocmd AppendTree BufWinEnter quickfix let s:lsp_done = 1

    copen                            " quickfixに移動し、
    let l:pos = line('.')            " 現在の行番号と、
    let l:parent_tree = getqflist()  " 内容を取得し、
    call setqflist([])               " いったん空する
    let l:level = count(l:parent_tree[l:pos-1].text, '⬅️  ')
    wincmd p

    " 元のバッファで指定したコマンドを実行し、
    let s:lsp_done = 0
    execute a:cmd

    " 完了するかある程度時間が経過するまで待つ
    let l:cnt = 0
    while !s:lsp_done && l:cnt < 100
        sleep 10m
        let l:cnt += 1
    endwhile

    let l:child = getqflist()
    if len(l:child) != 0
        " 新たに取得した分は先頭に⬅️を付けて元の位置の下に挿入する
        call extend(l:parent_tree, map(l:child, 'extend(v:val, {"text": repeat("⬅️  ", l:level+1) . v:val.text})'), l:pos)
    endif

    " 結果(取得できなかった場合は元の内容)をquickfixに表示し、
    " 次の場所にジャンプする
    call setqflist(l:parent_tree)
    execute 'cc ' . string(l:pos + 1)

    autocmd! AppendTree
endfunction

AppendCallTree実行後は@:などで繰り返せるので調査が楽になった。

f:id:daisuzu:20210312165925g:plain
例: goplsのCallHierarchy

  • LSPを直接呼ぶのは面倒なのでコマンドを実行する形式にした
    • 特にCallHierarchy...
  • 専用バッファよりquickfixの方が何かと扱いやすいのでやらなかった
    • 何も考えずにジャンプできるし
    • フィルタも簡単だし

go/analysisのSuggestedFixでコードを修正する

Goの既存コードを修正するツールを作る時、

  • 既存コードをどう書き換えて
  • 出力して
  • テストするか

を考えなければいけないのが少し面倒だと思っていました。
が、golang.org/x/tools/go/analysisSuggestedFixを使えばすごく簡単にできてしまいます。

golang.org/x/tools/go/analysisstaticcheckgolangci-lintなどの静的解析ツールでよく使われているパッケージです。

例えば以下のような、関数の引数にcontext.Contextがあるかどうかチェックするツールがあったとして、

func run(pass *analysis.Pass) (interface{}, error) {
    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

    nodeFilter := []ast.Node{
        (*ast.FuncDecl)(nil),
    }

    inspect.Preorder(nodeFilter, func(n ast.Node) {
        decl := n.(*ast.FuncDecl)
        if decl.Type.Params.NumFields() > 0 {
            // NOTE: 第1引数のみを文字列でチェックしているので厳密ではない
            if types.ExprString(decl.Type.Params.List[0].Type) == "context.Context" {
                return
            }
        }

        pass.Reportf(decl.Pos(), "missing ctx in parameter")
    })

    return nil, nil
}

これを、もしチェックに引っ掛かったら引数にcontext.Contextを追加できるように変更してみます。

まずはpass.Reportfpass.Reportに変更し、直接Diagnosticを渡せる形にします。

pass.Report(analysis.Diagnostic{
    Pos:     decl.Pos(),
    Message: "missing context in parameter",
})

そしてSuggestedFixesとしてコードを変更する場所(PosからEnd)と書き換え後のコード(NewText)を渡します。

pass.Report(analysis.Diagnostic{
    Pos:     decl.Pos(),
    Message: "missing context in parameter",
    SuggestedFixes: []analysis.SuggestedFix{{
        Message: "add ctx to parameter",
        TextEdits: []analysis.TextEdit{{
            Pos:     decl.Pos(),
            End:     decl.Type.Params.Closing + 1,
            NewText: b,
        }},
    }},
})

書き換え後のコードは標準パッケージのformat.Nodeを使って作ります。

func newText(pass *analysis.Pass, decl *ast.FuncDecl) ([]byte, error) {
    // Godoc、戻り値、関数の中身は使わずにコードを整形する
    f := &ast.FuncDecl{
        Recv: decl.Recv,
        Name: decl.Name,
        Type: &ast.FuncType{
            Params: &ast.FieldList{
                List: append([]*ast.Field{{
                    Names: []*ast.Ident{{Name: "ctx"}},
                    Type: &ast.SelectorExpr{
                        X:   &ast.Ident{Name: "context"},
                        Sel: &ast.Ident{Name: "Context"},
                    },
                }}, decl.Type.Params.List...),
            },
        },
    }

    var buf bytes.Buffer
    if err := format.Node(&buf, pass.Fset, f); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

この書き換えを実際に適用するにはコマンドラインツールとして実行する時に-fixフラグを付けるようにすればOKです。
なお、-fixフラグはunitcheckerだと渡せないため、main.gosinglecheckermulticheckerを使う必要があります。

もしくは、goplsAnalyzerとして組み込むことでエディタと連携して使うことも可能です。
多少作り込みが甘くても、リファクタリングする時だけ以下に追加し、go installして使ってみても良いかもしれません。 https://github.com/golang/tools/blob/gopls/v0.6.4/internal/lsp/source/options.go#L1108-L1150

vim + vim-lspは該当箇所で:LspCodeActionを実行すると呼び出せます。

f:id:daisuzu:20210128120803g:plain
vim-lspのLspCodeAction

テストについてはanalysistest.Runanalysistest.RunWithSuggestedFixesに変更すればgoldenファイルと比較してくれるようになります。

編集を加速するVimのquickfix機能

この記事はVim Advent Calendar 2020の3日目の記事です。
昨日は@mira010さんのvim pluginsをインストールしてみましょうでした。

みなさんquickfixを使っていますか?
Vimのquickfix機能はgrepやmakeなどの結果を保持する専用のバッファと、それを扱うための各種コマンドからなります。
IDEには当たり前のようにあるような機能ですが、Vimの場合は他の機能と組み合わせることで編集操作を格段に効率化できます。

:grep:makeも、

  1. 外部コマンドを指定した引数で実行し、
  2. ファイル名や行番号、メッセージなどの出力を解析し、
  3. ジャンプのために使えるリストを作ってくれる

のは共通です。
このリストはquickfixリストと呼ばれるもので、:copenで専用のウィンドウが開きます。
そして<Enter>キーやダブルクリックで該当行にジャンプします。

ただデフォルトのgrepコマンドはまだしも、makeコマンドは滅多に使わないという人もいるかもしれません。
その際はgrepprgmakeprgオプションで実行する外部コマンドを任意のコマンドに変更できます。
(出力される結果が解析できない形式の場合はerrorformatなども変更する必要があります。)

grepコマンドはgit grep

" スペースはエスケープが必要
set grepprg=git\ grep\ -n\ --no-color

makeコマンドはlinterやタスクランナーなどにするとグッと使いやすくなります。

" 使用例
" :make ./...
" :make --disable-all -E staticcheck
set makeprg=golangci-lint\ run

これらの設定はvimrcに書いておいたり、簡単に切り替えられるコマンドやマッピングを用意しておいても良いでしょう。

しかし、quickfixリストは同時に複数の結果を表示することができません。
そのため、複数のquickfixリストを扱うには:colder:cnewer:chistoryを使って履歴を行き来する必要があります。
もしくはlocationリストを使います。

locationリストとはウィンドウローカルなquickfixリストのことで、コマンドのプレフィックス

だけで、quickfixリストと同じように使えます。

そのため、別のウィンドウやタブページで個別にlocationリストを開くことで複数の結果を表示できます。

さて、ここまで紹介した機能だと便利なジャンプリストでしかありません。
quickfixリストを使ってさらに効率的な編集をするには、:cnextとマクロを組み合わせて使います。

例えばgrepで絞り込んだ行の特定の文字列を置換したければ、以下の操作を(qqなどで)マクロに記録し、

:s/Before/After/
:w
:cnext

大きな数を指定して(100@q1000@qで)実行すると、quickfixリストの最後まで自動的に繰り返してくれます。
リストが指定した数より少なければそこでマクロが止まってくれるので重複実行は気にしなくて大丈夫です。

この時、さらにquickfixリストを絞り込みたくなることがあるかもしれません。 :grepの場合は正規表現を工夫しても良いですが、:packadd cfilterで使えるようになる:Cfilterでquickfixリストを絞り込んでしまうのがとても楽です。

それでも絞り込むのが難しい場合、いったん別のバッファにコピーして編集し、:cbuffer:cgetbufferでquickfixリストを読み込み直しても構いません。
(quickfixリストを:setlocal modifiableして書き換えるのはちょっと乱暴なので非推奨です。)

また、quickfixリスト自体をファイルとして保存しておき、:cfile:cgetfileで読み込み直すことも可能です。
そうするとジャンプ可能なTODOリストとしても使えるので、リストが巨大だったら少しずつ進めたり、他のVimmerと作業を分担する、なんてこともできるかもしれません。

ということで、ぜひquickfix機能を活用してみてください。

本当はより実践的な例として、最近やったことを具体的なコマンド付きで紹介できれば良かったんですが、ある事情でそのヒストリーをロストして再現環境もなく...
覚えている範囲でフワッと紹介して終わります。

目的はterraformerで生成したmonitoring_alert_policy.tfファイルのdocumentationに、フィルター付きでCloud LoggingのWeb画面に飛べるリンクを追加することでした。
そのフィルターの内容はlogging_metric.tfファイルに定義されているものを使います。

  • monitoring_alert_policy.tf(のサンプル)
resource "google_monitoring_alert_policy" "alert_policy_error" {
  display_name = "My Alert Policy(Error)"
  combiner     = "OR"
  conditions {
    display_name = "test condition"
    condition_threshold {
      filter     = "resource.type=gae_app AND metric.type=logging.googleapis.com/user/my-error-metric"
      duration   = "60s"
      comparison = "COMPARISON_GT"
      aggregations {
        alignment_period   = "60s"
        per_series_aligner = "ALIGN_RATE"
      }
      threshold_value = 0.1
      trigger {
        count = 1
      }
    }
  }

  documentation = {
    mime_type = "text/markdown"
    # TODO: ここに↓の形式でリンクを入れたい
    # - [NAME](https://console.cloud.google.com/logs/query?project=PROJECT_ID&query=FILTER)
    content = ""
  }
}
  • logging_metric.tf(のサンプル)
resource "google_logging_metric" "logging_metric_error" {
  name   = "my-error-metric"
  filter = "resource.type=gae_app AND severity>=ERROR"
  metric_descriptor {
    metric_kind = "DELTA"
    value_type  = "INT64"
  }
}

手順:

  1. :grepする
  2. :copenして、不要な行があれば除外する
  3. マクロを記録開始して、
    1. getline()substitute()で検索するmetricの名前を抽出する
    2. logging_metric.tfのバッファに移動する
    3. search()getline()substitute()を使ってfilterを取得する
    4. 元バッファに戻る
    5. documentationのcontentがある行に移動する
    6. リンクを生成して追加する
      • conditionsが複数ある場合はそれぞれのリンクを追加する
      • フィルターはbase64化してクエリパラメータにする必要がある
    7. :wで保存する
    8. :cnextする
  4. マクロの記録を終了する
  5. 1000回ほど繰り返す

実は初めの数回は関数などを使用せず、ノーマルモードコマンドで普通に編集をしていました。
ただ同じような操作を繰り返していることは薄々感じていたのと、残りの件数を見て即マクロに切り替えたという経緯があります。
そのまま続けていたら数時間はかかっていたと思いますが、ほぼ一瞬で終わらせることができました。

明日は@kaneshinさんです。

goplsでworkspace/symbolが使いたい

現状の最新版(gopls/v0.2.2-pre2)では未実装。

daisuzu.hatenablog.com

を書いていたら欲しくなったので第1回 ゴリラ合宿で実装したのがコチラ。

github.com

symbolの探索はServersessionViews()から Snapshotを取ってきて、
KnownPackages()CompiledGoFiles()Parse()して*ast.Fileを引っ張り出して、
あとは地道にWorkspaceSymbolParamsにマッチするものを探していくようにした。

ただ、WorkspaceSymbolParamsの仕様*1がよくわかっておらず、実装の仕方も上記のやり方で良かったのか全然わかっていない。
また、最初は素直にスライスを作るように実装してみたところ、Vimから:tselectなどを実行すると

  • タグが重複したり、
  • 実行するたびに順番が変わったり、

するので、
重複を排除した上で、Vimで指定した文字列に近く、現在のパッケージに近いものを優先するようにしてみた。

とりあえず自分用としては使えそうなので色々と試してみることにする。

*1:特にQuery

VimのtagfuncでLSPを使う

この記事はVim Advent Calendar 2019の6日目の記事です。

今年の4月にv8.1.1228tagfuncという機能が追加されました。
こちらは:tag:tselectなどのタグ系コマンド*1を実行した時、tagsファイルを検索する代わりに呼ばれる関数を設定するためのオプションです。

設定する関数の形式としては次のようなものです。

" pattern: タグ検索中に使用されたタグ識別子
" flags: 関数の挙動を制御するためのフラグのリスト
"   'c' -> ノーマルモードのコマンドで呼び出された
"   'i' -> インサートモードのタグ補完で呼び出された
" info: 以下の情報を持つ辞書
"   {
"     'buf_ffname': 'フルファイル名',
"     'user_data': 'カスタムデータ文字列',
"   }
function! MyTagFunc(pattern, flags, info)
  " タグ情報のリストを返す
  " もしくはv:nullを返すとtagsファイルが使われる
  return [
        \  {
        \    'name': 'タグ名',
        \    'filename': 'タグが定義されているファイル名',
        \    'cmd': 'ファイル内のタグを見つけるためのExコマンド',
        \    'kind': 'タグの種類(Optional)',
        \    'user_data': 'カスタムデータ文字列(Optional)',
        \  },
        \]
endfunction

ヘルプにはtaglist()の結果を並べ替える関数が例として記載されていますが、LSPを使ってタグ情報のリストを生成できればタグジャンプがすごく便利になりそうです。

VimConf 2019ではsettagstack()を使ってタグスタックを直接操作する方法を紹介しましたが、こちらは、

  • タグ系のコマンドを使うために、
    • キーマップの変更が必須
    • Exコマンドはユーザ定義コマンドを定義する必要がある
  • Vim scriptでタグスタックの管理をしなければならない

といったものでした。

これに対し、tagfuncはオプションを設定するだけで、

  • 標準のキーマップ、Exコマンドがそのまま使える
    • タグ補完の時にも使える
  • タグスタックの管理をVim本体に任せられる

と、すごく良さそうです。

試しに任意の文字列で検索するworkspace/symbolvim-lspから呼び出してみます。

function! MyTagFunc(pattern, flags, info) abort
  let l:servers = filter(lsp#get_whitelisted_servers(),
      \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)')
  if len(l:servers) == 0
    echoerr 'not supported: workspace/symbol'
    return []
  endif

  " タグ補完の場合はa:patternの先頭に \< がつくので削除する
  " (サーバ側が正規表現に対応していないかもしれないので...)
  let l:query = a:flags =~# 'i' ?
      \ substitute(a:pattern, '^\\<', '', '') : a:pattern

  let l:ctx = {'result': []}
  for l:server in l:servers
    call lsp#send_request(l:server, {
        \ 'method': 'workspace/symbol',
        \ 'params': {
        \   'query': l:query,
        \ },
        \ 'sync': 1,
        \ 'on_notification': function('s:make_taglist', [l:ctx]),
        \ })
  endfor
  return l:ctx.result
endfunction

" ctx.resultにタグ情報を追加する
func s:make_taglist(ctx, data) abort
  for result in a:data.response.result
    call add(a:ctx.result, {
        \ 'name': result.name,
        \ 'filename': lsp#utils#uri_to_path(result.location.uri),
        \ 'cmd': string(result.location.range.start.line + 1),
        \ })
  endfor
endfunction

上記関数をset tagfunc=MyTagFuncで設定すると、ctagsとほぼ同じ使い勝手のタグジャンプができるようになります。

ただ、workspace/symboltextDocument/definitionなどのようなカーソル位置を起点に対象を探すメソッドとは異なり、100%目当ての場所にジャンプできるとは限りません。

確実にジャンプしたい時はtextDocument/definitionを呼べるような実装にできれば良いのですが、MyTagFuncの引数からだと、それがExコマンドの引数に指定したタグ名なのか、カーソル位置のキーワードなのかを判別できないので、メソッドの呼び分けをするのは非常に困難です。

そのため、tagfuncに設定する関数で全部をやるのではなく、

  • CTRL-]CTRL-W ]textDocument/definition*2を呼ぶ
    • 定義元にジャンプしたい
  • それ以外はtagfuncからworkspace/symbolを呼ぶ
    • 同名の別メソッド/関数やインタフェースの実体などを探したい

というように、用途に応じて使い分けるのがオススメです!

結局settagstack()tagfuncを両方使うことに。。。

[2019/12/06 17:00追記]
flagsで判別可能でした。
@presukuさん、ありがとうございます!

tagfuncだけで完結させるにはMyTagFuncを次のようにします。

function! MyTagFunc(pattern, flags, info) abort
  let l:ctx = {'result': []}

  if a:flags ==# 'c'
    " ノーマルモード(CTRL-]など)の場合はカーソル位置の定義に飛ぶ
    let l:method = 'textDocument/definition'
    let l:servers = filter(lsp#get_whitelisted_servers(),
        \ 'lsp#capabilities#has_definition_provider(v:val)')
    let l:ctx.pattern = a:pattern
    let l:params = {
        \   'textDocument': lsp#get_text_document_identifier(),
        \   'position': lsp#get_position(),
        \ }
  else
    " Exコマンド(:tag {name}など)やタグ補完の場合はa:patternで探す
    let l:method= 'workspace/symbol'
    let l:servers = filter(lsp#get_whitelisted_servers(),
        \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)')

    " タグ補完の場合はa:patternの先頭に \< がつくので削除する
    " (サーバ側が正規表現に対応していないかもしれないので...)
    let l:query = a:flags =~# 'i' ?
        \ substitute(a:pattern, '^\\<', '', '') : a:pattern
    let l:params = {
        \   'query': l:query,
        \ }
  endif

  if len(l:servers) == 0
    echoerr 'not supported: ' . l:method
    return []
  endif

  for l:server in l:servers
    call lsp#send_request(l:server, {
        \ 'method': l:method,
        \ 'params': l:params,
        \ 'sync': 1,
        \ 'on_notification': function('s:make_taglist', [l:ctx, l:method]),
        \ })
  endfor
  return l:ctx.result
endfunction

" ctx.resultにタグ情報を追加する
func s:make_taglist(ctx, method, data) abort
  for result in a:data.response.result
    if a:method ==# 'workspace/symbol'
      let l:name = result.name
      let l:location = result.location
    else
      let l:name = a:ctx.pattern
      let l:location = result
    endif
    call add(a:ctx.result, {
        \ 'name': l:name,
        \ 'filename': lsp#utils#uri_to_path(l:location.uri),
        \ 'cmd': string(l:location.range.start.line + 1),
        \ })
  endfor
endfunction

ただ一番利用頻度の高いgoplsworkspace/symbolに対応していないのでまだ常用できていません。。。

VimConf 2019に行ってきた

今年もVimConf 2019に行ってきました。

今回はテーマが “how to be more productive with Vim?” ということもあったのか、比較的Vim暦が浅い人の登壇が多かったような気もしますが、その割には発表内容が素晴らしいものばかりだったので、やっぱりVimを使う人は(良い意味で)ヤバい人ばっかりだなと思いました。

毎年のことですが、運営のみなさま大変お疲れ様でした。本当にありがとうございます。
おかげでとても楽しく過ごすことができました。


発表メモ

Vim Renaissance by Prabir Shrestha

愛用しているvim-lspの作者によるキーノート。
色々なエディタ・IDEプログラミング言語があった状況からLSPが登場し、そして未来への革命へ、的な。
乱立していると辛いとか、選択肢を広げるようにしているところとか、わかる部分もあれば自分の英語力だとちゃんと理解できているのか不安になるところもあったり...
多くの人が使えるようにするのは大事なことですけど、そこにもトレードオフがあったりするので難しいところですよね。

We can have nice things by Justin M. Keyes

2つ目のセッションはneovim作者によるキーノート。
neovimはあんまり使っていないんですが、その思想とか実装を直接聞くことができてすごく興味深かったです。
UI周りに関する作り込みとかもそうですけど、特に 「Legacy paradox」 の「表面化にある価値のあるモノ」についてはなるほどなーと思いました。

Your Vim is Only for You by mopp

午後の最初のセッション。
自分のVimを改善していくフローは今までそうしてきたことも多々あるし、今後もそうありたいと思っていることなのでとても共感できました。

Grown up from Vim User to Vim plugin developer side by IK

Vimを使い始めてからvim-jpに参加するまでの流れ。
課題を自分が直すしかない!みたいなところは思い当たることがあったり...
最後の「The next OSS contributer is YOU!!!」はとてもやる気が湧き起こる言葉ですね。

Usage and manipulation of the tag stack by daisuzu

こちらを発表してきました。 speakerdeck.com

make test by m-nishi

Vim本体のテストについて。
ターミナルが小さくてfailするのはやったことありますw
自分が以前blogに書いたことを見てくれていたりして、そういったことがこの発表に繋がっているのかなと思うとなんだか嬉しいです。

My Vim life by gorilla0513

1年前にVimを使い初めてやってきたことの軌跡。
ゴリラ.vimとか本とか、近くで関わっていて良く知ってる事もありますけど、ゴリラさんの情熱と行動力は本当にスゴいし尊敬しています!

Using Vim at Work! by Danish Prakash

心理学 + Vimといったとても興味深い発表でした。
UIについては(自分が思うように使えれば良いや、とか思ったりして)あんまり深く考えなかったりもしたけど、

  • キーボードよりマウスの方が効率的な事がある
  • テキストよりもビジュアル情報の方が6万倍速い

と言われると確かになーとなりました。

Let's Play with Vanilla Vim by Hezby Muhammad

プラグインを使わないVanilla Vimの話。
Vimを使いこなすと基本機能だけでもなんとかなってしまう事も多々あるんですよね。 :find とか補完とかタグジャンプとか。
1つ前の発表でも少し出てきましたが、まさか他の発表でタグジャンプがあるとは思っていませんでした。
そして :normalI とか A とか使うやり方はやった事なかったです。

13 Vim plugins I use every day by Tatsuhiro Ujihisa

毎日使っているプラグインの紹介。
ライブコーディングでサーバをパパッと作っていましたが、流れるようなVim捌きが本当に凄かったです!

My dark plugins development history ~ over 10 years ~ by Shougo

shougo wareの開発の歴史。
最近は使わなくなってしまいましたが...第二世代くらいまではすごくお世話になっていました!
今回で最後の登壇になるかもしれないというのがちょっと信じられません。
来年でなくてもまたいつか発表していただければなと思います。