この記事はVim Advent Calandar 2016の7日目の記事です。
なお、この記事はvimconf2016で発表した
Go、C、Pythonのためのdeoplete.nvimのソースの紹介と、Neovim専用にpure Goでvim-goをスクラッチした話
の続きだったりします。
長いので飛ばし飛ばしどうぞ。
あと、この記事で実際伝えたいのは最後のあたりの簡単な作り方です。とりあえずそこだけでも読んでくださいお願いします。
deoplete.nivm
巷で有名なShougoさんが開発されている(Neo)vim用補完フレームワーク、deoplete.nvimをご存知でしょうか?
neocomplete.vimの元に、真の非同期を目指して作られた次世代の補完プラグインです。
github.com/Shougo/deoplete.nvim
🌠 Dark powered asynchronous completion framework for neovim
とあるように、だーくぱわーどな非同期補完フレームワーク for neovimです。
なぜだーくぱわーどなのかは作者が厨二(ry
framework and source
さて、なぜリポジトリの紹介に "framework" と明記されているんでしょうか。
それは、本体にビルドインされたものには特定の言語のものを用意せず、他のリポジトリ(開発者)が書いたコードをインクルードして使うようなインターフェイスになっているからです。
いまではどこでも常識になっている、OSSベースのエコシステムにのるスタイルですね。
いま開いてるバッファがC++で書かれていたら、Pythonだったら、Goだったら…etc。状況に応じてdeoplete.nvimがその言語用のものを呼んでくるようなシステムです。
そこで呼び出されるコードのことを、通称 "ソース (source)" と呼びます。
今回はそのソースに関しての内部構造と、簡単な作り方を紹介したいと思います。
Note:
なお、ご存知かもしれませんがdeoplete.nvimはVimでは動きません。Vimお使いであればneocomplete.vimからどういう変わったか見ていただけたらと思います。
また、前提としてdeoplete.nvim本体のみは既にインストールされているものとします。インストール方法は他にお譲りすることにします。
よって現状ではNeovim用の記事になります。現状では…!
source
さて、ソースの中はどうなってるのでしょう。
ここでは僕が開発した3(~5)つの言語用のソースをベースに紹介したいと思います。
ちなみに開発中のものは以下です。
- zchee/deoplete-go
- zchee/deoplete-jedi
- zchee/deoplete-clang
- (zchee/deoplete-docker) // やっつけ
- (zchee/deoplete-zsh) // やっつけ
やっつけのものは、あんまり見ないでください。
==
deoplete.nvimのソースの特徴として、以下のものが挙げられます。
- Python3 で書ける(でしか書けない)
- subprocess しようが threading しようがやりたい放題
- さすがのPythonなので、なんでもできるしなんでも揃ってる
- deoplete.nvimのために初めてPythonを書いたぼくでも書けたので、大概なんとかなる
- 余談
Python3 で書ける(でしか書けない)
deoplete.nvimのコア部分は全てPython3で書かれています。Vim script部分も多少ありますが、補完に関する部分はほぼPython3と言って差し支えないと思います。
ですので、インクルードされるソースも自ずとPython3で書く必要があります。
ここまでいちいち 3 をつけているのには理由があり、Python 2 は公式ではサポートしていません。力技でなんとかすればできるかもしれませんが、おすすめはしません。
逆に言うと 3 だけを考えれば良いので、import
でtry
したりとかなど互換性の部分で困ったりとかはないです。
ぼくはdeoplete.nvimのために初めてPythonを書き始めたので、2と3の違いすら分かってないので全くもって困る要素がありませんでした。
でしか書けない、と言うのは他にも意味があって、Python以外の他の言語でラップして書こうとすると急にハードルが高くなります。
お察しの通り、ujsonやrapidjsonのようにCライブラリにしてimportしなければならないからです。
RubyのパーサーをRubyで書いてPythonにbindingsして…とか考えるとそれだけでなんかやばいので素直にやめておきましょう。(もしかしてそんなのあったりしますか?)
じゃあPython以外の言語用のソースどうすんねんという話なんですが、
subprocess しようが threading しようがやりたい放題
言語別の補完エンジンのバイナリをsubprocess
で直接叩きましょう。しょっちゅう叩きたくなかったらthreading
あたりとかasyncio
とかなんかその辺で適度にコントロールしましょう。万事解決です。
だいたいどの言語でも天才で物好きな人がASTパーサー書いてるはずですので(ありがたい時代です)、そのバイナリのコマンドフラグとかに従って愚直に書いていけばだいたいそれっぽいのができてしまいます。
例として zchee/deoplete-go なんですが、これはsubprocess
一本です。
Go言語のデファクトであるgocode
というエンジンを使っているのですが、幸いjson出力があったのでそれをujson
でパースしてちょいちょい加工してダンプしてdeoplete.nvimに渡しておしまい。
これはgocodeが数ある言語パーサーの中でも断トツ速い部類(だと思ってる、たぶん)だったからですが、Go用のソースはまぁまぁ使えるレベルになっていると思っています。
ただ完全に依存するので、性能や評価はそのエンジン次第になります。実際ぼくもgocodeに言語仕様の追従のために何度かプルリクしてます。
使わせてもらってるので貢献は大事!
さすがのPythonなので、なんでもできるしなんでも揃ってる
Pythonの特徴のひとつだと思うんですが、思いつくものはだいたい既にモジュールがあるという素晴らしいエコシステムを存分に使いましょう。
urllib3
とかでREST APIさっと取ってこれるし、ドキュメントのパース必要だったらSphinx
ベースならなんとかなりそう感ありますし(知らない)
補完エンジンにDeep learninig取り入れて適切に補完したい? Tensorflowあります(ほんとに知らない)
deoplete.nvimのために初めてPythonを書いたぼくでも書けたので、大概なんとかなる
題の通りですが、deoplete.nvimのソース書くまでじつはPython嫌いでした。
その昔 #!/usr/bin/env python
でpython2立って、でもそのツールはPython3用だったとかそういうよくある理由からですw
「はーまじ+1のバージョンで動かないのなんでなん?」という初心者だったぼくが、とりあえず動くやつ書けたので皆さんならすぐ書けますたぶん。
あ、今は好きです。
余談
余談ですが、zchee/deoplete-clang のC/C++に関してだけは、libclangのPython binding使っても速度的に結構厳しかったので(ctypes
のオーバーヘッドっぽい)、いまバックエンドサーバーをGoで書き直してます。
そのままPythonで頑張れたかもしれませんが、ぼくのPython力ではなんともならんと思ったので素直に諦めました。
サーバーにするのでキャッシュ効いてdb(leveldbのよてい)使えて、あとLLVMのlibclang周りのライブラリをcgoでスタティックコンパイルしてバイナリ配布する予定なので、libclangのインストールいらなくなるとかそんな感じです。
ていうかYouCompleteMeに勝ちたいんです。ほぼ現時点で最速のあのYCM。あれに勝てないとdeopleteにC系ユーザーが移ってこない。
コアC++だからめっちゃ速いんです。書いたけど現存のより遅いの超悔しいんですよ。
だからあえてC++では書きません。(うそです。ぼくはC/C++は読める程度で書けません)
でも知らん言語をパースするのほんと苦行ですね。
How to
いつもの癖で前置きが長くなりましたが、じゃあどうやってソースを書くのか。
Why?
最近はdeoplete.nvimも有名になり(失礼)、割と有名な言語のソースは既に開発されてきてて、それ使えばOKになりつつあります。(See deoplete.nvim.txt)
じゃあなぜ書き方を紹介するのかというと、すごいエッジケースだけど毎回ドキュメントとか見て書くの超だるい、という言語ではないものも簡単に補完できるよということをお伝えしたかったからです。
自分で勝手に思っている最たるものは、大概のプロジェクトに必須なのにもうDSL化してるんじゃないかと思ってる 、.travis.yml
。
https://github.com/zchee/docker-machine-driver-xhyve/blob/master/.travis.yml#L3-L9
osx_image
, language
とかいくつあるか分からんしバージョン調べなきゃだし、
https://github.com/Dobiasd/FunctionalPlus/blob/master/.travis.yml#L9-L23 (Dobiasd/FunctionalPlus 引用)
apt source
とかいちいち調べるより補完して欲しくないですか?
ということで.travim.yml
の補完の簡易版を例に書き方を紹介します。仮に名前は deoplete-travis
。
ちなみにもし既にそういうのあったら教えてください。全力でdeoplete.nvimポートします。
source location
とりあえずdeoplete.nvimが認識するディレクトリは固定されてて、
plugin-repository-root/rplugin/python3/deoplete/sources
なのです。
が、開発時は何かと面倒なんで、仕様が変わってなければ自分の $XDG_CONFIG_HOME/nvim
以下も読んでくれるはずです。
ので、
cd $XDG_CONFIG_HOME/nvim
mkdir -p rplugin/python3/deoplete/sources
touch rplugin/python3/deoplete/sources/deoplete-travis.py
template
適当なテンプレートです。
まぁいろいろまずいんですが、そういうのは後で良いのです。
基本的には、deoplete.nvimはソースに記述されていると期待するfunction名が決められており、所定のタイミングでそれらのfunctionをコールするような動作です。
Goだと interface
に近いんですが、こういうのは一般的になんて言うんですか?
import re
from .base import Base
class Source(Base):
def __init__(self, vim):
Base.__init__(self, vim)
self.name = 'travis'
self.mark = '[Travis]'
self.filetypes = ['yaml']
self.min_pattern_length = 0
self.rank = 500
def on_init(self, context):
vars = context['vars']
self.foo = vars.get('deoplete#sources#travis#foo', '')
self.bar = vars.get('deoplete#sources#travis#bar', False)
try:
# init(load suorce) only work
pass
except Exception:
# Ignore the error
pass
def on_event(self, context):
if context['event'] == 'BufRead':
try:
# vim autocmd event based works
pass
except Exception:
# Ignore the error
pass
def get_complete_position(self, context):
m = re.search(r'\w*$', context['input'])
return m.start() if m else -1
def gather_candidates(self, context):
# return dict in list ([{},{},{}...])
def __init__
ソースのロード時の初期化です。ここがコールされるのはPython側の仕様です(よね?)
いま書いてある self.***
達はdeoplete.nvimで決められているソースのルール的なもので、deoplete.nvim.txtに詳しいのが書いてあります。
今のコードだと全部のymlファイルで補完が呼ばれますが、まぁとりあえず。
def on_init
ソースがロードされた時だけ呼ばれます。
vars = context['vars']
は呪文です。
あとは self.foo = vars.get('fooooo', '')
でvimの g:fooooo
値が self.foo
に入ります。無かったら後の ''
とか False
が入る感じです。
ここでも何かしら呼べるので、 os.path.isfile(fname)
とかでファイルチェックしたり、例えばdeoplete-goだと clang.Config.set_library_file(self.libclang_path)
でlibclangにライブラリのファイルパス渡したりしてます。
def on_event
autocmd
ベースでなんかしたい時に使います。
なんか、BufWritePost
でバッファのデータ取ったりとかでしょうか。
def get_complete_position
deoplete.nvimが補完の位置を取る時に呼ばれます。
これはinsertモードで文字打ってるとだいたい呼ばれます。(アバウト)
ちょっと説明難しいので、とりあえずそのままでいいです。
def gather_candidates
これが一番の重要な関数で、deoplete.nvimが補完リストを生成する時に毎回呼ばれます。
なので、この関数の処理をどれだけ軽くできるかで、そのソースの性能が決まってきます。
また、ひとつのリストの中に複数のdictがあるものをreturnで返す必要があります。
中のdictの仕様はvimのomnifuncと同じですので、:help complete-items
で見れます。
例としては
def gather_candidates(self, context):
# some works...
.
.
.
return [
{
'word': '補完されるワード',
'abbr': '補完リストに表示されるワード',
'info': '追加情報、プレビュー用など',
'kind': '要素の種類',
'dup': 1 # デフォで 1 で良さそう
},
# Go言語のcgo補完とかだと、
{
'word': 'CBytes',
'abbr': 'CBytes([]byte) unsafe.Pointer',
'info': 'CBytes([]byte) unsafe.Pointer',
'kind': 'function',
'dup': 1
},
]
.
.
.
.
.
朝だ…
すみません
vim各位、ターミナルで記事書いててほっといたらカーネルパニックでふっとびましたのであさまでお待ちください宜しくお願い致します
— zchee / tʃí / ちー (@_zchee_) 2016年12月6日
あさになってしまいました。
ふっとばしてからここまでがやっとでした…!
ごめんなさい。
とりあえず、パッと思いついた .travis.yml
のキーワードだけ補完するソースです。
import re
from .base import Base
KEYWORD = [
'sudo', 'os', 'osx_image', 'language', 'env', 'global',
'before_install', 'install', 'before_script', 'script',
'after_success', 'after_failure',
'before_deplop', 'deploy', 'after_deploy',
'after_script'
]
class Source(Base):
def __init__(self, vim):
Base.__init__(self, vim)
self.name = 'travis'
self.mark = '[Travis]'
self.filetypes = ['yaml']
self.min_pattern_length = 0
self.rank = 500
self.result_keywords = [{'word': x} for x in KEYWORD]
def on_init(self, context):
vars = context['vars']
self.foo = vars.get('deoplete#sources#travis#foo', '')
self.bar = vars.get('deoplete#sources#travis#bar', False)
try:
# init(load suorce) only work
pass
except Exception:
# Ignore the error
pass
def on_event(self, context):
if context['event'] == 'BufRead':
try:
# vim autocmd event based works
pass
except Exception:
# Ignore the error
pass
def get_complete_position(self, context):
m = re.search(r'\w*$', context['input'])
return m.start() if m else -1
def gather_candidates(self, context):
# return dict in list ([{},{},{}...])
return self.result_keywords
これで test.yml
を編集、そしてなんか文字をinsertすれば KEYWORD
にあるものが補完されるはずです。
あとはAPIからなにか取ってくるなり、language
のリストをディスクにキャッシュしておくなり…
ぼくもたまに補完結果をドキュメントの代わりにしたりしてます。
この50数行でとりあえず動いちゃう deoplete.nvim ソース、カジュアルにもっと書いていくとみんながいろいろなファイル補完できて良いと思います!
…しかしこの簡易版travisソース、ちょっとあれなんで…もうちょいちゃんとしたのに更新しますね…
いいね、かストック?すると通知がいくんでしょうか?
良かったらお願いします🙇
ではでは。良い補完ライフを!