暗黒美無王の作り給うた至上のNeoVimプラグインの一つである、Deniteの拡張をPythonで書いてみる。

Sourceについて

簡単な例から

ごたくはともかく、とにかくコードである。その方が日本語よりも通じるはずである。

from .base import Base

class Source(Base):
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'hoge'
        self.kind = 'hoge' # <-今は気にしない

    def gather_candidates(self, context):
        return [
            { 'word': 'This is first entry', },
            { 'word': 'This is second entry', },
        ]

こんなものを <プラグインディレクトリ>/rplugin/python3/denite/source/hoge.py に置く。自分は ~/.vim/rplugin/python3/denite/source/hoge.py においている(正直この辺の仕組みよく分かってない)。

Vim内から :Denite hoge で呼び出せるようになるので、実行すると以下のようになる。

1.png

いい感じ。

もう少し発展

上の例では物足りない。なぜなら我々ギークがやりたいことを実現するためには同期的な処理だけでは済まず、シェルスクリプトやコマンドを非同期で実行しなければならないからだ。Deniteの非同期処理の書き方には作法がある。

以下を <プラグインディレクトリ>/rplugin/python3/denite/source/hoge2.py として書いた。

from .base import Base
from denite import util, process


class Source(Base):
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'hoge2'
        self.kind = 'hoge'

    def on_init(self, context):
        context['__proc'] = None

    def on_close(self, context):
        if context['__proc']:
            context['__proc'].kill()
            context['__proc'] = None

    def gather_candidates(self, context):
        args = ['echo', str(context['args'])]
        # args = ['ls', '-l'] # cwdはどこ?
        context['__proc'] = process.Process(args, context, context['path'])
        outs, errs = context['__proc'].communicate(timeout=0.5)
        if errs:
            return [{ 'word': x, } for x in errs]
        context['is_async'] = not context['__proc'].eof()
        if context['__proc'].eof():
            context['__proc'] = None
        return [{ 'word': x, } for x in outs]

Denite本体の Denite file/recなどを参考に書いた。'__proc' フィールドにProcessを入れるのはDeniteとのやりとりのためではなく単にon_close()kill()するためである(内部の仕組みはわからないがSourceオブジェクトの寿命が不定?)。

communicate() の返り値の第一値は出力だが、改行ごとにsplitされたlistになっている。コメントアウトしてあるls -l を流してみるとわかりやすい。 そして:Denite hoge2:fuga:piyo と実行したのが以下の通りである。

2.png

Sourceに対して:区切りで与えた引数が、context['args']で拾えていることが分かる。とりあえずここまでできればあとはなんとでもなりそうな気がしてくる。

Kind

何かを表示することができたので、次にエントリに対して何かをやってみる。具体的には、開いたり、消したり、プレビューしたりとかである。そのやれることを定義したものがKindである。

from denite.base.kind import Base


class Kind(Base):
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'hoge'
        self.default_action = 'piyo'

    def action_piyo(self, context):
        # 複数エントリを選択できるので配列で周ってくる
        for target in context['targets']:
            self.vim.command('call append(line("."), "{}")'.format(target['word']))

    def action_fuga(self, context):
        for k, v in context.items():
            self.vim.command('call append(line("."), "{}: {}")'.format(k, str(v)))

以下を <プラグインディレクトリ>/rplugin/python3/denite/kind/hoge.py として書いた。

上で作ったSourceの self.kind = 'hoge' というのが self.name = 'hoge' に対応することになる。default_actionでSourceのエントリで単に<CR>を押したときに発火するアクションを決める。この場合はaction_piyo()が実行される。

3.png

def action_fuga(self, context): と定義することでfugaアクションが追加されているのが分かる。このままpiyofugaを選べばaction_piyo()action_fuga()が実行される。見ての通りSourceで選んだものを好きなように処理できるので、ここから先はあなたの完全なる自由の世界というわけである。

さいごに

$ git logをSourceに流して、選んだコミットで変更されたファイルを並べる」拡張を作ったので(むしろこれを作りたいがためにDenite拡張について勉強した)、下にリンクをおいておく。

dotfiles/vim/rplugin/python3/denite at master · endaaman/dotfiles

見た目はこんな感じ。

4.png

5.png

今回の記事には書いてないけどハイライトを付けている。やっぱり色があるとそれっぽく見えて良い。

先行研究