denite.nvimでプレビュー機能付きのソースを作る

6 min read読了の目安(約5600字

はじめに

denite.nvimにはユーザーが作ったソースを動的に読み込む仕組みがあり、簡単に機能を追加することができます。
そのようなソースの作成において、Vim/Neovimのターミナル機能を使ったプレビューができるようになったので紹介します。
本機能は筆者がPRを出して、Shougoさんに取り込んでいただいたものです。

機能の説明

例えば、git logを一覧表示できるようなソースを作ることを考えます。
このとき、コミットの変更内容をプレビューできると便利そうです。
しかし、git diffなどで変更内容を得られたとしても、それを色付きでVimのバッファに出力するのは大変です。
gitの出力ならば、Vimのシンタックスファイルがあるのでまだ簡単ですが、それでもプレビューウィンドウの処理とか色々なことを考えなければならず面倒です。
今回新たに、そうした面倒なことを考えずに簡単に外部コマンドを使ったプレビュー機能を追加できるようになりました。

背景

最近deniteに、batを使ったプレビュー機能が追加されました。
これまでのVimのバッファを使ったプレビューでも十分便利なのですが、Denite file/rec -auto-action=previewのように自動プレビューを有効にした場合に、読み込まれていないファイルにはVimのシンタックスハイライトが効かなかったり、効いたとしてもシンタックスは比較的重い処理なので、カーソルを素早く動かしたときに動きがカクついてしまうということがありました。
それに対して、batによるプレビューはVimのターミナル機能を使っており、非同期でファイルの内容を色付きで表示してくれます。
これにより、カーソルを早く動かしてもストレスなくプレビューすることができるようになりました。

外部コマンドによるプレビューは、非同期で表示ができて便利ですし、batだけでなく他にも様々な用途に使えるのではないかと思い、より汎用的な使い方ができるようにPRを出してみました。

使用例

外部コマンドによるプレビューの具体例として、git logのソースを作ってみます。

sourceの定義

以下はgit logの出力をdeniteで表示する最小構成のコードです。
gitのディレクトリの中にVimのカレントディレクトリがあれば、ログの一覧を表示します。

import subprocess

from denite.base.source import Base
from denite.util import Nvim, UserContext, Candidates
from denite.base.kind import Base as KindBase

class Source(Base):
    def __init__(self, vim: Nvim) -> None:
        super().__init__(vim)
        self.vim = vim
        self.name = "git/log"
        # self.kind = Kind(vim)

    def gather_candidates(self, context: UserContext) -> Candidates:
        candidates: Candidates = []
        cwd = self.vim.call('getcwd')
        output = subprocess.run(['git', 'log', '--pretty=oneline',
                                 '--abbrev-commit', '--', '.'],
                                stdout=subprocess.PIPE, cwd=cwd)
        items = output.stdout.decode().split('\n')
        if not items:
            return []
        for item in items:
            candidates.append(
                {
                    "word": item,
                    "__obj": item.split(' ')[0],
                }
            )
        return candidates

このファイルを(&runtimepath)/rplugin/python3/denite/source/git/log.pyに保存すると、Denite git/logでコミットのオブジェクト名とコミットメッセージが見られるようになります。

kindの定義

次にgit/logソースに対して、kindを定義します。今回はdefault_actionであるopenと、本題のpreviewアクションを追加します。

class Kind(KindBase):
    def __init__(self, vim: Nvim) -> None:
        super().__init__(vim)
        self.name = 'gitdiff'
        self.default_action = 'open'

    def action_open(self, context: UserContext) -> None:
        target = context['targets'][0]
        self.vim.command("Gina show {}".format(target['__obj']))

    def action_preview(self, context: UserContext) -> None:
        target = context['targets'][0]

        diff_cmd = ['git', 'diff', target['__obj'] + '^!']

        self.preview_terminal(context, diff_cmd, 'preview')

openについてはgina.vimを使いました。
previewはとてもシンプルで、プレビューするためのコマンドをself.preview_terminalに渡せば良いです。
3つ目の引数はアクションの名前です。action_preview_batであれば、preview_batになります。

ハイライトも追加した完成版はこちらです。

denite git/log source
import subprocess

from denite.base.source import Base
from denite.util import Nvim, UserContext, Candidates
from denite.base.kind import Base as KindBase

GITLOG_OBJ_SYNTAX = (
    'syntax match {0}_obj '
    r'/^\s\S*/ '
)

GITLOG_OBJ_HIGHLIGHT = (
    'highlight default link {0}_obj Statement'
)


class Source(Base):
    def __init__(self, vim: Nvim) -> None:
        super().__init__(vim)
        self.vim = vim
        self.name = "git/log"
        self.kind = Kind(vim)

    def highlight(self) -> None:
        self.vim.command(GITLOG_OBJ_SYNTAX.format(self.syntax_name))
        self.vim.command(GITLOG_OBJ_HIGHLIGHT.format(self.syntax_name))

    def gather_candidates(self, context: UserContext) -> Candidates:
        candidates: Candidates = []
        cwd = self.vim.call('getcwd')
        output = subprocess.run(['git', 'log', '--pretty=oneline',
                                 '--abbrev-commit', '--', '.'],
                                stdout=subprocess.PIPE, cwd=cwd)
        items = output.stdout.decode().split('\n')
        if not items:
            return []
        for item in items:
            candidates.append(
                {
                    "word": item,
                    "__obj": item.split(' ')[0],
                }
            )
        return candidates


class Kind(KindBase):
    def __init__(self, vim: Nvim) -> None:
        super().__init__(vim)
        self.name = 'gitdiff'
        self.default_action = 'open'

    def action_open(self, context: UserContext) -> None:
        target = context['targets'][0]
        self.vim.command("Gina show {}".format(target['__obj']))

    def action_preview(self, context: UserContext) -> None:
        target = context['targets'][0]

        diff_cmd = ['git', 'diff', target['__obj'] + '^!']

        self.preview_terminal(context, diff_cmd, 'preview')

使ってみる

こんな感じで使えます。

動画内で実行しているコマンドはこちらです。

Denite git/log -auto-action=preview -vertical-preview -no-auto-resize -preview-height=100 -winheight=100 -preview-width=100

おわりに

今回実装したgit/logのように、簡単なコードで実用的なdeniteのソースを作ることができます。
deniteはtelescope.nvimfzf-preview.vimに比べると、一見地味に見えるかもしれませんが、プレビュー機能も充実しています。

私のdotfilesでは今回のgitの他にも色々なソースを作っています。参考になれば幸いです。

https://github.com/matsui54/dotfiles