Humanity

Edit the world by your favorite way

プロンプトして実行、のインターフェースの変遷。

こんにちは。
この記事はVim Advent Calendar 41日目の記事です。
Vimmerの話は変態成分多めでほぼ役に立たないから部屋を明るくして離れたところから話半分に聞いてね。
それじゃ、はっじま:wjjjjjjjjjjjjjjjjj

helpの中にあった、ような... 〜inputlist()〜

Vimにはデフォルトで選択肢をコマンドラインに表示しユーザに答を選ばせることのできる関数があります。inputlist()です。
またマウスでのクリックにも対応しています。(マウスサポートが有効になっていれば)
またこれはinput*()関数全般に言えることですが自分で定義した:cmapも使えます。

(以下はhelpからの引用をちょっと改変)

:echo inputlist(['Select color:', '1. red', '2. green', '3. blue'])


ただ自分はこの関数に一つ不満がありました。
それは入力の際に改行を待ってしまうことです。
改行を打たないと次の操作に移ってくれず、
それが1タイプごとに意味を求めるVimmerとしてはイライラするのです。
何か策はないものでしょうか。

デフォルトで用意されていること、それはとってもうれしいなって 〜getchar()〜

Vimにはデフォルトで改行を待たずに入力を受け取ることのできる関数があります。getchar()です。

:echo join(['Select color:', '1. red', '2. green', '3. blue'], "\n")
:echo get({'1': 'red', '2': 'green', '3': 'blue'}, getchar() - 48, 'error')

inputlist()ほどの手軽さはなくなりますし:cmapが効かなくなりますが、一連の動作は速くなるでしょう。
目標は達成したかのように見えました。


しかし、ああ、若いとは素晴らしいことですが、同時に茨道に首をつっこめる好奇心と時間を十分に持っています。
そこで僕が思ったことは、「入力をinputlist()でもget()でも切り替えられて、かつ入力のバリデーションから入力値が合っていた場合に対応する処理の実行もしてくれる関数が欲しい!」でした。若いとは素晴らしいです。
最近も引越しの準備中に昔のCD-ROMがザクザク出てきてソウルジェムが真っ黒になりました。いろんな意味で魔女化が近いかもしれません。

もう誰にも頼らない 〜prompt.vim

おかげで2009年12月頃*1prompt.vimというものが出来あがりました。
prompt.vimはリッチなプロンプト関数を提供するLibrary for .vimrc*2です。
PerlのIO::Promptに影響を受けています。
ただしもし使おうと思ってくれている方、ドキュメントはないので例から感じ取ってください(すみません)
こんな感じで使います。

" set enc=... {{{
function! s:change_encoding()
    if expand('%') == ''
        echohl WarningMsg
        echo "current file is empty."
        echohl None
        return
    endif
    let result = prompt#prompt("re-open with...", {
    \   'menu': [
    \     'latin1',
    \     'cp932',
    \     'shift-jis',
    \     'iso-2022-jp',
    \     'euc-jp',
    \     'utf-8',
    \     'ucs-bom'
    \   ],
    \   'escape': 1,
    \   'one_char': 1,
    \   'execute_if': '<f-value> != ""',
    \   'execute': 'edit ++enc=<value>',
    \})
    if result !=# "\e"
        echo printf("re-open with '%s'.", result)
    endif
endfunction

Map [n] <prompt>a     :<C-u>call <SID>change_encoding()<CR>
" }}}


まずMap [n]はnnoremapと読み替えてください。
は上の方で

DefMacroMap [nvo] prompt ,t

してあるので,tと置き換わります。(さりげなくemap.vimの紹介)


prompt#prompt()関数の部分のみ抜き出してみると、

let result = prompt#prompt("re-open with...", {
\   'menu': [
\     'latin1',
\     'cp932',
\     'shift-jis',
\     'iso-2022-jp',
\     'euc-jp',
\     'utf-8',
\     'ucs-bom'
\   ],
\   'escape': 1,
\   'one_char': 1,
\   'execute_if': '<f-value> != ""',
\   'execute': 'edit ++enc=<value>',
\})

"menu"に与えたリストが選択肢です。
"escape"は1にするとでキャンセルできるようになります。
上の例だと"onechar"*3オプションを1にしているのでgetchar()を使い改行を待たないようになってくれます。
"execute_if"はもしここで与えたが真になれば"execute"オプションに与えたExコマンドが実行されます。


と、まぁオプションに渡す値の統一性がない感じだったり、
デフォルト値が悪いせいでオプション引数多すぎたりと、
所々イケてないのでvim.orgにもアップロードしていません。

他にも'fileencoding'オプションと'fileformat'オプションを変える設定もついでに載せておきます。

" set fenc=... {{{
function! s:change_fileencoding()
    let enc = prompt#prompt("changing file encoding to...", {
    \   'menu': [
    \     'latin1',
    \     'cp932',
    \     'shift-jis',
    \     'iso-2022-jp',
    \     'euc-jp',
    \     'utf-8',
    \     'ucs-bom'
    \   ],
    \   'escape': 1,
    \   'one_char': 1,
    \   'execute_if': '<f-value> != ""',
    \   'execute': 'set fenc=<value>',
    \})
    if enc ==# "\e"
        return
    endif
    let &l:bomb = enc ==# 'ucs-bom'
    echomsg printf("changing file encoding to '%s'.", enc)
endfunction

Map [n] <prompt>s    :<C-u>call <SID>change_fileencoding()<CR>
" }}}

" set ff=... {{{
function! s:change_newline_format()
    let result = prompt#prompt("changing newline format to...", {
    \   'menu': ['dos', 'unix', 'mac'],
    \   'one_char': 1,
    \   'escape': 1,
    \   'execute_if': '<f-value> != ""',
    \   'execute': 'set ff=<value>',
    \})
    if result !=# "\e"
        echomsg printf("changing newline format to '%s'.", result)
    endif
endfunction

Map [n] <prompt>d    :<C-u>call <SID>change_newline_format()<CR>
" }}}

また"menu"にこんな風にネストした構造を与えることもできます。
確かIO::Promptがこれに対応してたのでprompt.vimでも実装したんだったと思います。

echo prompt#prompt("Which editor do you like?: ", {
\                  'menu': {
\                       'Editor': ['Vim', 'Emacs', 'サクラエディタ', 'TeraPad', '秀丸'],
\                       'IDE'   : ['Visual Studio', 'Eclipse', 'NetBeans']
\                   }})

Perl Hacksに載ってたPerlコードをぺたり。

my $device = prompt 'Select your platform:',
                    -menu =>
                    {
                        Windows => ['WinCE', 'WinME', 'WinNT'],
                        MacOS   => {
                                        'MacOS 9' => 'Mac (Classic)',
                                        'MacOS X' => 'Mac (New Age)',
                                    },
                        Linux   => 'Linux',
                    };

prompt.vimで同じことをやっても完璧に動きます。

echo prompt#prompt('Select your platform:', {
\                  'menu': {
\                      'Windows': ['WinCE', 'WinME', 'WinNT'],
\                      'MacOS'  : {
\                                    'MacOS 9': 'Mac (Classic)',
\                                    'MacOS X': 'Mac (New Age)',
\                                 },
\                      'Linux'  : 'Linux',
\                  }})


結局こうしてprompt.vimを作ったものの、自分の中でなんとなく発展しなかったりして一部設定のためだけに.vimディレクトリの中に残っている状態です。
Vimプラグインハブサイトであるvim.orgではkarmaといういわゆるレーティングがあり、Life Changing = +4, Helpful = +1, Unfulfilling = -1 のようにレーティング*4することができるのですが、このkarma(業)が溜まりすぎると魔女化すると言われています。
結局Vimmerの業からは逃れられず、Vi魔女化(いずれVi魔女になる君等のことはVi少女と呼ぶべきだよね)しつつある.vimrcと.vimディレクトリ...
せっかくいろいろ機能追加したのに...こんなのってないよ...あんまりだよ...

私の、最高の友達 〜unite.vim

時は戻すことはできませんが、流れに身を任すことはできます。
それが本当に需要のある操作なら、誰かがきっと作ってくれるはず。
僕等Vimmerはコードで以って目の前のVim(現実)を変えることができる。
コミットログ(歴史)がいくら汚くったっていい。
最後に幸せになれればいいのだから。
それが逸般人であるVimmerとしての生き方だと、僕は思います。*5


それはまだ1年弱前の2010年7月中旬のことでした。githubにShougoさんがunite.vimをアップロードし、そして有志が次々とsourceを(略)そんな訳でVimにはunite.vimというキラープラグインがまた一つ加わった訳です。よかったですね。


ただ考えてみれば「選択して実行」するsourceが見当りません。
unite.vimほどそれに適したプラグインもないと思うのですが...
というわけで記事のために以下のようなものをでっち上げたいと思います。

function! Encodings()
    return {
    \   'menu': [
    \     'latin1',
    \     'cp932',
    \     'shift-jis',
    \     'iso-2022-jp',
    \     'euc-jp',
    \     'utf-8',
    \     'ucs-bom'
    \   ],
    \   'escape': 1,
    \   'one_char': 1,
    \   'execute_if': '<f-value> != ""',
    \   'execute': 'edit ++enc=<value>',
    \}
endfunction
Map [n] <prompt>a     :<C-u>Unite prompt:Encodings<CR>

:Uniteコマンドの引数でメニューを与えるのは辛かったので関数名を渡せばいろいろ挙動変えられるよね、ってことでこんな形にしようとしました。


...がsource作るためにuniteのhelp漁ってたらすでにそれと同じようなmenuというsourceが定義されてました。
すでにsourceを半分近く書いたあとだったのでまたソウルジェムが黒く濁った気がしました。(あれ、いつのまにか朝になってる...)
ともかく:help unite-source-menuから引用します。

                    *unite-source-menu*
menu        予め定義されたメニューを候補とする。いちいちsourceを定義する
    必要がない。引数が与えられない場合、menuの一覧を候補とする。
    メニューの作り方については、|g:unite_source_menu_menus|を参
    照せよ。

ってことで:help g:unite_source_menu_menusを見るとこんな例が。

let g:unite_source_menu_menus = {}
let g:unite_source_menu_menus.test = {
        \     'description' : 'Test menu',
        \ }
let g:unite_source_menu_menus.test.candidates = {
        \       'ghci'      : 'VimShellInteractive ghci',
        \       'python'    : 'VimShellInteractive python',
        \       'Unite Beautiful Attack' : 'Unite -auto-preview colorscheme',
        \     }
function g:unite_source_menu_menus.test.map(key, value)
    return {
        \       'word' : a:key, 'kind' : 'command',
        \       'action__command' : a:value,
        \     }
endfunction

nnoremap <silent> fm  :<C-u>Unite menu:test<CR>

やはり引数にはg:unite_source_menu_menusにあるキー名のみ指定して起動する形になってますね。
しかもより簡潔に実現できるようになってます。
これを.vimrcに書いてfmを押して実行すると

ghci
python
Unite Beautiful Attack

という風に候補が表示されて、エンターで選択すると無事候補に応じたExコマンドが実行されます。やりましたね。


という訳で先程prompt.vimで定義したenc,fenc,ffを変えるマッピングをUniteで定義して終わりにしたいと思います。

" unite-source-menu {{{

let g:unite_source_menu_menus = {}

function! UniteSourceMenuMenusMap(key, value)
    return {
    \   'word' : a:key,
    \   'kind' : 'command',
    \   'action__command' : a:value,
    \}
endfunction


" set enc=... {{{
let g:unite_source_menu_menus.enc = {
\   'description' : 'set enc=...',
\   'candidates'  : {},
\   'map': function('UniteSourceMenuMenusMap'),
\}
for s:tmp in [
\           'latin1',
\           'cp932',
\           'shift-jis',
\           'iso-2022-jp',
\           'euc-jp',
\           'utf-8',
\           'ucs-bom'
\       ]
    call extend(g:unite_source_menu_menus.enc.candidates,
    \           {s:tmp : 'edit ++enc='.s:tmp},
    \           'error')
endfor
unlet s:tmp

Map [n] -silent <prompt>a  :<C-u>Unite menu:enc<CR>
" }}}
" set fenc=... {{{
let g:unite_source_menu_menus.fenc = {
\   'description' : 'set fenc=...',
\   'candidates'  : {},
\   'map': function('UniteSourceMenuMenusMap'),
\}
for s:tmp in [
\           'latin1',
\           'cp932',
\           'shift-jis',
\           'iso-2022-jp',
\           'euc-jp',
\           'utf-8',
\           'ucs-bom'
\       ]
    call extend(g:unite_source_menu_menus.fenc.candidates,
    \           {s:tmp : 'set fenc='.s:tmp},
    \           'error')
endfor
unlet s:tmp

Map [n] -silent <prompt>s  :<C-u>Unite menu:fenc<CR>
" }}}
" set ff=... {{{
let g:unite_source_menu_menus.ff = {
\   'description' : 'set ff=...',
\   'candidates'  : {},
\   'map': function('UniteSourceMenuMenusMap'),
\}
for s:tmp in ['dos', 'unix', 'mac']
    call extend(g:unite_source_menu_menus.ff.candidates,
    \           {s:tmp : 'set ff='.s:tmp},
    \           'error')
endfor
unlet s:tmp

Map [n] -silent <prompt>d  :<C-u>Unite menu:ff<CR>
" }}}

" }}}

まとめ

この記事では僕の「選択して実行する」インターフェースに対する変遷が見てとれますが、どの選択肢が優れている、という訳ではないと思います。
メインで使っているのはunite.vimですが、prompt.vimはinputlist()やgetchar()の機能が引数を変えるだけで使えますし無駄に多機能だし、
inputlist()やgetchar()は手軽だしデフォルトで提供されている関数ってことだけでも価値があると思います。
ただ自分はuniteでprompt.vimの例を実装し直して、あんまり速度とか考えずもうこれでいいやーと.vimrcにあるprompt.vimのコードは消しました。
ファイルのエンコーディング変える程度のものに速度とか求めてもしょうがないし。


というわけで自分はVim Advent Calendar 2週目でした。いつまで続くんでしょうか...

*1:https://github.com/tyru/prompt.vim/commit/1fb53a4706dfd8ced26bdcf8150a9fdc072c6463

*2:BoostのLibrary for Librariesをもじったものですが、今ならLibrary for (Libraries|Vimプラグイン)なvital.vimというVimScript版Boost(といってもいいようなもの)があります

*3:与えるオプション名のアンダーバーは取り除かれるのでone_charでもonecharでもo_n_e_c_h_a_rでも一緒です

*4:匿名でもレーティングできて、かつなぜそのように投票したかがわからないのでいきなりUnfulfillingとかつけられると開発者としては萎えるのだが...

*5:Shougoさんのコミットログが汚いと言ってる訳ではありません(ちょっと粒度がでかいとは思うけど)