作って PR して入れてもらいました。そもそも vim-go にそんな機能あるの?という話から unite/denite source の作り方まで簡単に解説してみます。
- Add support for unite.vim about :GoDecls / :GoDeclsDir by delphinus · Pull Request #1391 · fatih/vim-go
- Add support for denite.nvim about :GoDecls / :GoDeclsDir by delphinus · Pull Request #1604 · fatih/vim-go
まず完成形から。以下は denite での例を示しますが、unite でも見た目は一緒です。
尚、前提として Vim8(denite 使うなら Python3 も)を使ってるものとします。NeoVim でも多分動きますが僕は使ってないので試してません。
又、この記事では vim-go / unite / denite のインストール・設定については解説しません。以下のような優れた記事がありますのでそちらをご参照ください。
- fatih/vim-go-tutorial: Tutorial for vim-go
- vimプラグイン入れて使ってみた(Unite.vim編) - Qiita
- unite.vimより高速なdenite.nvimを使う - Qiita
GoDecls
/ GoDeclsDir
って知ってますか?
Show all function and type declarations for the current [file / dir].
と doc には書いてあります。要するに、ファイル・ディレクトリに含まれる関数と型を列挙してくれる機能です。元々これは ctrlp.vim使ってる人のための機能(最近 fzfサポートも入った)だったので、unite / denite派の僕には使えませんでした。
もっとも、unite / denite には似た機能があります。outline
source です。
GoDecls
と似てますね。これは ctags
コマンドを使って関数・型を列挙してくれるものです。普通の言語ならこれで十分なのですが、Go ではこれでは不足でして……なぜかというと、Go では関数・型の名前空間(パッケージ)がディレクトリ単位だからです。
ディレクトリ内のソース全部の関数・型を列挙したい!
今まで Go のソースを読んでるとき「このパッケージにこんな関数なかったっけ……」と思って探すには、ディレクトリ内で grep するしか方法がありませんでした(IDE 使ってる場合を除く)。vim-go の作者も同じ思いだったのでしょう。これを解決するため、便利ツールを作って解決してくれました。
motion
は vim-go インストール時に行った :GoInstallBinaries
コマンドでインストールされてます(やってますよね?)。適当なディレクトリでコマンドを叩くと、以下のような出力が取れます。
$ 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 形式での出力も可能です。
$ 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
Note: Active development on unite.vim has stopped. The only future changes will be bug fixes.
unite source を作るのに unite.vim のソースをいじる必要はありません。vim-go レポジトリの中で規定のパスにソースを置くだけです。完全なソースはリンク先を見ていただくとして要点だけ書いていきます。
source 属性の定義
unite source でまず必要なのは source の名前などを定義する辞書です。
let s:source = {
\ 'name': 'decls'," ソースの名前
\ 'description': 'GoDecls implementation for unite'," ソースの説明
\ 'syntax': 'uniteSource__Decls'," 候補を列挙する際のシンタックスハイライト
\ 'action_table': {},
\ 'hooks': {},
\ }
この中で name
だけが必須です。今回は候補を綺麗に色づけしたかったので syntax
も設定しています。
候補の列挙
そして source のキモとなるのが gather_candidates
関数です。名前の通り、候補を列挙する関数です。
function! s:source.gather_candidates(args, context) abortletl:bin_path =go#path#CheckBinPath('motion')if empty(l:bin_path)return []endifletl:path= expand(get(a:args,0,'%:p:h'))if isdirectory(l:path)letl:mode='dir'elseif filereadable(l:path)letl:mode='file'elsereturn []endifletl:include= get(g:,'go_decls_includes','func,type')letl:command = printf('%s -format vim -mode decls -include %s -%s %s',l:bin_path,l:include,l:mode, shellescape(l:path))letl:candidates = []tryletl:result = eval(unite#util#system(l:command))letl:candidates = get(l:result,'decls', [])catchcall unite#print_source_error(['command returned invalid response.',v:exception], s:source.name)endtryreturn 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
コマンドのパス
letl:bin_path =go#path#CheckBinPath('motion')
これは vim-go の便利関数で求められます。
2. コマンドの実行とエラー処理
tryletl:result = eval(unite#util#system(l:command))letl:candidates = get(l:result,'decls', [])catchcall unite#print_source_error(['command returned invalid response.',v:exception], s:source.name)endtry
実行には unite#util#system()
、エラーを吐くときは unite#print_source_error()
という便利関数を使いましょう。
3. 候補の作成
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 のシンタックスハイライト関数を呼んで設定します。
function! s:source.hooks.on_syntax(args, context) abortsyntaxmatch uniteSource__Decls_Filepath /[^:]*\ze:/ contained containedin=uniteSource__Declssyntaxmatch uniteSource__Decls_Line /\d\+\ze :/ contained containedin=uniteSource__Declssyntaxmatch uniteSource__Decls_WholeFunction /\vfunc %(\([^)]+\) )?[^(]+/ contained containedin=uniteSource__Declssyntaxmatch uniteSource__Decls_Function /\S\+\ze(/ contained containedin=uniteSource__Decls_WholeFunctionsyntaxmatch uniteSource__Decls_WholeType /type \S\+/ contained containedin=uniteSource__Declssyntaxmatch uniteSource__Decls_Type /\v( )@<=\S+/ contained containedin=uniteSource__Decls_WholeType
各候補には予め uniteSource__(ソース名)
という syntax region が設定されています。これを親として(containedin=uniteSource__Decls
)、各パーツを正規表現で設定します。一部は孫 region まで作ってます。
highlight default link uniteSource__Decls_Filepath Commenthighlight default link uniteSource__Decls_Line LineNrhighlight default link uniteSource__Decls_Function Functionhighlight default link uniteSource__Decls_Type Type
各パーツに色を設定します。通常は新規の色を設定するのではなく、カラースキームと連携させた方が使い勝手がいいでしょう。highlight default link
は文字通り既存の色と“リンク”させてこれを実現してくれます。関数名には Function
の色を使ってくれる訳ですね。
syntaxmatch uniteSource__Decls_Separator /:/ contained containedin=uniteSource__Decls concealsyntaxmatch uniteSource__Decls_SeparatorFunction /func / contained containedin=uniteSource__Decls_WholeFunction concealsyntaxmatch uniteSource__Decls_SeparatorType /type / contained containedin=uniteSource__Decls_WholeType concealendfunction
最後に、画面から消したい文字列を 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 の中のここにソースを置いています。
unite の時と同じく要点だけを書いていきましょう。
Source クラスの定義
classSource(Base):def__init__(self,vim):super().__init__(vim)self.name='decls'self.kind='file'
unite の時と比べるとずいぶん簡潔……というか分かりやすい。Vimscript ではクラスとかないのでいろいろ黒魔術が使われてましたからね……。
候補の列挙
これは gather_candidates
メソッドで行っています。やることは unite の時と同じですのでここも要点だけにしましょう。
defgather_candidates(self,context):bin_path=self.vim.call('go#path#CheckBinPath','motion')ifbin_path=='':return[]
Python3 から Vim のユーザー定義関数を呼び出す際は self.vim.call()
メソッドを使います。ここでは unite の時と同じく motion
コマンドのパスを検索しています。
expand=context['args'][0]ifcontext['args']else'%:p:h'target=self.vim.funcs.expand(expand)
Vim の関数のうち expand()
のような Vim 本体の関数ならばショートカットが用意されています。self.vim.funcs.(関数名)()
です。
try:cmd=subprocess.run(command,stdout=subprocess.PIPE,check=True)exceptsubprocess.CalledProcessErroraserr: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 に用意されたエラー出力関数です。
defmake_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'],}returnlist(map(make_candidates,output['decls']))
最後に候補を成形して終わり。unite の時と一緒なので特に説明は要らんと思いますが一つ、kind
だけが抜けてますね。unite では jump_list
となっていた kind が denite では file
に変わっています。今回のソースではクラスのプロパティで self.kind = 'file'
しちゃってますので、ここでは特に指定しなくて大丈夫です。
候補のシンタックスハイライト
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},]...defhighlight(self):forsyninDECLS_SYNTAX_HIGHLIGHT:containedin=self.syntax_namecontainedin+='_'+syn['parent']if'parent'insynelse''conceal=' conceal'if'conceal'insynelse''self.vim.command('syntax match {0}_{1} /{2}/ contained containedin={3}{4}'.format(self.syntax_name,syn['name'],syn['re'],containedin,conceal))if'link'insyn: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 を作るのも簡単だと思いますので、皆さんもいろいろ試してみてください。