反省はしても後悔はしない

Vim とか備忘録とか。それと関数型言語勉強中

tig なんて目じゃない! Git のログ系 Vim プラグイン gitv & gitv をGit 統合インターフェース化する最強の設定

この記事は Vim Advent Calendar 2012 の 168 日目の記事です。
昨日は id:yonchu さんの accelerated-smooth-scroll という Vimプラグイン を作った (Vim Advent Calendar 2012, 167日目) - よんちゅBlog でした。

はじめに

最近、Git のログを見る系のエントリが多い気がします。今回の Vim Advent Calendar でも

という記事がありましたし、また最近

なんかもありました。
流行ってるのかな???


今回は私が Vim 上で Git のログを見る(+α)ときどうやってるかについて書きます。

gitv!

gitv というプラグインがあります。

上の図のように左側に Git のログ、右側にその時のコミットの diff が表示されます。便利!!
なお、gitv は vim-fugitive に依存しているので vim-fugitive のインストールは必須です。

実は gitv の紹介記事は前書いたことがあるので基本的なことはそちらを参照してください。

vim で gitk 的なことを行う gitv が便利 - 反省はしても後悔はしない。

gitv の使い方応用編

今日の主題はこちらです。
gitv はデフォルトのままでもログを見る分には便利なんですが、それ以外のことはあまり出来ません。*1

そこで、自分の vimrc に gitv を便利に使うための設定を追加します。これにより gitv を単なるログビューアから Git のための統合インターフェースへと昇華させることができます。

下準備

まず準備として vimrc に以下の記述を追加します。

autocmd FileType gitv call s:my_gitv_settings()
function! s:my_gitv_settings()
  " ここに設定を書く
endfunction

*2
gitv を開くと filetype=gitv が設定されるので、これを利用して gitv のバッファだけで使える素敵設定を追加していきます。
以降、設定は上の s:my_gitv_settings() 関数内に書いていきます。

現在のカーソル位置にあるブランチ名を取得してログ上でブランチに checkout する

gitv のログにはブランチ名も表示されますが*3、カーソルをブランチ名に合わせて、そのブランチ名に対して git checkout や reset などの操作ができると便利です。

そのために以下の設定を追加します。

" s:my_gitv_settings 内
setlocal iskeyword+=/,-,.
nnoremap <silent><buffer> C :<C-u>Git checkout <C-r><C-w><CR>

ブランチ名は単なる英数字だけでなく / や - といった記号も使えるので、まずはそれらをキーワードとして含めます。
コマンドモードで <C-r><C-w> を入力するとカーソルの下にあるキーワードを取得できることを利用して、Git のコマンドを組み立てます。
なお、:Git というコマンドは vim-fugitive が提供しているコマンドです。*4
この例だと、ブランチ名の上で C を押すと即座にそのブランチに git checkout することができます。
checkout のところをいろいろ変えればブランチ名を利用する任意のコマンドをすぐに実行することができます。
ちなみに、カーソルを移動させるときは r や R を利用するとブランチの間を簡単に移動できます。

現在のカーソル行の SHA1 ハッシュを取得してログ上であらゆることを実行する

ぶっちゃけ Git はハッシュが全てです。ハッシュの値さえ取得出来れば割となんでも出来ます。いちいち、HEAD からいくつ離れているかを数える必要はありません。

幸い、gitv のウィンドウにはハッシュ値が一番右に表示されています。表示されていれば取得するのは簡単です。

" これは外に定義!
function! s:gitv_get_current_hash()
  return matchstr(getline('.'), '\[\zs.\{7\}\ze\]$')
endfunction

この関数をトップレベルに書いておきます。
あとは、これと expression レジスタ*5を利用して Git のコマンドを組み立てます。

" s:my_gitv_settings 内
nnoremap <buffer> <Space>rb :<C-u>Git rebase <C-r>=GitvGetCurrentHash()<CR><Space>
nnoremap <buffer> <Space>R :<C-u>Git revert <C-r>=GitvGetCurrentHash()<CR><CR>
nnoremap <buffer> <Space>h :<C-u>Git cherry-pick <C-r>=GitvGetCurrentHash()<CR><CR>
nnoremap <buffer> <Space>rh :<C-u>Git reset --hard <C-r>=GitvGetCurrentHash()<CR>

これらの設定により、カーソル位置のコミットに対して rebase, revert, cherry-pick, reset などのコマンドを実行することができます。もちろんそれ以外でもリビジョン*6を指定するコマンドはたいてい可能です。

この設定は非常に強力です。日常の add, commit などは fugitive から(もちろん Vim のマッピングを利用して)実行し、rebase や reset などブランチを使うコマンドを gitv から簡単に行うようにすると、ほぼシェルで git コマンドを叩くことがなくなります。

便利すぎるので、ある日突然の自分の Vim が使えなくなった時に非常にストレスになるので注意しましょう。

ファイルの diff じゃなくて変更されたファイルの一覧が見たい

gitv だけじゃなく tig なんかでもログと一緒にファイルの差分が表示されています。でも時にはファイルの中身よりもどのファイルが変更されたかの方が重要な場合があります。そういう時に、該当のコミットで変更されたファイルの一覧が取得出来れば便利です。

実は、この表示されている diff はファイル名の部分で折りたためるようになっています。すなわち、すべての折りたたみを閉じることでファイル一覧を表示することが可能となります。

この折りたたみを簡単に操作できるように、以下の設定をトップレベルに追加します。

autocmd FileType git setlocal nofoldenable foldlevel=0
function! s:toggle_git_folding()
  if &filetype ==# 'git'
    setlocal foldenable!
  endif
endfunction

そして以下の設定を s:my_gitv_settings 関数内に追加します。

" s:my_gitv_settings 内
nnoremap <silent><buffer> t :<C-u>windo call <SID>toggle_git_folding()<CR>1<C-w>w

これにより、t キーを押すだけですべての diff のウィンドウに対して folding を切り替えます。つまり、ファイルの diff とファイルの一覧を切り替えることができます。
ちなみに、fugitive が nice な foldtext を提供しているのでファイル名だけじゃなくて何行変更があったかも表示されます。*7

まとめ

fugitive と gitv の組み合わせることによって、Git のたいていの作業を Vim 内で完結させることができます。
慣れ過ぎるとシェルで git コマンドを叩くのが苦痛になるので注意。

明日は @s_of_p さんです。

*1:checkout や merge もできますがインターフェースが非常にアレ

*2:例を書いといてなんですが、autocmd には augroup とをつけて vimrc をリローダブルにしましょう

*3:すべてのブランチを表示するには :Gitv --all とします

*4:実は、gitv 上で :Git は上書きされていて fugitive の :Git を呼び出した後、自分自身を更新します

*5:インサート・コマンドモードで<C-r>=を入力することにより Vim の式の結果を挿入できます。:help quote=

*6:ブランチ名とか HEAD^ とか、あとなんだっけ?

*7:foldtext を自分でカスタマイズしてる場合は autocmd FileType git setlocal foldtext=fugitive#foldtext()すると治ります。

はじめてプラグインを作ってみた。それとhelpの書き方など

この記事は Vim Advent Calendar 2012 の 124 日目の記事です。
昨日は @supermomonga さんの vimprocでRubyでプロセス通信・ソケット通信しよう でした。

はじめに

Vim にはもともと colorcolumn という機能があります。
たとえば

setlocal colorcolumn=4

という設定を打ち込むと、下のようになります。

4桁目がハイライトで表示されるようになります。桁位置を綺麗に揃えたいときにたまに役に立ちます。

この colorcolumn を現在のカーソル位置から即座に設定出来れば便利そうだと思い、今回はじめてプラグインを作ってみることにしました。

Vim の colorcolumn の機能を簡単に使うためのプラグインを作ってみた

easy-colorcolumn

インストールは例によって NeoBundle で行います。

NeoBundle 'cohama/easy-colorcolumn'

このプラグインはデフォルトではキーマッピングを提供しないので、.vimrc にこういう設定を書いておきます。(一例です)

nmap <Space>c <Plug>(easy-colorcolumn-toggle)
xmap <Space>c <Plug>(easy-colorcolumn-toggle)
nmap <Space>C <Plug>(easy-colorcolumn-clear)

この設定により、おもむろに <Space>c を押せばカーソル位置の桁が colorcolumn によりハイライトされます。
さらに、ビジュアルモードと合わせて使うと、複数の列を一気に設定できたりもします。わーい(棒

消すときは <Space>C です。現在のバージョンだとプラグイン以外で設定したもの(モードラインや.vimrcに set colorcolumn=.. と書いたもの)も問答無用で消します。余力があれば将来のバージョンで直すかも知れません。

ドキュメント(help)書くときのはなし

プラグインの書き方は @osyo-manga さんの Vim プラグインを github で公開するまで がとても参考になりました。
しかし、この記事ではドキュメントの書き方については言及されていません。
なので、自分なりにまとめます。

まずは doc フォルダを作ってその中に {プラグイン名}.txt というファイルを作ります。

中身を編集していきます。ファイルの1行目に*で囲ったプラグイン名と簡単な説明を書きます。*で囲むとその単語がタグとして認識されるので、:help コマンドで検索できるようになります。その隣の説明は引数なしで:helpしたときに表示されるようになるので書いておくと良いと思います。あと、最後の行にモードラインを書いておきます。ちなみにこの説明は vim のヘルプにあります(:help help-writing)。

 *easy-colorcolumn.txt*  easy to use a |colorcolumn| feature

 vim:tw=78:ts=8:ft=help:norl:noet:fen:fdl=0:

書いたら :e して filetype を help にします。色がつきます。
tw (textwidth) を78に設定するのがデフォルトっぽいです。書くときは1行の文字数が78を超えないようにしましょう。基本的には Vim が自動でフォーマットしてくれますが、追記したりすると超えることがあります。その時は gq コマンドを使いましょう。

それと、@thinca さんの Vim プラグインのヘルプを書く人がするべき設定 にある設定をしとくと良いと思います。
あとは help の中身を書いていきます。まずは最初にAuthor (作者) と License を書いて自己主張しておきましょう。
その後は

  • Contents (目次)
  • Introduction (概要)
  • Install (インストール方法)
  • Mapping
  • Usage (使い方)
  • などなど

を書いていきます。
これらは適宜セクション区切り(====)で区切ります。また、セクションのはじめにヘルプタグを付けておくようにします。既存のタグと被らないように {プラグイン名}-introduction などとします。

==============================================================================
INTRODUCTION                                   *easy-colorcolumn-introduction*

なんとかかんとかの概要です。

最後にContents(目次)のところにセクションのはじめに書いたタグを集めます。

Contents *easy-colorcolumn-contents*

Introduction    |easy-colorcolumn-introduction|
Mappings        |easy-colorcolumn-mappings|
....


完成!

正直プラグインのヘルプ書くときは他の人の書いたものを真似すればすぐ書けます。

まとめ

  • プラグインを作るのは思ったより簡単
  • ドキュメント書くまでがプラグイン
  • でもドキュメント書くのも簡単

今回のことでプラグイン書くのも面白いなと思ったので、今後も思いついたらいろいろ作って行きたいと思います。
cohama の今後の活躍にご期待ください。


次回は @deris0126 さんです。

Functional 花見 2013 に行ってきた

Functional 花見 2013 に行ってきました。
天気は良いし、桜は満開で最高のお花見日和でした。
そして、なぜか電源もWi-Fiもあるったのでハッカソンにも最適な環境でした。

本日の成果

  • @ さんに opam という、OCaml 用のパッケージ管理ツールを教えてもらいました。早速、インストールしようとしたらハマッてしまったのですがそれも mzp さんが一瞬で解決してくれました。mzp さんすごい。
  • @ さんから F# から JavaScript にコンパイルする FunScript というものの存在を教えてもらいました。コンパイルだけでなく、TypeScript の型定義ファイルから TypeProvider で F# の型をつくることもできるらしいです。時間があれば遊んでみたいです。
  • TaPL 和訳本をちょっと解説してもらいました。あと、@ さんと @ さん(多分)に一番最初の演習問題を解説してもらいました。TaPL の訳本は本屋で少し立ち読みして第2章の最初のページですでに圧倒されてしまって買うのを諦めてたのですが、今日の話では2章はとりあえず流して3章から読めばよいとのことだったので今度時間があるときに買って勉強してみようと思います。
  • @ さんと @ さんにそそのかされ勧められて Coq をインストールしようとしました。前半で opam を入れていたので opam install coq で簡単に入るはずだったのですが、あまりのビルドの遅さにバッテリーが耐え切れずにインストール中に強制シャットダウンとなってしまいましたが・・・。→お家帰ってからちゃんと入れました! Coq も今までは名前だけ知っているだけでよく分かっていなかったのですが、今日話を聞いてやっと Coq の概要を理解しました。少し興味が湧いたので触ってみたいです。
  • @ さんが幻の焼酎である「森伊蔵」を持ってきてくれたので少しだけ頂きました。地元鹿児島でもなかなか手に入らないだけあってすごくおいしくて飲みやすい焼酎でした。sunotora さんありがとうございました。
  • 来栖川電算さんのオフィス見学をさせて頂きました。他社さんのオフィスを見学する機会なんてほとんどないので、新鮮でした。広々とした机にデスクトップマシンがどーんとあり、まさにエンジニアのためのオフィスといった感じで、羨ましかったです。来栖川電算さんありがとうございました。


上に書いた以外にもいろいろ興味深い話をいろいろ聞かせて頂きました。花見のはずなのに勉強会以上に勉強になった気がします。みなさんありがとうございました。

課題としては、インプットばかりで自分からアウトプットが全然ないことですかね〜。もっと勉強してアウトプットもできるようになりたいです。

最後に、主催のITプランニングさん、電源確保してくださった @ さん、あと前日の夜から場所取りをしてくださった方、買い出しに行ってくださった方ありがとうございました。

今更ながら F# で逆 FizzBuzz を解いてみた。それと List モナド

背景

先週のなごやかScala #9にて、逆 FizzBuzz 問題というものを Scala で解いたものをコードレビューするというのがありました。
はてなダイアリーふっかつと逆FizzBuzz問題をScalaで解こう(前編) - スノトラさんのつれづれ日記
元ネタは多分これ
逆FizzBuzz問題 (Inverse FizzBuzz) - 猫とC#について書くmatarilloの雑記


私はその時は問題を知らなかったのでコードレビューの時は聞き流していたのですが、帰ってから調べてみたら面白そうだったので挑戦してみることにしました。Scala で書くのは sunotra さんにおまかせするとして、私は F# でやってみました。

ひけっていけいさん → List モナド

元ネタの Scala のコードを読むと [1..1], [1..2], ... , [1..100], [2..2], [2..3], ... というリストのリストから答えとなる文字列にが合致するものを探すアルゴリズムになっています。(正確にはすべての要素を FizzBuzz 化させたものを Map にしていて、入力の文字列のリストをキーとして答えを取り出している)
答えの候補となるものをとりあえず全部並べていく = 非決定計算ということで、コンピュテーション式を使った List モナドで実装してみました。
実はコンピュテーション式よくわかっていないので*1、List モナドのところは以下を参考にしました。
F#で順列(Permutation)と組み合わせ(Combination)。YOU、Listモナドしちゃいなよ。集合モナドもあるよ。 - Bug Catharsis

書いてみた

let fizzbuzz x =
    match (x%3, x%5) with
    | 0, 0 -> ["FizzBuzz"]
    | 0, _ -> ["Fizz"]
    | _, 0 -> ["Buzz"]
    | _, _ -> []

let tofizzbuzz xs = List.collect fizzbuzz xs

type ListBuilder() =
    member this.Bind (m, f) = m |> List.map f |> List.concat
    member this.Return x = [x]
    member this.Zero () = []

let list = ListBuilder()

let answers input = list {
    let! start = [1..100]
    let! end = [start..100]
    if tofizzbuzz [start..end] = input then
        return [start..end]
}

let inverse_fizzbuzz input =
    match answers input with
    | [] -> []
    | xs -> List.minBy List.length xs

printfn "%A\n" <| inverse_fizzbuzz ["Fizz"; "Buzz"; "Fizz"; "FizzBuzz"; "Fizz"; "Buzz"]
(* [9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20] *)

上のコードの answers のところでコンピュテーション式を使っています。本来なら2重ループになるところがすっきり書けていて素敵ですね。

逃げ

F# 力低いので上のコードは突っ込みどころ満載かも知れません。

*1:なんで Zero がいるの?とか

Vim でコピペするときの Tips

この記事は Vim Advent Calendar 2012 の 40 日目の記事です。
昨日は @kokukuma さんのなんかvimがセグメンテーションフォルト吐いたんですけど。。でした。

はじめに

プログラミングに限らず、何かを書くときにコピー&ペーストってよく使いますよね。Vim でも当然その機能はありますが、Vim の場合はレジスタという仕組みによってさらに便利に使うことができます。
今回は、私が最近覚えてすごく便利に思ったコピー&ペーストに関する小ネタを書きます。

前提知識 レジスタとは

文字列を y でコピー(Vim ではヤンクといいます)すると、その文字列は無名レジスタに蓄えられます。d とか c とかで削除した時も同様です。無名レジスタに記録された文字列を貼り付けるには単純に p や P を使います。
"ayy とか "bdw とすると、明示的に a レジスタや b レジスタに書き込みます。こうすると、たくさんの文字列を記録させることができます。明示的にレジスタを指定して貼り付ける時は "ap や "bP などとします。
詳しくは :h registers してください。
ちなみに :reg とすると、現在のレジスタの一覧を参照できます。

0(ゼロ)レジスタで同じ文字列を何度も貼り付ける

y でヤンクした文字列をいろんな場所に貼り付けるということはよくあると思います。普通にいろんな場所に p で貼り付ければいいのですが、困るのが途中で編集をして関係ない文字列を削除してしまった時。せっかくヤンクした文字列が上書きされてしまって貼り付けられなくなってしまった。
実はヤンクされた文字列は 0 というレジスタにも自動的に入ります。そして、削除などでは 0 レジスタは使われません。なので、

"0p

と打てばヤンクされた文字列をいつでも使うことができます。

別解

明示的に _ レジスタに書き込むと無名レジスタは使用されなくなります。なので削除するときに "_dd などとすると、その後も単純に p でヤンクした文字列を貼り付けることができます。

* レジスタクリップボードと連携する

ネットの海をさまよっていたら、Vim のナイスな設定を見つけた!よし、自分の .vimrc にコピペしよう。
あまりお行儀は良くないですが、ブラウザなど他のアプリケーションにある文字列を Vim で使うにはクリップボードを使うと便利です。ブラウザなどで Ctrl+C でコピーしたあと、Vim

"*p

とすると、クリップボードの内容を貼り付けることができます。*1
逆にクリップボードに文字列を書き込むには "*yy などとします。*2

<C-R>でインサートモードから貼り付け

インサートモードにいるときに、ヤンクした文字列を貼り付けたくなったら・・・?インサートモードで

<C-R>"

とすると、ノーマルモードに戻らずに無名レジスタの内容を貼り付けることができます。<C-R>のあとはレジスタを指定できます。<C-R>a で a レジスタの内容を貼り付けます。無名レジスタのときは " です。
あと、<C-R> による貼り付けはコマンドモードでも使えます。

/ レジスタでさっき検索した文字列を挿入

/ や ? で検索した文字列は自動的に / というレジスタに格納されています。これが何の役に立つかというと、正規表現をつかって置換をするときに便利です。
まず、/ を使って正規表現を検索して自分が望む文字列を検索できているかを確認します。

以下を入力すると・・・

:%s/<C-R>/

以下のように即座に置換コマンドにするできます。

追記

単純に前回の検索パターンを使って置換を行うには下記で良いとのこと。

:%s//hoge/

id:deris さん、情報ありがとうございました。

コマンドモードでカーソル位置の単語を貼り付け

地味だけどすごく便利な機能です。
下の図のような(exists にカーソルがある)ときに

このように入力すると

:help <C-R><C-W>

exists 関数についてのヘルプを引くことができます。

おわりに

いかがだったでしょうか。Vim を使いこなしている方には結構当たり前なことだったかもしれないですが、私は最近知ったので半分は自分の備忘録として書きました。
間違いや補足、他にもこんな事出来るよというのがあればコメントいただけると幸いです。

次回は @Qvic_ さんです。

*1:* レジスタがうまくいかないときは + レジスタだとできるかも?

*2:set clipboard=unnamed すると自動でクリップボードにも書き込まれる

Vim で使える Ctrl を使うキーバインドまとめ

キーマップに Ctrl キーを使うものを割り当てたいんですが、既存の機能と衝突するのが怖いので調べてみました。

keybind normal visual insert
<C-a> 数字を加算 なし さっき挿入した文字を挿入
<C-b> 1ページ上にスクロール 1ページ上にスクロール なし*1
<C-c> (検索)コマンドの中止 visual モードの終了 insert モードの終了
<C-d> 半ページ下にスクロール 半ページ下にスクロール 字下げの削除
<C-e> 1行下にスクロール 1行下にスクロール カーソルの下の行の同じ位置の文字を挿入
<C-f> 1ページ下にスクロール 1ページ下にスクロール インデントの再調整
<C-g> カーソル位置とファイルの状態表示 セレクトモードへ移行 1行下の、挿入開始位置と同じ列へ移動
<C-h> カーソルを左に移動 カーソルを左に移動 カーソル前の文字を削除
<C-i> ジャンプリスト中の新しいカーソル位置へ移動 なし =<Tab>
<C-j> =<NL> 1行下に移動 =<NL> =<NL>
<C-k> なし なし 合字を入力
<C-l> 再描画 なし なし*2
<C-m> =<CR> 1行下の先頭に移動 =<CR> =<CR>
<C-n> 1行下へ移動 1行下へ移動 補完(後方検索)
<C-o> 古いカーソル位置へ移動 なし 1つコマンドを実行して、挿入モードに戻る
<C-p> 1行上へ移動 1行上へ移動 補完(前方検索)
<C-q>*3 =<C-v> =<C-v> =<C-v>
<C-r> リドゥ なし レジスタの内容を貼り付け
<C-s>*4 なし なし なし
<C-t> タグスタックをたどる なし 字下げの挿入
<C-u> 半ページ上にスクロール 半ページ上にスクロール 現在の行の入力文字を全て削除
<C-v> 矩形 visual モードへ移行 矩形 visual モードへ移行 次に入力した文字を入力
<C-w> ウィンドウコマンド ウィンドウコマンド 前の単語を削除
<C-x> 数字を減算 なし CTRL-X モードへ (補完やスクロール)
<C-y> 1行上にスクロール 1行上にスクロール カーソルの下の行の同じ位置の文字を挿入
<C-z> サスペンド サスペンド なし
<C-^> alternate ファイルを開く (= :e #) なし IM切り替え
<C-]> カーソル位置の単語でタグジャンプ カーソル位置の単語でタグジャンプ 短縮入力のトリガ
<C-@> なし なし さっき挿入した文字を挿入して insert モードを終了

意外と insert モードでも Ctrl を使ったコマンドがあるのは知らなかったなぁ。

*1:+revins でコンパイルされたときは revins の切り替え

*2:insertmode がオンのときはノーマルモードへ移行

*3:ただし、端末によっては使えない

*4:ただし、端末によっては使えない

vim-smartinput と vim-endwise が競合した時の対処法

vim-smartinput も vim-endwise もどちらもすごく良いプラグインなのですが、2 つ一緒に使ってると smartinput の C 系の中括弧入力がうまくはたらかなくなってしまいました。

↓正常時

if (hoge) {
  #
}

vim-endwise と競合時

if (hoge) {
#}

原因

vim-smartinput も vim-endwise もどちらもインサートモードの <CR> をマッピングしているのでそれが競合したようです。

対処法

vim-endwise が対応する filetype のみでバッファローカルなマップを定義します。

let g:endwise_no_mappings = 1
autocmd CohamaAutoCmd FileType lua,ruby,sh,zsh,vb,vbnet,aspvbs,vim imap <buffer> <CR> <CR><Plug>DiscretionaryEnd

とりあえずは、これで治りました。