koturnの日記

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

denite.nvimのsourceを作ってみる

はじめに

unite.vim に替わる新しいプラグインとして,denite.nvimが開発されている. まだまだ開発途上ではあるものの,じわじわとunite.vimから移行する人が見受けられる. denite.nvimはneovimだけでなく,Vim 8.0以降であれば利用可能であるのも魅力の1つであるだろう.

そこで,(ゴミ)プラグイン作者の一人として,denite-sourceの作成方法について記したいと思う.

source / kind作成

サンプルとして作るもの

何よりもまず,実例を示すのが早いだろう. サンプルとして,以下のようなsourceを作成してみることにしよう.

  • sourceの引数で指定されたディレクトリ(複数指定可)のファイル一覧を表示
  • sourceの引数が指定されなかった場合はホームディレクトリのファイル一覧を候補として表示
  • 選択したファイルの行数を print() する

ファイル構成

以下の構成でプラグインディレクトリを作成する. 今回は denite-source のみのプラグインであるため, plugin/autoload/ も存在しない. 同じものは GitHub: koturn/vim-denite-sampleにも置いてある.

vim-denite-sample/
|
+-.gitignore
|
+-README.md
|
+-LICENSE
|
+-rplugin/
  |
  +-python3/
    |
    +-denite/
      |
      +-kind/
      | |
      | +-sample.py
      |
      +-source/
        |
        +-sample.py

コード

vim-denite-sample/rplugin/pyhton3/denite/source/sample.py

本体同梱のsourceを真似て,sourceを作成する. まず, from .base import Base のようにBaseクラスをインポートし,それを継承して, Source クラスを作成する. Source クラスには必要なメソッドを実装するとよい. 特に, gather_candidates メソッドは候補取得のためのメソッドなので,必ず実装することになる.

# -*- coding: utf-8 -*-
# FILE: sample.py
# AUTHOR: koturn <jeak.koutan.apple@gmail.com>
# License: MIT License

from .base import Base
import glob
import itertools
import os


class Source(Base):
    def __init__(self, vim):
        #  このvimという引数はVimとPythonで相互にやりとりするためのインタフェース
        # :help pyth を参照すれば,おおよそのことが記述してある
        super().__init__(vim)
        # Denite xxx の xxx に相当する部分
        self.name = 'sample'
        # kind名を指定
        self.kind = 'sample'

    def on_init(self, context):
        '''
        このsourceが指定されて起動されたときに呼び出される.
        元のバッファのファイルタイプの取得などに利用する.
        今回は必要ないので,とりあえずechomsgしておく
        '''
        # print() は :echomsg と同等
        print('on_init')
        # filetypeなどの値は,以下のように context のキーとして生やす
        # selfのメンバにするのはよくない
        # context['__filetype'] = self.vim.eval('&filetype')

    def on_close(self, context):
        '''
        Denite終了時に呼び出される.
        '''
        # コンストラクタ以外では,self.vim を参照して,Vimインタフェースを利用する
        print('on_close')

    def gather_candidates(self, context):
        '''
        候補の取得を行う関数
        '''
        # sourceの引数context['args']はユーザがDenite実行時に以下のように : 区切りで指定する
        #   Denite sample:arg1:arg2:arg3
        dirpaths = ['~'] if len(context['args']) == 0 else context['args']
        print(dirpaths)
        candidates = filter(lambda path: os.path.isfile(path),
                        itertools.chain.from_iterable(
                            map(lambda dirpath: glob.glob(os.path.expanduser(dirpath) + '/*'), dirpaths)))
        # 辞書を返す
        #   word: 候補欄に表示される文字列
        #   アクション側で利用するための情報を増やしたい場合は 'action__xxx'
        #   という名前のキーを利用すること
        return list(map(
            lambda candidate: {
                'word': candidate},
            candidates))

vim-denite-sample/rplugin/pyhton3/denite/kind/sample.py

denite-kindはアクション部分を記述するものである. denite-kindに関しても,sourceと同様に from .base import Base として,Base クラスをインポートする. そして,それを継承した Kind クラスを作成し,必要なメンバ,メソッドを実装する. メンバの中で特に重要なのが default_action であり,これは <CR> を押下したときのアクションに相当する.

# -*- coding: utf-8 -*-
# FILE: sample.py
# AUTHOR: koturn <jeak.koutan.apple@gmail.com>
# License: MIT License

from .base import Base

class Kind(Base):
    def __init__(self, vim):
        super().__init__(vim)
        # kindの名前.この名前をsourceで指定することにより,関連付けられる
        self.name = 'sample'
        # デフォルトのアクション(候補上で<CR>を押下したときに実行するアクション)を指定する.
        # self.default_action = 'xxx' と指定すると,このクラスのメソッド:
        # action_xxx() が呼び出される
        self.default_action = 'sample'

    def action_sample(self, context):
        '''
        コンストラクタで指定したように,デフォルトのアクションを記述する関数
        '''
        for target in context['targets']:
            filepath = target['word']
            print(filepath + ' has ' + str(sum(1 for line in open(filepath, encoding='utf-8'))) + ' lines')

    def action_preview(self, context):
        '''
        候補上にカーソルが移動する度に呼び出される関数
        :Denite -auto-preview ... のように -auto-preview オプションを付加すると,previewモードになる
        付加しない場合でも,以下のキーを押下することでpreview動作を行うことは可能
          insertモード: <C-v>
          normalモード: p
        return Trueとしないと,deniteが終了する
        '''
        # context['targets'] に source側のgether_candidatesの返り値が格納されている
        filepath = context['targets'][0]['word']
        print(filepath + ' has ' + str(sum(1 for line in open(filepath, encoding='utf-8'))) + ' lines')
        return True

サンプルの使い方

以下のように起動すると,ホームディレクトリのファイル一覧が表示される. 選択したファイルの行数が print() に渡されるので, :message 等で確認するとよい.

:Denite sample

次のように,sourceの引数とディレクトリを渡すと,ホームディレクトリではなく,そのディレクトリのファイル一覧が表示される.

:Denite sample:~/Desktop

引数を複数指定することも可能だ.

:Denite sample:~/Desktop:~/Documents/

テンプレート

最低限のdenite-sourceを実装するのであれば,以下のようなテンプレートを用意しておくと楽になるだろう. on_initon_close については,必ずしも利用するわけではないので,コメントアウトしてある.

denite-source のテンプレート

# FILE: xxx.py
# AUTHOR: xxx <xxx.yyy.zzz@gmail.com>
# License: MIT license

from .base import Base


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

    # def on_init(self, context):
        # TODO

    # def on_close(self, context):
        # TODO

    def gather_candidates(self, context):
        # TODO: Following code is a sample
        candidates = ['apple', 'banana', 'cake']
        return list(map(
            lambda candidate: {
                'word': candidate},
            candidates))

denite-kind のテンプレート

# FILE: sample.py
# AUTHOR: xxx <xxx.yyy.zzz@gmail.com>
# License: MIT license

from .base import Base


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

    def action_sample(self, context):
        # TODO

    # def action_preview(self, context):
        # TODO

余談

実は,unite.vimにもdeite-sourceが実装され,unite-sourceをdeniteから利用することが可能になった. 以下のようにして,deniteからuniteを利用する.

" xxx がunite-source名
:Denite unite:xxx

このため,既にプラグインにunite-sourceを実装している場合は,改めて同等のdenite-sourceを実装し直す必要はないということだ. しかし,ユーザとしてはdenite-sourceも提供されていた方が気持ちが良いと思われる.

ちなみに,denite-sourceはPythonであるため,当然バイトコードが生成される. そのため, .gitignore に,

*.py[cod]

と追記しておくのがよいだろう.

おまけ

実はちょっとした自作プラグインに取り込んである.

:Denite mplayer

とすることで,デフォルトでは ~/ 以下のファイルを再帰的に表示し,mplayer を用いて,選択したファイルを再生するようになっている.

デフォルトディレクトリは g:mplayer#default_dir を設定することで変更可能であるし,

:Denite mplayer:~/Music/

のように指定してもよい.

また,プレビュー機能にも対応しているため,

:Denite mplayer:~/Music/ -auto-preview

とすることで,Deniteバッファ上で,カーソル移動の度にカーソル下のファイルを再生できる.

このように,ちょっとした自作プラグインに導入することで,格段に便利になる.

まとめ

この記事では,最も単純なdenite-sourceの作成方法を示した. プラグイン作成初心者のdenite-source作成の足掛かりとなれば幸いである.

ただし,あくまで,最も初歩的な作成方法について述べただけであり,触れていない項目の一例として matcherやsorter,converterといった話もある. それらの機能についてはdocやdenite.nvim自体のソースコードを参照して欲しい.

なお,deniteは開発が活発なプラグインであるため,数ヶ月後にはこの記事の内容が古いものとなっている可能性は十分にある.