denite-gitを改造してみる

これは 日本情報クリエイト [Engineer's Advent Calendar 2017](https://qiita.com/advent-calendar/2017/njc) の6日目の記事です。

## はじめに

私はvimのライトユーザーですが、少し前に [unite.vim](https://github.com/Shougo/unite.vim) から [denite.nvim](https://github.com/Shougo/denite.nvim) 乗り換えました。

使っているプラグインもそれほど多くなく、移行自体は簡単にできましたがまだまだunite.vimに比べてプラグインが少ない気がします。。。
そんなdenite.nvimのプラグインの中にgitを操作する [denite-git](https://github.com/neoclide/denite-git) というプラグインがあります。
unite.vimにも同系統のプラグインがあり、とても重宝していたので早速入れてみました。

(denite.nvimおよびdenite-gitのインストール方法については割愛します）

## branch操作をdeniteで！

とても便利に使っていたdenite-gitですが一つ気になることが・・・
それは、branchの操作が行えないことです。

uniteで生活していたときには、このブランチ操作をとても重宝しておりましたのでどうしても欲しい！！
というわけで、branch操作を行えるように機能を追加してみました。

## sourceの追加

今回始めてdeniteのプラグインに手を出してみたのですが、sourceの追加は驚くほど簡単！
`./rplugin/python3/denite/source/` 以下にファイルを一つ作ってその中に`Source`と`Kind`というクラスを作るだけ。[^1]

`Source`にはdeniteのウィンドウに表示させる内容を表示させるための処理を（この場合はブランチの一覧）、`Kind`には表示された内容に対する処理をそれぞれ記述します。

ブランチの一覧を取得するコードはこんな感じです。

```gitbranch.py
class Source(BaseSource):

    def __init__(self, vim):
        super().__init__(vim)

        self.name = 'gitbranch'
        self.kind = Kind(vim)

    def on_init(self, context):
        cwd = os.path.normpath(self.vim.eval('expand("%:p:h")'))

        context['__root'] = _find_root(cwd)

    def gather_candidates(self, context):
        root = context['__root']
        if not root:
            return []
        args = ['git', 'branch', '--no-color', '-a']
        candidates = []

        try:
            p = subprocess.run(commands, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            lines = p.stdout.decode(encoding).split('\n')
        except subprocess.CalledProcessError:
            lines = []

        for line in lines:
            if not line:
                continue
            path = os.path.join(root, line[3:])
            current_symbol = line[0]
            candidates.append({
                'word': line,
                'action__path': line[2:],
                'source__root': root,
                'source__current': current_symbol == '*',
                'source__remote': line[2:10] == 'remotes/',
            })

        return candidates
...
```

`gather_candidates`というメソッド内にbranchの一覧を返す処理を記述するだけです。

## kindの追加

さて、コレでブランチの一覧を表示することができるようになったので、後はブランチに対する操作をKindに記述するだけです。

`Kind`クラスに`action_hoge`というメソッドを生やせばそれだけで*hoge*というアクションが追加されるので実行したい操作の種類だけ`action_*`というメソッドを追加していけばいいですね。

```gitbranch.py
...

class Kind(BaseKind):
    def __init__(self, vim):
        super().__init__(vim)

        self.persist_actions += ['open', 'delete']  # pylint: disable=E1101
        self.redraw_actions += ['open', 'delete']  # pylint: disable=E1101
        self.name = 'gitbranch'
        self.default_action = 'open'

    def action_open(self, context):
        target = context['targets'][0]
        args = ['git', 'checkout']
        root = target['source__root']
        path = target['action__path']

        if target['source__remote'] : path = path[8:]
        args.append(path)

        run_command(args, root)

    def action_delete(self, context):
        target = context['targets'][0]
        args = []
        root = target['source__root']
        path = target['action__path']

        force = util.input(self.vim, context, 'Force delete? [y/n] : ', 'n') == 'y'

        if target['source__remote']:
            if force == True:
                args ['git', 'push', 'origin', ':' + target['action__path'][8:]]
        else:
            args = ['git', 'branch', '-D' if force == True else '-d', target['action__path']]

        if len(args) > 0 : run_command(args, root)

    def action_merge(self, context):
        target = context['targets'][0]
        args = ['git', 'merge', target['action__path']]
        root = target['source__root']

        if target['source__remote'] == False and target['source__current'] == False:
run_command(args, root)
```

ここでは*open（checkout）*、*delete*、*merge*の3つのアクションを追加しました。[^2]
選択した業の情報は`context['targets']`に入りますのでこの中から必要な情報を取得して実行したい操作を行います。

## まとめ

機能追加という形ですが少しだけ触ってみたところ、とても簡単に書けてしまいました。
みなさんも色々なプラグインを作ってvimライフを充実させましょう！！


[^1]: sourceとkindをファイルで分割する場合はそれぞれ`./rplugin/python3/denite/source/`と`./rplugin/python3/denite/kind`以下にファイルを作って分割しましょう。
[^2]: `run_command`は外部コマンドを実行する関数で`Source.gather_candidates()`内で行っていることとほとんど同じす。