vim-go に unite/denite source を作って contribute した話

作って PR して入れてもらいました。そもそも vim-go にそんな機能あるの？という話から unite/denite source の作り方まで簡単に解説してみます。

* [Add support for unite.vim about :GoDecls / :GoDeclsDir by delphinus · Pull Request #1391 · fatih/vim-go][1391]
* [Add support for denite.nvim about :GoDecls / :GoDeclsDir by delphinus · Pull Request #1604 · fatih/vim-go][1604]

[1391]: https://github.com/fatih/vim-go/pull/1391
[1604]: https://github.com/fatih/vim-go/pull/1604

まず完成形から。以下は denite での例を示しますが、unite でも見た目は一緒です。

<dl>
<dt><code>Denite decls</code> —— ディレクトリ（パッケージ）に含まれる関数と型を列挙します。</dt>
<dd><img width="682" alt="スクリーンショット 2017-12-11 20.11.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/8880/58ca594c-205e-c41d-50f6-4ac47f067b86.png"></dd>
</dl>

尚、前提として Vim8（denite 使うなら Python3 も）を使ってるものとします。NeoVim でも多分動きますが僕は使ってないので試してません。

又、この記事では vim-go / unite / denite のインストール・設定については解説しません。以下のような優れた記事がありますのでそちらをご参照ください。

* [fatih/vim-go-tutorial: Tutorial for vim-go](https://github.com/fatih/vim-go-tutorial)
  - 日本語訳: [hnakamur/vim-go-tutorial-ja: Tutorial for vim-go](https://github.com/hnakamur/vim-go-tutorial-ja)
* [vimプラグイン入れて使ってみた（Unite.vim編） - Qiita](https://qiita.com/omega999/items/bc9b0ffb47cc23742860)
* [unite.vimより高速なdenite.nvimを使う - Qiita](https://qiita.com/okamos/items/4e1665ecd416ef77df7c)

# `GoDecls` / `GoDeclsDir` って知ってますか？

> Show all function and type declarations for the current [file / dir].

と doc には書いてあります。要するに、ファイル・ディレクトリに含まれる関数と型を列挙してくれる機能です。元々これは [ctrlp.vim][] 使ってる人のための機能（最近 [fzf][] サポートも[入った][]）だったので、[unite][] / [denite][] 派の僕には使えませんでした。

<dl>
<dt><code>GoDecls</code> —— ファイルに含まれる関数と型を列挙します。</dt>
<dd><img width="682" alt="スクリーンショット 2017-12-11 20.08.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/8880/9854c8f6-4297-b357-923d-1bbc583a38ca.png"></dd>
<dt><code>GoDeclsDir</code> —— ディレクトリ（パッケージ）に含まれる関数と型を列挙します。</dt>
<dd><img width="682" alt="スクリーンショット 2017-12-11 20.09.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/8880/95f9037c-e916-f7f4-a1f2-8232c498757f.png"></dd>
</dl>

[ctrlp.vim]: https://github.com/ctrlpvim/ctrlp.vim
[fzf]: https://github.com/junegunn/fzf.vim
[入った]: https://github.com/fatih/vim-go/pull/1437
[unite]: https://github.com/Shougo/unite.vim
[denite]: https://github.com/Shougo/denite.nvim

もっとも、unite / denite には似た機能があります。`outline` source です。

<dl>
<dt><code>Denite outline</code> —— ファイルに含まれる関数と型を列挙します。</dt>
<dd><img width="682" alt="スクリーンショット 2017-12-11 20.11.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/8880/2339953c-048d-c3c4-488f-fa5d5ddac3d9.png"></dd>
</dl>

* [Shougo/unite-outline: outline source for unite.vim][outline]

[outline]: https://github.com/Shougo/unite-outline

`GoDecls` と似てますね。これは `ctags` コマンドを使って関数・型を列挙してくれるものです。普通の言語ならこれで十分なのですが、Go ではこれでは不足でして……なぜかというと、Go では関数・型の名前空間（パッケージ）が**ディレクトリ単位**だからです。

# ディレクトリ内のソース全部の関数・型を列挙したい！

今まで Go のソースを読んでるとき「このパッケージにこんな関数なかったっけ……」と思って探すには、ディレクトリ内で grep するしか方法がありませんでした（IDE 使ってる場合を除く）。vim-go の作者も同じ思いだったのでしょう。これを解決するため、便利ツールを作って解決してくれました。

* [fatih/motion: Navigation and insight in Go][motion]

[motion]: https://github.com/fatih/motion

`motion` は vim-go インストール時に行った `:GoInstallBinaries` コマンドでインストールされてます（やってますよね？）。適当なディレクトリでコマンドを叩くと、以下のような出力が取れます。

```sh
$ cd ~/.go/src/github.com/golang/appengine
$ motion -mode decls -include func,type -dir .
{
        "mode": "decls",
        "decls": [
                {
                        "keyword": "func",
                        "ident": "TestValidGeoPoint",
                        "full": "func TestValidGeoPoint(t *testing.T)",
                        "filename": "appengine_test.go",
                        "line": 11,
                        "col": 1
                },
                {
                        "keyword": "func",
                        "ident": "BackgroundContext",
                        "full": "func BackgroundContext() context.Context",
                        "filename": "appengine_vm.go",
                        "line": 18,
                        "col": 1
                },

...

```

JSON 以外に Vim 形式での出力も可能です。

```sh
$ motion -mode decls -include func,type -dir .
{"mode": "decls", "decls": [{"keyword": "func", "ident": "IsTimeoutError", "full": "func IsTimeoutError(err error) bool", "filename": "timeout.go", "line": 10, "col": 1}, ...
```

ここまで出来てれば簡単ですね！ このコマンドの出力を読み取って、いい感じに成形して表示すればいい訳です。

# unite source の作り方

もうありとあらゆるところで解説されていることですから詳しく書くこともないでしょう。そもそも unite.vim 自体がすでに開発が終了してますからね。

> [Shougo/unite.vim: Unite and create user interfaces](https://github.com/Shougo/unite.vim)
> Note: Active development on unite.vim has stopped. The only future changes will be bug fixes.

unite source を作るのに unite.vim のソースをいじる必要はありません。vim-go レポジトリの中で規定のパスにソースを置くだけです。完全なソースは[リンク先][decls.vim]を見ていただくとして要点だけ書いていきます。

[decls.vim]: https://github.com/fatih/vim-go/blob/master/autoload/unite/sources/decls.vim

## source 属性の定義

unite source でまず必要なのは source の名前などを定義する辞書です。

```vim
let s:source = {
      \ 'name': 'decls',  " ソースの名前
      \ 'description': 'GoDecls implementation for unite', " ソースの説明
      \ 'syntax': 'uniteSource__Decls', " 候補を列挙する際のシンタックスハイライト
      \ 'action_table': {},
      \ 'hooks': {},
      \ }
```

この中で `name` だけが必須です。今回は候補を綺麗に色づけしたかったので `syntax` も設定しています。

## 候補の列挙

そして source のキモとなるのが `gather_candidates` 関数です。名前の通り、候補を列挙する関数です。

```vim
function! s:source.gather_candidates(args, context) abort
  let l:bin_path = go#path#CheckBinPath('motion')
  if empty(l:bin_path)
    return []
  endif

  let l:path = expand(get(a:args, 0, '%:p:h'))
  if isdirectory(l:path)
    let l:mode = 'dir'
  elseif filereadable(l:path)
    let l:mode = 'file'
  else
    return []
  endif

  let l:include = get(g:, 'go_decls_includes', 'func,type')
  let l:command = printf('%s -format vim -mode decls -include %s -%s %s', l:bin_path, l:include, l:mode, shellescape(l:path))
  let l:candidates = []
  try
    let l:result = eval(unite#util#system(l:command))
    let l:candidates = get(l:result, 'decls', [])
  catch
    call unite#print_source_error(['command returned invalid response.', v:exception], s:source.name)
  endtry

  return map(l:candidates, "{
        \ 'word': printf('%s :%d :%s', fnamemodify(v:val.filename, ':~:.'), v:val.line, v:val.full),
        \ 'kind': 'jump_list',
        \ 'action__path': v:val.filename,
        \ 'action__line': v:val.line,
        \ 'action__col': v:val.col,
        \ }")
endfunction
```

長いですが、大半は `motion` コマンドに渡すオプションを作成しています。大事なのは3点。

### 1. `motion` コマンドのパス

```vim
let l:bin_path = go#path#CheckBinPath('motion')
```

これは vim-go の便利関数で求められます。

### 2. コマンドの実行とエラー処理

```vim
try
  let l:result = eval(unite#util#system(l:command))
  let l:candidates = get(l:result, 'decls', [])
catch
  call unite#print_source_error(['command returned invalid response.', v:exception], s:source.name)
endtry
```

実行には `unite#util#system()`、エラーを吐くときは `unite#print_source_error()` という便利関数を使いましょう。

### 3. 候補の作成

```vim
return map(l:candidates, "{
      \ 'word': printf('%s :%d :%s', fnamemodify(v:val.filename, ':~:.'), v:val.line, v:val.full),
      \ 'kind': 'jump_list',
      \ 'action__path': v:val.filename,
      \ 'action__line': v:val.line,
      \ 'action__col': v:val.col,
      \ }")
```

各候補の情報を設定します。大体は見たまんまです。

| 項目           | 説明                             |
|----------------|----------------------------------|
| `word`         | 画面に表示される文字列。         |
| `kind`         | 候補の“kind”（後述）。           |
| `action__path` | 候補を開く際に使うパス。         |
| `action__line` | ファイルの中で候補が存在する行。 |
| `action__col`  | ファイルの中で候補が存在する桁。 |

`kind` がちょっと特殊ですね。これは名前の通り候補の「種別」を表すものです。「種別」はファイル、ディレクトリ、Vim のバッファー、URL などなどいろいろ用意されており、それぞれの種別によって取れる“action”が異なってきます。この辺はドキュメントが非常に詳しいので `:h unite-kinds` で読んでみてください。

今回はファイルの特定の場所に飛びたいだけですので `jump_list` という kind を使いました。

## 候補のシンタックスハイライト

実はここが一番大変でした。Vim のシンタックスハイライト機構は非常に多機能なのですが、正直言ってその記法は複雑怪奇です。今回説明する以外にも色んな機能がありますので詳細は `:h syntax` を見てみてください。

unite では `hooks.on_syntax()` 関数を定義し、その中で Vim のシンタックスハイライト関数を呼んで設定します。

```vim
function! s:source.hooks.on_syntax(args, context) abort
  syntax match uniteSource__Decls_Filepath /[^:]*\ze:/ contained containedin=uniteSource__Decls
  syntax match uniteSource__Decls_Line /\d\+\ze :/ contained containedin=uniteSource__Decls
  syntax match uniteSource__Decls_WholeFunction /\vfunc %(\([^)]+\) )?[^(]+/ contained containedin=uniteSource__Decls
  syntax match uniteSource__Decls_Function /\S\+\ze(/ contained containedin=uniteSource__Decls_WholeFunction
  syntax match uniteSource__Decls_WholeType /type \S\+/ contained containedin=uniteSource__Decls
  syntax match uniteSource__Decls_Type /\v( )@<=\S+/ contained containedin=uniteSource__Decls_WholeType
```

各候補には予め `uniteSource__(ソース名)` という syntax region が設定されています。これを親として（`containedin=uniteSource__Decls`）、各パーツを正規表現で設定します。一部は孫 region まで作ってます。

```vim
  highlight default link uniteSource__Decls_Filepath Comment
  highlight default link uniteSource__Decls_Line LineNr
  highlight default link uniteSource__Decls_Function Function
  highlight default link uniteSource__Decls_Type Type
```

各パーツに色を設定します。通常は新規の色を設定するのではなく、カラースキームと連携させた方が使い勝手がいいでしょう。`highlight default link` は文字通り既存の色と“リンク”させてこれを実現してくれます。関数名には `Function` の色を使ってくれる訳ですね。

```vim
  syntax match uniteSource__Decls_Separator /:/ contained containedin=uniteSource__Decls conceal
  syntax match uniteSource__Decls_SeparatorFunction /func / contained containedin=uniteSource__Decls_WholeFunction conceal
  syntax match uniteSource__Decls_SeparatorType /type / contained containedin=uniteSource__Decls_WholeType conceal
endfunction
```

最後に、画面から消したい文字列を `conceal` 機能で隠します。`func` とか自明ですからね。

# denite source の作り方

今回の記事はそもそも denite の入門記事を書こうと思って始めたのですが、unite source と対比させた方が分かりやすいだろうと思いましてそっちを先に詳しく載せました。

denite source の一番の特徴は denite のそれです。つまり **Python3 で書いてある**のです。そりゃ Vimscript も別に嫌いじゃないし、僕も denite のために Python3 触ったくらいなので特に思い入れもなかったんですが、unite source で散々苦労した後なので感慨もひとしおでした。denite source というか、Python3 のいいところは

* 何でもできるし、道具も最初から揃ってる。
* 開発を助けるツールがたくさんある。
* 分からないことは Google 先生に聞けば一瞬で分かる。

に尽きます（特に最後）。やっぱりメジャーな言語はいいなあ……

更にこれは denite に限った特徴ですが、denite 本体が Python 3.5+ を必須として書かれています。今回のような拡張機能を書くときに古い環境のことを考えなくていいのはいいこと（？）です。

いい加減本題に入ります。denite も unite と同様、特定のパスにソースを置くとそれを実行時に読み込んでくれます。今回は vim-go の中のここにソースを置いています。

* [vim-go/decls.py at master · fatih/vim-go](https://github.com/fatih/vim-go/blob/master/rplugin/python3/denite/source/decls.py)

unite の時と同じく要点だけを書いていきましょう。

## Source クラスの定義

```python
class Source(Base):

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

        self.name = 'decls'
        self.kind = 'file'
```

unite の時と比べるとずいぶん簡潔……というか分かりやすい。Vimscript ではクラスとかないのでいろいろ黒魔術が使われてましたからね……。

## 候補の列挙

これは `gather_candidates` メソッドで行っています。やることは unite の時と同じですのでここも要点だけにしましょう。

```python
    def gather_candidates(self, context):
        bin_path = self.vim.call('go#path#CheckBinPath', 'motion')
        if bin_path == '':
            return []
```

Python3 から Vim のユーザー定義関数を呼び出す際は `self.vim.call()` メソッドを使います。ここでは unite の時と同じく `motion` コマンドのパスを検索しています。

```python
        expand = context['args'][0] if context['args'] else '%:p:h'
        target = self.vim.funcs.expand(expand)
```

Vim の関数のうち `expand()` のような Vim 本体の関数ならばショートカットが用意されています。`self.vim.funcs.(関数名)()` です。

```python
        try:
            cmd = subprocess.run(command, stdout=subprocess.PIPE, check=True)
        except subprocess.CalledProcessError as err:
            denite.util.error(self.vim,
                              'command returned invalid response: ' + str(err))
            return []

        txt = cmd.stdout.decode('utf-8')
        output = json.loads(txt, encoding='utf-8')
```

コマンドの出力結果（JSON）をデコードしています。`denite.util.error()` は denite に用意されたエラー出力関数です。

```python
        def make_candidates(row):
            name = self.vim.funcs.fnamemodify(row['filename'], ':~:.')
            return {
                'word': '{0} :{1} :{2}'.format(name, row['line'], row['full']),
                'action__path': row['filename'],
                'action__line': row['line'],
                'action__col': row['col'],
                }
        return list(map(make_candidates, output['decls']))
```

最後に候補を成形して終わり。unite の時と一緒なので特に説明は要らんと思いますが一つ、`kind` だけが抜けてますね。unite では `jump_list` となっていた kind が denite では `file` に変わっています。今回のソースではクラスのプロパティで `self.kind = 'file'` しちゃってますので、ここでは特に指定しなくて大丈夫です。

## 候補のシンタックスハイライト

```python
DECLS_SYNTAX_HIGHLIGHT = [
    {'name': 'FilePath', 're': r'[^:]*\ze:', 'link': 'Comment'},
    {'name': 'Line', 're': r'\d\+\ze :', 'link': 'LineNr'},
    {'name': 'WholeFunction', 're': r'\vfunc %(\([^)]+\) )?[^(]+'},
    {'name': 'Function', 'parent': 'WholeFunction',
     're': r'\S\+\ze(', 'link': 'Function'},
    {'name': 'WholeType', 're': r'type \S\+'},
    {'name': 'Type', 'parent': 'WholeType',
     're': r'\v( )@<=\S+', 'link': 'Type'},
    {'name': 'Separator', 're': r':', 'conceal': True},
    {'name': 'SeparatorFunction', 'parent': 'WholeFunction',
     're': r'func ', 'conceal': True},
    {'name': 'SeparatorType', 'parent': 'WholeType',
     're': r'type ', 'conceal': True},
    ]

...

    def highlight(self):
        for syn in DECLS_SYNTAX_HIGHLIGHT:
            containedin = self.syntax_name
            containedin += '_' + syn['parent'] if 'parent' in syn else ''
            conceal = ' conceal' if 'conceal' in syn else ''

            self.vim.command(
                'syntax match {0}_{1} /{2}/ contained containedin={3}{4}'
                .format(self.syntax_name, syn['name'], syn['re'],
                        containedin, conceal))

            if 'link' in syn:
                self.vim.command('highlight default link {0}_{1} {2}'.format(
                    self.syntax_name, syn['name'], syn['link']))
```

Vim の「関数」ではなく「ex コマンド」を呼び出す場合は `self.vim.command()` を使います。やってることは unite の時と一緒です。

ここだけは Python っぽく書いたら逆に見づらくなったかもしんない……。素直に `self.vim.command()` を羅列した方がいいのかも。

----

以上です。今回説明した基本だけ分かっとけば他のコマンド出力から source を作るのも簡単だと思いますので、皆さんもいろいろ試してみてください。