koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

ctrlp.vimのエクステンションの作り方

はじめに

この記事はctrlpvim/ctrlp.vimのエクステンションの作り方についての記事であり,ctrlpの公式リポジトリのextensionsブランチに書かれているエクステンションの作り方,および以下の3つの記事の内容に個人的な知見を追加したものである.

CtrlPとは

ctrlpvim/ctrlp.vimは候補絞り込み検索型のファイラである. Shougo/unite.vimと双璧を成すプラグインとして,とても有名である.

オリジナルはkien氏のものであるが,kien氏と連絡が取れないことから,それをforkしたctrlpvim/ctrlp.vimが誕生した.

ctrlp.vimは標準機能であるファイラだけではなく,候補絞り込み検索のインタフェースを利用して,ctagsのタグやundo履歴を候補として表示し,選択された場合にそれに応じたアクションが実行される拡張が本体に付属している.

このctrlp.vimの拡張は,第三者が作成することもできる. unite.vimの様々な拡張(unite source)が多数の人により開発され公開されているように,ctrlp.vimの拡張(以後,エクステンションと呼ぶ)も多数の人により開発され公開されている.

ctrlp.vimのエクステンションは,unite.vimのsourceほど自由に作ることはできないが,シンプルかつ簡単に作ることが可能な点が魅力であると思っている.

2015 / 12 / 01 追記

kien氏と連絡が取れ,2015年11月30日にkien氏のリポジトリではメンテナンスされていないということがオリジナルの方のREADME.mdに明記された

エクステンションの作り方

g:ctrlp_ext_vars というリストを保持するグローバル変数があり,このグローバル変数に作成するエクステンションに関する辞書を追加するだけで,エクステンションの作成は終わりである. そして,関数 ctrlp#init() に適切なインデックスを渡すことで,作成したエクステンションを利用することができる.

ただし,作成するエクステンションは,1つのファイルにし, autoload/ctrlp/sample.vim に置く. リスト g:ctrlp_extensions にエクステンションを登録してある場合,ctrlp.vim本体の読み込み時に,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として読み込みを行うので,エクステンションのファイルは autoload/ctrlp/ 以下に置かなければならない.

何はともあれ,実例を見てもらうとしよう. 以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するというシンプル(かつ何の役にも立たない)なエクステンションのサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPSample  call ctrlp#init(ctrlp#sample#id())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/ctrlp/sample.vim
if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
let s:id = g:ctrlp_builtins + len(g:ctrlp_ext_vars)


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! ctrlp#sample#init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! ctrlp#sample#accept(mode, str) abort
  call ctrlp#exit()
  echo len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

先に述べたように,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として,作成したエクステンションを読みにいくので,autoloadファイルであっても,ファイルの先頭に二重読み込み防止の記述をしておく必要がある.

このサンプルは,koturn/ctrlp-sampleにあるので,インストールして動作を確認したり,改造したりして,ctrlp.vimのエクステンションの作り方の勉強に役立てていただきたい. なお,masterブランチのものは,後述する僕の好みに合わせてコードを洗練してあるので,上記のコード例そのままのものが欲しいなら,not-refiedブランチのものを参照していただきたい.

g:ctrlp_ext_vars に追加する辞書のそれぞれのキー

エクステンションの作成において,要となるのが, g:ctrlp_ext_vars に追加する辞書である. ここでは,その辞書が持つべきキーについて述べる.

必須

  • init : 文字列(evalすると関数呼び出しになる文字列)
    • 評価されるべき関数呼び出しの文字列( 'ctrlp#sample#init()' など)
    • 候補の取得に用い,関数から候補のリストを返却する
    • unite.vimでいうところの gather_candidates
    • CtrlPのバッファに移動した後に呼び出される
  • accept : 文字列(候補が選択されたときに呼び出される関数名)
    • 選択した候補に対するアクションを定義した関数の名前を渡す
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#accept(mode, str) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
      • mode : 候補の選択に入力したキーに応じた文字列が入っている.対応は以下の通り
        • <CR> : 'mode' は 'e'
        • <C-v> : 'mode' は 'v'
        • <C-t> : 'mode' は 't'
        • <C-x> : 'mode' は 'h'
      • str : 選択した候補
    • 通常,関数の先頭で ctrlp#exit() をコールするとよい
    • mode に応じて処理を変更しているエクステンションは少ない
  • lname : 文字列(省略無しのエクステンション名 - long name)
    • ステータスラインに表示される省略無しのエクステンション名
  • sname : 文字列(省略したエクステンション名 - short name)
    • ステータスラインに表示される省略されたエクステンション名
  • type : 文字列で 'line''path''tabs''tabe' のいずれか
    • 'line'
      • それぞれの候補に対し,末尾まで入力文字列とのマッチを行う
    • 'path'
      • それぞれのファイルパスのような形式の候補に対し,末尾まで入力文字列とのマッチを行う
      • それぞれの候補について,入力にマッチした部分がハイライト される
    • 'tabs' (tab start)
      • 最初のTAB文字までマッチする
      • 候補の文字列の最初のTAB以降を,ユーザに情報として提示したい場合に用いる(候補に関するヒントの提示)
    • 'tabe' (tab end)
      • 最後のタブ文字までマッチする
      • 用途は 'tabs' と同じで,どのTABまでをマッチ対象にするかが異なる

任意

  • enter : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが起動する前に呼び出される処理
    • 元のバッファの filetype の取得することなどに用いる
  • exit : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが終了する前に呼び出される処理
  • opts : 文字列(evalすると関数呼び出しになる文字列)
    • オプションの設定などに用いるらしい
    • enter で指定した関数より前に呼び出される
  • sort : 数値( 01
    • 候補をソートするか否か
    • 0 なら候補をソートしない(デフォルト), 1 なら候補をソートする
  • specinput : 数値( 01 ) - special input
    • ..@cd といった特殊な入力を可能にする
      • :h ctrlp-input-formats を参照
    • 0 なら特殊入力を不可に(デフォルト), 1 なら特殊入力を可能にする
    • ..../ と等価で,1つドットを増やす度にディレクトリ階層を1つ上にした入力として解釈され,リターンキーを押すと,ワーキングディレクトリを変更し,再度 init に指定した関数が呼び出され,候補が更新される
      • ... なら ../../.... なら ../../../ に移動する
    • @cd はワーキングディレクトリを変更する
      • 候補選択時に @cd ./plugin/ と入力して,リターンキーを押すと,ワーキングディレクトリを ./plugin/ に変更し,再度 init に指定した関数が呼び出され,候補が更新される
    • CtrlP起動中にワーキングディレクトリを変更したい場合に用いる
    • 候補の絞り込みに用いるのではない
    • ワーキングディレクトリが候補の取得に関わっているものに有効

ドキュメント化されていないもの(任意)

  • nolim : 数値( 01 ) - no limit
    • 0 なら制限あり(デフォルト), 1 なら無制限(スクロール可能)に
    • 日本語などの検索しづらい候補があるときに用いると便利かもしれない
    • mattnさんのctrlpのエクステンションに多く用いられているのを見掛ける
  • opmul : 数値( 01 ) - open multiple
    • 0 なら複数選択不可(デフォルト), 1 なら複数選択可能に
    • 掛け算記号のことではない
    • 複数選択は候補上で <C-z> と入力することで候補にマークを付け, <C-o> を入力することで行うことができる
    • ただし, 複数選択のアクションは "基本的に" 定義できず,ファイルを開くという動作のみ である
      • 存在するファイルのパスに限定されるが,autocmdBufReadCmd を用いることで強引にアクションを定義することは可能(危険)
    • 候補はファイルパスとして解釈され,存在しないファイルパスである候補は選択しても除外される
    • 基本的に定義しない.本体付属のプラグインに用いられている
  • wipe : 文字列(関数名)
    • <F7> で候補を除去するときのアクションを指定する
      • opmul1 のとき,<C-z> でマークを付け,<F7> でマークを付けた候補を指定した関数に渡す
      • マークが1つも付いていない場合,<F7> で全ての候補を除去するかどうかのメッセージが表示される
        • o を入力すると,指定した関数に空リストを渡してコールし,その後ctrlpバッファに戻る
        • c を入力すると,候補を除去せず,ctrlpバッファに戻る
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#wipe(entries) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
    • 新たな候補リスト (引数の除去対象が除去されたリストが想定されている)を関数から返却する
    • 基本的に定義しない.本体付属のプラグインに用いられている

opmulwipe を用い,指定した候補を除去したいのであれば,以下のようにするのがよいだろう.

function! ctrlp#sample#init() abort
  " 候補を他の関数で用いるために,スクリプトローカル変数で保持しておく
  " この s:candidates は ctrlp#sample#exit() でunletするとよい
  let s:candidates = filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
  return s:candidates
endfunction

function! ctrlp#sample#wipe(entries) abort
  return empty(a:entries) ? [] : filter(s:candidates, 'index(a:entries, v:val) == -1')
endfunction

上記の ctrlp#sample#wipe() が分かりにくいのであれば,以下のようにforを用いて書くとよい.

function! ctrlp#sample#wipe(entries) abort
  if empty(a:entries)
    return []
  endif
  for entry in a:entries
    let idx = index(s:candidates, entry)
    if idx != -1
      call remove(s:candidates, idx)
    endif
  endfor
  return s:candidates
endfunction

前者の filter() を用いたものと比べると, a:entriess:candidates の捉え方(ループの主体)が異なっているが,候補に重複が無ければ同一の結果が得られるはずだ. (前者は除去候補にマッチしたものを候補から全て取り除き,後者は候補のうち,最初にマッチしたもののみを取り除く)

Vim scriptのパースは時間がかかる(ループ毎にもパースを行っているはず)ため, s:candidates の数が多くないのであれば,前者の方が高速に動作すると思われる.

CtrlP起動時にワーキングディレクトリを変更する

ctrlp#init() に作成したエクステンションのIDを渡すことで,CtrlPを起動するようになっていることは最初に述べた. 実は, ctrlp#init() は,第二引数に辞書を取ることもでき,その辞書のキー dir の値に,ワーキングディレクトリを指定することで,CtrlP起動時のディレクトリを変更することができる. これは相対パスからファイルを簡単に辿りたい場合などに使用できる.

command! -nargs=1 -complete=dir CtrlPSample  call ctrlp#init(ctrlp#sample#id(), {'dir': <q-args>})

候補のシンタックスハイライト

本体付属のエクステンションやこの記事を参考にすると,CtrlPの候補にハイライトを適用する(すなわち,CtrlPバッファ内でハイライトを行う)には, ctrlp#sample#init() 内で,

function! ctrlp#sample#init() abort
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

などのようにするとよい. 本体付属のエクステンションでは,シンタックスハイライト用の設定を行う部分を関数として切り出し,

function! ctrlp#sample#init() abort
  call s:syntax()
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:syntax() abort
  if ctrlp#nosy()
    return
  endif
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
endfunction

としている( ctrlp#nosy() については後述する).

ctrlp#hicheck() はハイライトのリンクをチェックする関数であり,リンクが存在しないならば,リンクを行う. 実装は非常に単純で,

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#hicheck(grp, defgrp)
    if !hlexists(a:grp)
        exe 'hi link' a:grp a:defgrp
    en
endf

となっている.

また, ctrlp#nosy() (no syntax) はシンタックスハイライトが利用できる環境であるかどうかを判別する関数である. シンタックス機能が利用できないVimや, :syntax enable をしていないVimならば 1 を返却し,シンタックスハイライトが可能ならば, 0 を返却する.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#nosy()
    retu !( has('syntax') && exists('g:syntax_on') )
endf

グローバル変数 g:syntax_on は, :syntax enable とコマンドを実行する(大抵,.vimrcに書かれているだろう)ことで定義される変数で, :syntax off とコマンドを実行すると unlet される. 大抵のユーザはシンタックス機能をオンにしているので,わざわざチェックする必要も無いが,本体付属のプラグインに沿った形に記述しておくと,雰囲気は良いだろう.

シンタックスハイライトの用途としては, typetabs を指定した場合,最初のタブ文字以降を目立たない色にするといったものが考えられる. 実は,先述のハイライトの例は,本体付属の typetabs であるエクステンションにも書かれている「タブ文字以降のハイライトをコメントと同じものにして,目立たなくする」ものである.

改良案

ここまではctrlp.vimのエクステンションの作り方について述べたが,ここからは「エクステンションのコードはこう書くのが好きだ」という個人の意見を書く.

ctrlp.vim本体のグローバル変数の参照

ctrlp.vimの拡張はプラグインとしてオプショナルであり,NeoBundle等で依存関係を明示したくない場合があるかもしれない(そんなユーザがいるのかは疑問であるが). このことについて考えているときに,以下の記事を見て,なるほど,と感じた.

ctrlp.vimのExtensionを書くときに:NeoBundleLazy autoloadを考慮してg:ctrlp_builtinsをそのまま使わない - cafegale

ctrlp.vimの拡張はあくまでオプショナルとしてプラグインに付属させており,NeoBundleに依存関係が記述されておらず,ctrlp.vimが遅延読み込みされるように設定してある場合,先述の記事にあるように,作成したプラグインを読み込みしている段階で g:ctrlp_builtins が定義されていないという問題が起こることもあるだろう. 従って,ctrlpが提供しているautoload関数 ctrlp#getvar() を用いて,ctrlp.vim本体の読み込みを発生させ,ctrlpのグローバル変数が読み込まれるようにした方が望ましいはずだ. そのことを踏まえ,作成するエクステンションのファイルの先頭に,

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

と記述し, g:ctrlp_builtins の代わりに, s:ctrlp_builtins を用いるようにするとよさそうだ.

ただ,プラグインマネージャである neobundle.vim のためだけの記述をするというのは,本質的ではなく,少し嫌な気持ちになる. この g:ctrlp_builtins の取得方法に関しては,個人の好みが大きいだろう. 正直,ユーザがNeoBundleの依存関係をキッチリ書けば問題の無い話でもある.

ちなみに, ctrlp#getvar() は以下のような実装になっている.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#getvar(var)
    retu {a:var}
endf

a:var は文字列と想定されるので,{a:var} は, eval(a:var) と読み替えてよく, a:val を評価した結果に置き換えられる. すなわち, ctrlp#getvar() は, autoload/ctrlp.vim 内で引数を評価した結果を返す関数であり,autoload/ctrlp.vimスクリプトローカル変数を取得することもできる.

g:ctrlp_ext_vars へのエクステンションの辞書の追加

Vimプラグインの拡張機能プラグインを作ってVimをさらに使いやすくしよう - 29th Sta. という記事の例では,

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif

としているが, add() は第一引数のリストに対して破壊的変更を加えるので,返却値を再び第一引数のリストに代入する必要はない(どちらが読みやすいかは置いておくとして). また,スクリプトローカル変数 s:sample_var を保持する必要もない. これらのことから,上記コードは以下のように記述することができる.

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

個人的にはif文を用いずにシンプルに書きたいので,以下のように記述することにしている.

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})

add() の第一引数が即値 [] となることもあるので,この場合は add() の返却値を利用しなければならない.

可能な限り,グローバル関数を使用しないようにする

エクステンション側にグローバル関数を定義し,ctrlp.vimに渡す辞書にそのグローバル関数名を渡しているが,必ずしもそうする必要はない. スクリプトローカル関数であっても,スクリプト番号付きであれば,グローバルに参照することが可能だからだ.

function! s:hoge() abort
  echomsg 'Hello, World'
endfunction
" :call <SNR>18_hoge() などという形で,グローバルに呼び出すことが可能

このことを踏まえ, g:ctrlp_ext_vars に辞書を追加する記述を,以下のような記述に変更した.

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:sid_prefix

僕は,不特定多数の相手(プラグインであったり,.vimrcであったり)にAPIを提供する場面では,グローバル関数を用い,今回のように作成したプラグインから 能動的に 他のプラグインに自身の関数を渡す場合では,スクリプトローカル関数を用いるとよいだろうと考えている. ただ,ここまでしてグローバル関数を嫌う必要があるのかと問われれば,どうなんだろうと思ってしまう. また,スクリプト番号の取得は,コードとしてややごちゃごちゃしており(5行は必要になる),グローバル関数を用いる方がスマートという気もするが,この場では気にしないことにする.

ここまでのことを踏まえ,最初に紹介したカレントディレクトリのファイルを表示し,選択されたファイルの行数を表示するエクステンションを以下のように書き直した.

if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! s:init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimのエクステンションのテンプレート

僕はctrlp.vimのエクステンションを作成する際に,以下のテンプレートを用いている. <+FILEBASE+> を適宜,エクステンションのファイル名からディレクトリ部と,拡張子を取り除いたものにすればよい( :%s/<+FILEBASE+>/\=fnamemodify(expand('%'), ':t:r') ). これを thinca/vim-template などのテンプレートプラグインと組み合わせると,サクッとエクステンションを作ることができるはずだ.

なお,以下のテンプレートには,CtrlPバッファでハイライトするコードは含まれない. 僕の場合,ハイライトが必要なエクステンションを書くことがあまり無いので,必要に応じて追加するようにしているためだ.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'enter': s:sid_prefix . 'enter()',
      \ 'exit': s:sid_prefix . 'exit()',
      \ 'opts': s:sid_prefix . 'opts()',
      \ 'sort': 0,
      \ 'specinput': 0,
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction

function! s:enter() abort
  " Called before s:init()
  " For example: get filetype
endfunction

function! s:exit() abort
  " Called when exit ctrlp
endfunction

function! s:opts() abort
  " Called before s:enter()
  " Set options etc...
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

最小限の機能に絞ったテンプレートならば,以下のようになるだろう.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

基本的にこれらのテンプレートの s:init()s:accept() を中心に実装するだけでよいはずだ.

最後に

ctrlp.vimのエクステンションを作るにあたって覚えなければならないことは,unite.vimより少なく,unite sourceより簡単に作ることができる. 日本人が作ったプラグインには,unite sourceが付属しているものが多いが,ctrlp.vimのエクステンションが付属しているプラグインは少ない. unite sourceを付属させるのであれば,是非ともctrlp.vimのエクステンションも付属させてみてはどうだろうか?

追記

2015 / 12 / 01

何とあのmattnさんにこの記事を取り上げていただいた!

当初は割とパパッと書いた記事で,恥ずかしい間違いなどもあったのだが,まさかmattnさんに紹介していただけると思っていなかった.

mattnさんはブログ中で, g:ctrlp_ext_vars に追加するときに if ~ else を用いるか,get() を用いるかについて堀下げて解説してくださっている. 特に,get() の第一引数に g: という一見変数っぽくないものを与えるあたりについて解説してくださっている.

let s:sample_var = {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

とするより,

let g:ctrlp_ext_vars = get(g:, 'ctrlp_ext_vars', []) + [{
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
\}]

とした方が上級Vimmerに見られるようなので,みなさんも要チェックだ.

ちなみに,Vim scriptはパースがネックである部分もあるので,前者より後者の方が高速に動作するはずだ. カッコよさの他,マシンに優しくしたり,電力消費を僅かに抑えたいのであれば,後者を用いるとよいだろう. (1度しか実行されない処理に何を言っているんだ,というツッコミは無しでお願いしたい)

参考