ども、ゴリラです。
今年もやってまいりましたVimアドベントカレンダーです。
今日はpopup windowを使ってファイル一覧をpopup windowで選択して開くプラグインを作ってみようと思います。
プラグイン作ってみたい方は、ぜひ読んでみてください。意外とプラグインは簡単に作れます。
はじめに
まずどんな機能を作るかを整理します。本記事は次の機能を実装することをゴールにします。
- 指定パスのファイル一覧をpopup windowに表示
- 選択したファイルを開く(分割、タブ、カレントバッファ)
ディレクトリ構成
プラグインのディレクトリ構成は以下にします。一般的にautoload
配下にメインの処理関数を定義して、
plugin
でメイン処理の関数を呼び出すコマンドを定義します。
autoload
に定義した関数は、コマンドで呼び出されるまでロードされないようになっています。
これは、Vimの起動時間に影響を与えないようにするためです。
popupfiles.vim/
├── autoload
│ └── popupfiles.vim
└── plugin
└── popupfiles.vim
ディレクトリの置き場
プラグインマネージャーを使う方が多くいると思います、プラグイン開発を行うとき筆者はよくpackages
機能を使用します。
packpath
配下にstart
ディレクトリを作成してそこにディレクトリを置くことで、Vim起動時にプラグインを読み込んでくれます。
ちなみに、筆者の場合は次のようになっています。
/Users/skanehira/.vim/pack/plugins/start/popupfiles.vim
詳しく知りたい方は:h packages
でヘルプを引いてみてください。
ファイル一覧をpopup windowに表示
ファイル一覧を取得
まずはカレントディレクトリ配下のファイルを一覧の配列を返す関数を用意します。
コードはautoload/popupfiles.vim
に記述します。
" パスのセパレーターを取得
let s:sep = fnamemodify('.', ':p')[-1:]
function! s:get_files(path) abort
" `expand()`はパスをフルパスに展開
" 例えば`~/`は`/home/gorilla`に展開
let p = expand(a:path)
let entries = []
" `readdir`は指定したディレクトリ内の要素を配列で返す
for f in readdir(p)
" `.`から始まるディレクトリとファイルをスキップ
if f[0] is# '.'
continue
endif
let file_path = p . s:sep . f
if isdirectory(file_path)
let entries += s:get_files(file_path)
else
if file_path[0] is# '.'
let file_path = file_path[2:]
endif
call add(entries, file_path)
endif
endfor
return entries
endfunction
popup windowを作る
次にpopup windowを作る関数を定義します。popup windowを作る関数はいくつかありますが、今回はpopup_menu
を使用します。popup_menu
を使用することで要素の選択、操作を簡単に実装できます。
こちらも、コードはautoload/popupfiles.vim
に記述します。
function! popupfiles#files() abort
call popup_menu(s:get_files('.'), {})
endfunction
ここで1つポイントです。popupfiles#files()
という関数名はplugin
で呼び出すためにautoload
のルールに従って命名しています。
autoload
の命名ルールはfilename#funcname()
になっています。こうすることで、どのファイルのどの関数を呼び出すかVim側が判断できるようになるからです。
popup windowを呼び出す
メインの処理ができたので、次にplugin
でpopupfiles#files()
を呼び出すコマンドを次のように定義します。
" プラグインを再度ロードしないようにガード
if exists('g:loaded_popupfiles')
finish
endif
let g:loaded_popupfiles = 1
" vi互換の設定を一旦退避
let s:save_cpo = &cpo
set cpo&vim
" コマンドの定義
command! PopupFiles call popupfiles#files()
" 互換設定を戻す
let &cpo = s:save_cpo
unlet s:save_cpo
一度Vimを再起動するか、:source plugin/popupfiles.vim
と:source autoload/popupfiles.vim
でファイルを読み込むかしてください。
適用後:PopupFiles
コマンドを実行すると次のようにpopup windowにファイル一覧が表示されるはずです。
これでpopup windowにファイル一覧を表示させることができました。
ちなみに、popup_menu
ではデフォルトで次のキーが利用できます。
キー | 説明 |
---|---|
j |
次の要素 |
k |
前の要素 |
enter / space
|
要素を選択 |
x / esc / ctrl-c
|
キャンセル |
では、次に選択したファイルを開くキーバインドを追加していきましょう。
選択したファイルを開く
popup_menu
では第1引数に要素、第2引数にオプションを渡すことができます。
もう一度コードを見てみましょう。第2引数は{}
は空のオブジェクトになっています。
このオブジェクトにオプションを定義していきます。
function! popupfiles#files(path) abort
call popup_menu(s:get_files(a:path), {})
endfunction
popup windowにはfilter
というオプションがあります。
このオプションではキー入力を受け付けて何かしらの処理を行う関数を指定することができます。
このフィルター関数、デフォルトではwinid
とkey
の2つの引数を受け取ります。
フィルター関数の定義と呼び出しは次のようになります。
function! popupfiles#files() abort
call popup_menu(s:get_files('.'), {
\ 'filter': function('s:popup_files_filter')
\ })
endfunction
function! s:popup_files_filter(winid, key) abort
" 入力キーに応じた処理
" それ以外は通常のpopup_filter_menuに渡す
return popup_filter_menu(a:winid, a:key)
endfunction
ここで1つポイントですが、function()
で関数への参照を取得して、filter
オプションに渡す必要があります。
また、popup_filter_menu
というVimの組み込み関数がありますが、これはfilter
が何も指定されないときにデフォルトで使用されるフィルター関数になっています。この関数を使用することでj
やk
といったキーバインドの操作が出来るようになります。
しかし、このまま実装しようとすると、
フィルター関数に選択したファイルが渡ってこないため、ファイルを特定できないという問題があります。
そこで、Vim scriptのPartialという仕組みを使用して必要な情報をフィルター関数の引数に追加します。
function()
の第2引数に配列を渡すことで、その中身が関数の引数として先頭に順に追加されます。
function! popupfiles#files() abort
let files = s:get_files('.')
let ctx = {
\ 'idx': 0,
\ 'files': files,
\ }
call popup_menu(files, {
\ 'filter': function('s:popup_files_filter', [ctx])
\ })
endfunction
function! s:popup_files_filter(ctx, winid, key) abort
" 入力キーに応じた処理
" それ以外は通常のpopup_filter_menuに渡す
return popup_filter_menu(a:winid, a:key)
endfunction
idx
はfiles
から選択したファイルを取り出すために必要な配列番地の情報です。
これで必要な情報をフィルター関数内で扱うことができるので、あとはキー入力に応じてファイルを開く処理を実装すれば良いです。
今回は、次のキーバインドを実装することにします。
キー | 説明 |
---|---|
enter | カレントバッファに開く |
ctrl-v | 水平分割で開く |
ctrl-x | 垂直分割で開く |
ctrl-t | タブページで開く |
実装したコードが次になります。
function! s:popup_files_filter(ctx, winid, key) abort
" 入力キーに応じた処理
if a:key is# 'j'
if a:ctx.idx < len(a:ctx.files) -1
let a:ctx.idx = a:ctx.idx + 1
endif
elseif a:key is# 'k'
if a:ctx.idx > 0
let a:ctx.idx = a:ctx.idx - 1
endif
elseif a:key is# "\<cr>"
return s:open_file(a:winid, 'e', a:ctx.files[a:ctx.idx])
elseif a:key is# "\<c-v>"
return s:open_file(a:winid, 'vnew', a:ctx.files[a:ctx.idx])
elseif a:key is# "\<c-x>"
return s:open_file(a:winid, 'new', a:ctx.files[a:ctx.idx])
elseif a:key is# "\<c-t>"
return s:open_file(a:winid, 'tabnew', a:ctx.files[a:ctx.idx])
endif
" それ以外は通常のpopup_filter_menuに渡す
return popup_filter_menu(a:winid, a:key)
endfunction
function! s:open_file(winid, open, file) abort
call popup_close(a:winid)
execute a:open a:file
return 1
endfunction
これで簡易ファイラーの実装は以上になります。
まとめ
いかがだったでしょうか?合計で100行くらいで簡易ファイラーを実装できるVim script、便利ですね。
これをきっかけにプラグインを作ってみようってなってくれたら嬉しいなと思っています。
本記事のソースコードはリポジトリに置きましたので、全体像を掴めていない方は覗いてみてください。
なお、本記事はVim scriptの基本構文について触れていないので、詳しく知りたい方は:h script
でヘルプを参照してください。