Deniteのソースを作ろう

# Deniteとは?

まずは軽く[Denite](https://github.com/Shougo/denite.nvim)について紹介します。
Vim8/Neovimのプラグインの一つです。
[Unite](https://github.com/Shougo/unite.vim)の後継プラグインです。
「ある一覧から特定のデータを検索してアクションを実行する」ことができます。

例えば標準機能である、「ファイル一覧からファイルを検索、選択して開く」です。
このファイル一覧のことをソースと言います。
Deniteはこのソースなどを自分で開発し追加することができます。
ここまではUniteもDeniteも同じですね。

UniteはVim scriptで実装されており、速度が出ないという問題がありました。また複雑化によりメンテが困難になったという話を聞いたことがあります。
そこでDeniteが生まれました。コア機能はPythonで実装されており、理論上Uniteより高速に動作するようになったようです。
またUniteは開発が終了しておりDeniteを使用することが勧められているようです。
>Note: Active development on unite.vim has stopped. The only future changes will be bug fixes.
>Please see Denite.nvim.

以下は有志で紹介されている外部プラグインの一覧です。
https://github.com/Shougo/unite.vim/wiki/unite-plugins
https://github.com/Shougo/denite.nvim/wiki/External-Sources
ここに記載されていないものもありますが、DeniteとUniteを比べるとDeniteは外部プラグインの開発がまだまだ進んでいません。

そこでDeniteのソースの開発方法を軽く説明しようと思います。

## ソースの作り方

DeniteのソースはPython3で書きます。私はPythonについて然程詳しくありません。
なので今回はPythonについての説明などは省きます。
またDeniteのソースコード自体はあまり読んでいないので、曖昧な箇所があります。
確実でない箇所について「思います。」と表現していますので予めご了承ください。

#### ソースを作るにあたって

Deniteでは、
1. 何かの一覧を取得する
2. 検索・選択する
3. アクションを実行する

のが基本でした。まずは何の一覧を取得して、どういうアクションがしたいのか決めましょう。
今回は[ALE](https://github.com/w0rp/ale)の結果を一覧として使いたいとします。
ALEは非同期にlintを走らせるプラグインです。
アクションはlintの警告などがあった箇所にジャンプするにします。
`:Denite ale`と実行したいのでソース名は`ale`とします。

#### 調査

少し調べた結果、ALEの結果を取得するメソッドが`ale#engine#GetLoclist`だということが分かりました。引数はバッファ番号でした。
結果にはバッファ番号、行番号、カラム、結果のテキスト、タイプなどが入っています。
検索対象は「結果のテキスト」にしてジャンプ先は「バッファ番号、行番号、カラム」としましょう。

また、このアクションをDeniteで実現するにはfile kindが良さそうという事が分かりました。(`:help denite-kind-file`)
kindについては後で簡単に説明します。

#### 準備

`:help denite-create-source`を参考に作っていきます。
さてファイル構成を見てみましょう。
```
.
├── rplugin/
│   └── python3/
│       └── denite/
│           └── source/
│               └── ale.py
├── LICENSE
└── README.md
```
LICENSEやREADMEは必要に応じて配置してください。
`rplugin/python3/denite/source/*.py`に開発したいソースのファイルを配置します。`ale.py`がソースを定義しているファイルです。
ソース名と同一のファイル名にすると分かりやすいでしょう。

では、コーディング。

ソースにはBaseクラスがあり、これを継承する必要があります。
https://github.com/Shougo/denite.nvim/blob/1fd1217f928721255d7c671205e35a4b9e3d6d4f/rplugin/python3/denite/source/base.py
クラス名は`Source`にすると自動で読み込んでくれるので`Source`にしました。

```python3
# -*- coding: utf-8 -*-

from .base import Base


class Source(Base):
```

### `__init__` / コンストラクタ

ソースの初期化メソッドになります。
ソースをDeniteに登録する時に呼ばれると思っています。

```python3
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'ale'
        self.kind = 'file'
```

親のコンストラクタを呼ぶ必要があります。(デフォルト値の設定がされます)
また`self`に設定して効果があるものに、`name`(必須), `kind`, `syntax_name`, `max_candidates`などがあります。
詳しくは`:help denite-source-attribute-__init__`の辺りを参照してください。

`name`はソース名です。`ale`を設定しました。
これで`:Denite ale`と呼び出す事ができます。

`kind`にはkind名かKindクラスを指定します。kindを指定するとそのkindが持つactionを実行したりできます。デフォルトは`base`だったと思います。
kindもソースと同じ様に独自実装することができますが、今回は紹介しません。
詳しく知りたければ`:help denite-create-kind`を参照してください。

今回はALEの結果の一覧からファイルを開いて警告なりが出ている箇所にジャンプしたいので、`file`を設定しました。`file kind`のデフォルトのアクションはopenです。
ファイルを開くという事になりますが、行やカラムを指定することで開いた後にジャンプすることも可能です。

### on_init
ソースの初期化時に実行されます。Deniteコマンドが実行されたときに呼ばれると思います。
似たものに`on_close`があります。

```python3
    def on_init(self, context):
        context['__bufnr'] = str(self.vim.call('bufnr', '%'))
```

contextはコンテキスト情報をディクショナリで保持しています。
主な情報として、`input`, `args`, `candidates`, `event`などがあります。
詳細は`:help denite-notation-{context}`を参照してください。

コマンド実行時のバッファ番号を取得したかったので、contextに保持させています。
尚、元々使われているkeyと被らないように`__`をprefixとして付けるようにしています。

### gather_candidates /  _convert

`gather_candidates`は候補一覧を返すメソッドです。
候補一覧はリストで、リストの中身(候補情報)はディクショナリです。
ALEの情報を取得し、候補一覧に変換(`_convert`)し、それを返しています。

```python3
    def gather_candidates(self, context):
        loclist = self.vim.call('ale#engine#GetLoclist', context['__bufnr'])
        return [self._convert(loc) for loc in loclist]

    def _convert(self, info):
        abbr = '[%d:%d] %s' % (info['lnum'], info['col'], info['text'])
        return {
                'word': info['text'],
                'abbr': abbr,
                'action__path': self.vim.call('bufname', info['bufnr']),
                'action__line': info['lnum'],
                'action__col': info['col']
                }
```

候補情報には`word`(必須), `abbr`が設定できます。
`word`は表示・検索対象です。`abbr`を設定するとこちらを優先して表示します。
ただし`abbr`は検索対象にはなりません。
また、候補情報にはアクションに必要な情報を付加する事ができます。(`action__xxx`)
詳しくは`:help denite-source-attribute-gather_candidates`や`:help denite-candidate-attributes`を参照してください。
調査済みの`ale#engine#GetLoclist`を`vim.call`で呼ぶ事で結果を取得できます。
このまま返してもDeniteがうまく処理できないので、`_convert`で候補情報に変換します。
最初はtextだけあればいいかと思っていたんですが、行・カラムの番号があると分かりやすいのでフォーマットしてabbrに設定しました。
ところでkindはfileでしたね。
`:help denite-kind-file`に書いてありますが、`action__path`が必須で、`action__line`や`action__col`を指定することで、その箇所にジャンプすることができます。
ALEの結果情報から設定しています。

### 成果物
できあがったものはこちらです。

```python3:ale.py
# -*- coding: utf-8 -*-

from .base import Base


class Source(Base):

    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'ale'
        self.kind = 'file'

    def on_init(self, context):
        context['__bufnr'] = str(self.vim.call('bufnr', '%'))

    def gather_candidates(self, context):
        loclist = self.vim.call('ale#engine#GetLoclist', context['__bufnr'])
        return [self._convert(loc) for loc in loclist]

    def _convert(self, info):
        abbr = '[%d:%d] %s' % (info['lnum'], info['col'], info['text'])
        return {
                'word': info['text'],
                'abbr': abbr,
                'action__path': self.vim.call('bufname', info['bufnr']),
                'action__line': info['lnum'],
                'action__col': info['col']
                }
```

---

比較的簡単にできましたね。
という訳で、皆さんも何かDeniteのソースを作ってみましょう!
基本構成はUniteのソースと変わらないので、Uniteのソースを移植してみてもいいかもしれません。