2024/02/03 に v6.0.0 がリリースされており、この記事は非常に古くなっています。
この記事の内容は Denops のプリミティブな機能のみを利用しているため v6 でも問題なく動作しますが、LSP による補完や型チェックが効かないなど、開発者体験にまつわる問題があります。
LSP による補完や型チェックを有効にし、より体験が良い開発を行うチュートリアルが公式ドキュメント (英語) に記載されているため、ご一読ください。
🎉 2021/07/19 に v1.0.0 として正式にリリースしました 🎉
https://github.com/vim-denops/denops.vim/releases/tag/v1.0.0
If you prefer English, visithttps://vim-denops.github.io/denops-documentation
対象バージョン:
denops.vim v1.0.0 (2021.07.19)
denops_std v1.0.0 (2021.07.19)
どうも、最近 Rust と Deno にハマってるありすえです。
今日はvim-jp で開発を開始したdenops.vim の紹介と denops.vim を利用したプラグイン作成のチュートリアルを書きたいと思います。
denops.vim は JavaScript/TypeScript のランタイムであるDeno を利用して Vim/Neovim 双方で動作するプラグインを作るためのエコシステムです。以下のような特徴があります。
| 用語 | 意味 |
|---|---|
| Denops | Deno をランタイムとして利用した Vim/Neovim のプラグインエコシステムです |
| Denops プラグイン | Denops を用いて書かれた Vim/Neovim 双方で動作するプラグインを表します |
Denops プラグインを利用するためには Denops 自体をインストールする必要があります。
これは Denops プラグインを利用するだけのユーザーも行う必要があります。
https://deno.land/#installation を参考に Deno をインストールしてください。
Getting Started の以下のコマンドを実行して結果が帰ってくれば成功です。
deno run https://deno.land/std/examples/welcome.tsなお、既にインストール済みであれば、以下のコマンドで最新版にアップデートしてください。
deno upgrade通常の Vim プラグインとしてdenops.vim をインストールしてください。
例えばvim-plug を利用している場合には.vimrc に以下のように記載してから:PlugInstall を実行します。
Plug'vim-denops/denops.vim'ここから小さな Denops プラグインを実際に作ってみます。プラグイン名はhelloworld でプラグインディレクトリはホーム直下のdps-helloworld と仮定します。
Vim プラグインは Vim のruntimepath に存在している必要があります。Denops プラグインも Vim プラグインであるため、同様にruntimepath に存在する必要があります。そのため、以下を.vimrc に追記してください。
setruntimepath^=~/dps-helloworld次に Deno の起動時型チェックなどを有効にするため Denops をデバッグモードで動作させます。
以下を.vimrc に追記してください。
let g:denops#debug=1デバッグモードはパフォーマンス的な問題があります。開発が落ち着いたらデバッグモードを解除してください。
まず以下のコマンドで~/dps-helloworld を作成し、作業ディレクトリを変更します。
Windows を利用している方は、適時コマンドを読み替えてください。
mkdir ~/dps-helloworldcd ~/dps-helloworld次に以下のコマンドで必要最低限のディレクトリ構造を作成します。
mkdir-p denops/helloworldtouch denops/helloworld/main.ts最終的に以下のようなディレクトリ構造になっていれば OK です。
dps-helloworld└── denops └── helloworld └── main.tsDenops は自動的にruntimepath 内のdenops/*/main.ts を読み込みます。
そのため上記のような構造が Denops プラグインの基本型となります。
Denops プラグインはmain.ts がエクスポートするmain 関数を呼び出します。なお、渡されるdenops という値はdenops-std がエクスポートしているDenops クラスのインスタンスです。したがってmain.ts の内容を以下のように書き換えてください。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";exportasyncfunctionmain(denops: Denops):Promise<void>{// ここにプラグインの処理を記載するconsole.log("Hello Denops!");};この状態で一度 Vim を再起動すると起動時に[denops] Hello Denops! が表示されます。
Vim を再起動するのが面倒な方は:call denops#server#restart() として Denops を再起動するのが良いです。
Denops では各プラグインが API を関数として登録します。まず、与えられた文字列を返却するecho() 関数を API として登録してみましょう。main.ts を以下のように書き直してください。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";import{ ensureString}from"https://deno.land/x/unknownutil@v1.0.0/mod.ts";exportasyncfunctionmain(denops: Denops):Promise<void>{ denops.dispatcher={asyncecho(text:unknown):Promise<unknown>{// `text` が string 型であることを保証するensureString(text);returnawaitPromise.resolve(text);},};};引数が全てunknown 型で戻り値がPromise<unknown> もしくはPromise<void> な関数のみ API として登録可能です。
これでecho という API がhelloworld というプラグインに登録されます。この API を呼び出すにはdenops#request({plugin}, {func}, {args}) を利用します。Vim を再起動後以下のコマンドを実行してみてください。
:echo denops#request('helloworld','echo',["Hello Denops!"])これによりHello Denops! が表示されれば成功です。
なおdenops#request('helloworld', 'echo', [123]) のように、文字列以外を与えると以下のようにエラーを吐きます。
Denops プラグインから Vim の機能を呼び出すには渡されるdenops インスタンスを利用します。
先ほどのecho API を Vim のコマンドとして登録してみるので、以下のようにmain.ts を変更してください。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";import{ execute}from"https://deno.land/x/denops_std@v1.0.0/helper/mod.ts";import{ ensureString}from"https://deno.land/x/unknownutil@v1.0.0/mod.ts";exportasyncfunctionmain(denops: Denops):Promise<void>{ denops.dispatcher={asyncecho(text:unknown):Promise<unknown>{ensureString(text);returnawaitPromise.resolve(text);},};awaitexecute( denops,`command! -nargs=1 HelloWorldEcho echomsg denops#request('${denops.name}', 'echo', [<q-args>])`,);};execute() は渡された複数行文字列を Vim script として実行します。またdenops.name は実行中のプラグイン名を表します。これによりHelloWorldEcho コマンドが登録されるので Vim を再起動後以下のコマンドを実行してください。
:HelloWorldEcho Hello Vim!これにより以下のようにHello Vim! が表示されれば成功です。
なおdenops の詳細 API はhttps://doc.deno.land/https/deno.land/x/denops_std/mod.ts#Denops を参照してください。
ここまでで、基本的なプラグインの作り方は説明したので、次は実用的なプラグインを作ってみます。
突然ですが、皆様はプログラミングしているときに突然迷路を解きたくなったことはありませんか?
僕はありません。
ただ、世の中には迷路が好きで好きでたまらない人もいるはずなので Vim からいつでも迷路を生成して表示できるプラグインを作ってみます。
迷路生成アルゴリズムから自作しても良いのですが、せっかく Deno を利用しているのでサードパーティの迷路生成ライブラリであるmaze_generator を使います。
まずHelloWorldEcho コマンドと同様にしてMaze コマンドを定義し、内部では迷路を生成してconsole.log() で出力します。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";import{ Maze}from"https://deno.land/x/maze_generator@v0.4.0/mod.js";exportasyncfunctionmain(denops: Denops):Promise<void>{ denops.dispatcher={asyncmaze():Promise<void>{const maze=newMaze({}).generate();const content= maze.getString();console.log(content);},};await denops.cmd(`command! Maze call denops#request('${denops.name}', 'maze', [])`);};Vim を再起動し以下のコマンドで出力を確認すると迷路が生成できているのがわかります。
:Maze:mesこれで完成でもいいのですが、少し味気がないのでバッファに出力してみましょう。以下のようにプログラムを書き換えてください。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";import{ Maze}from"https://deno.land/x/maze_generator@v0.4.0/mod.js";exportasyncfunctionmain(denops: Denops):Promise<void>{ denops.dispatcher={asyncmaze():Promise<void>{const maze=newMaze({}).generate();const content= maze.getString();await denops.cmd("enew");awaitdenops.call("setline",1, content.split(/\n/));},};await denops.cmd(`command! Maze call denops#request('${denops.name}', 'maze', [])`);};上記ではdenops.cmd() で Vim のenew コマンドを呼び出し新規バッファを現在の Window で開いた後denops.call() でsetline() 関数を呼ぶことでバッファに迷路を書き込んでいます。
これを実行すると以下のようになります。
良い感じですね。
これで終わりでもいいですが、せっかくなのでenew 以外のコマンドを外部から与えられるようにしたり、現在の表示領域から迷路を生成したりなどいろいろ改良を加えて以下のようにしてみました。
import{ Denops}from"https://deno.land/x/denops_std@v1.0.0/mod.ts";import{ execute}from"https://deno.land/x/denops_std@v1.0.0/helper/mod.ts";import{ Maze}from"https://deno.land/x/maze_generator@v0.4.0/mod.js";import{ ensureString}from"https://deno.land/x/unknownutil@v1.0.0/mod.ts";exportasyncfunctionmain(denops: Denops):Promise<void>{ denops.dispatcher={asyncmaze(opener:unknown):Promise<void>{ensureString(opener);const[xSize, ySize]=(await denops.eval("[&columns, &lines]"))as[number,number];const maze=newMaze({ xSize: xSize/3, ySize: ySize/3,}).generate();const content= maze.getString();await denops.cmd(opener||"new");awaitdenops.call("setline",1, content.split(/\r?\n/g));awaitexecute(denops,` setlocal bufhidden=wipe buftype=nofile setlocal nobackup noswapfile setlocal nomodified nomodifiable`);},};await denops.cmd(`command! -nargs=? -bar Maze call denops#request('${denops.name}', 'maze', [<q-args>])`);};依存モジュールが増えてくると管理が煩雑になります。
Denops 本体及び関連モジュールdeps.ts およびdeps_test.ts で一括管理した上でudd というアップデートマネージャーでアップデート管理しています。
ちゃんと小さな迷路ができてますね。
どうでしょう?Denops を利用すると、かなり簡単に Vim/Neovim で動くプラグインが作れると思いませんか?
まだまだ開発中ですが Vim/Neovim 双方で効率的に動くポータビリティが高いエコシステムになっていると思います。
よければ、このチュートリアルと以下のドキュメントを参考に Denops プラグインを作ってみてください。
皆様のフィードバックをお待ちしております 🙇
今 Vim/Neovim の関係は大きな変貌期にいます。
Vim 側は Vim script の欠点を補った新しい言語である Vim 9 script の開発を進めており Neovim 側は Vim script を完全に捨てて Lua に移行しようとしています。
このように Vim/Neovim の乖離が大きく広がっており、双方で動作するプラグインを書くのが非常に難しくなってきている状態です。
そんな中coc.nvim はランタイムに Node.js を採用し Vim/Neovim のプラグイン機構の外 で独自のエコシステムを展開することで Vim/Neovim 双方をサポートすることを可能にしています。
しかし coc.nvim が採用している Node.js は依存管理が複雑なため、プラグインとして利用するにはビルドが必要だったりと、エコシステムとして使い勝手が良いものではありません。
そのため依存管理を内包し、バイナリ一つがあれば動作する Deno をベースにすれば Vim/Neovim 双方で動作し、開発も簡単なエコシステムができるのではないか?と思い開発に踏み切りました。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
