Movatterモバイル変換


[0]ホーム

URL:


AlisueAlisue
🐜

Deno で Vim/Neovim のプラグインを書く (denops.vim)

に公開
2024/02/04
!!!!

どうも、最近 Rust と Deno にハマってるありすえです。

今日はvim-jp で開発を開始したdenops.vim の紹介と denops.vim を利用したプラグイン作成のチュートリアルを書きたいと思います。

denops.vim とは

denops.vim は JavaScript/TypeScript のランタイムであるDeno を利用して Vim/Neovim 双方で動作するプラグインを作るためのエコシステムです。以下のような特徴があります。

  • Vim / Neovim で同一コードを利用可能
  • Vim プラグインとしてインストールが可能
  • Vim script と比較してエンジンが爆速なのでゴリ押しが可能
  • ユーザーによるライブラリの依存管理が不要
  • プラグインが別プロセスとして動作するため Vim が固まりにくい
  • プラグイン毎にスレッドが分かれているため相互干渉が起こりにくい

用語集

用語意味
DenopsDeno をランタイムとして利用した Vim/Neovim のプラグインエコシステムです
Denops プラグインDenops を用いて書かれた Vim/Neovim 双方で動作するプラグインを表します

Denops のインストール

Denops プラグインを利用するためには Denops 自体をインストールする必要があります。
これは Denops プラグインを利用するだけのユーザーも行う必要があります。

0. Deno のインストール

https://deno.land/#installation を参考に Deno をインストールしてください。
Getting Started の以下のコマンドを実行して結果が帰ってくれば成功です。

deno run https://deno.land/std/examples/welcome.ts

なお、既にインストール済みであれば、以下のコマンドで最新版にアップデートしてください。

deno upgrade

1. Denops のインストール

通常の Vim プラグインとしてdenops.vim をインストールしてください。
例えばvim-plug を利用している場合には.vimrc に以下のように記載してから:PlugInstall を実行します。

Plug'vim-denops/denops.vim'

開発チュートリアル

ここから小さな Denops プラグインを実際に作ってみます。プラグイン名はhelloworld でプラグインディレクトリはホーム直下のdps-helloworld と仮定します。

0. プラグイン開発前準備

Vim プラグインは Vim のruntimepath に存在している必要があります。Denops プラグインも Vim プラグインであるため、同様にruntimepath に存在する必要があります。そのため、以下を.vimrc に追記してください。

setruntimepath^=~/dps-helloworld

次に Deno の起動時型チェックなどを有効にするため Denops をデバッグモードで動作させます。
以下を.vimrc に追記してください。

let g:denops#debug=1
!

デバッグモードはパフォーマンス的な問題があります。開発が落ち着いたらデバッグモードを解除してください。

1. プラグインディレクトリ構造の作成

まず以下のコマンドで~/dps-helloworld を作成し、作業ディレクトリを変更します。
Windows を利用している方は、適時コマンドを読み替えてください。

mkdir ~/dps-helloworldcd ~/dps-helloworld

次に以下のコマンドで必要最低限のディレクトリ構造を作成します。

mkdir-p denops/helloworldtouch denops/helloworld/main.ts

最終的に以下のようなディレクトリ構造になっていれば OK です。

dps-helloworld└── denops    └── helloworld        └── main.ts

Denops は自動的にruntimepath 内のdenops/*/main.ts を読み込みます。
そのため上記のような構造が Denops プラグインの基本型となります。

2. 骨組みの追加

Denops プラグインはmain.ts がエクスポートするmain 関数を呼び出します。なお、渡されるdenops という値はdenops-std がエクスポートしているDenops クラスのインスタンスです。したがってmain.ts の内容を以下のように書き換えてください。

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 を再起動するのが良いです。

3. API の追加

Denops では各プラグインが API を関数として登録します。まず、与えられた文字列を返却するecho() 関数を API として登録してみましょう。main.ts を以下のように書き直してください。

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]) のように、文字列以外を与えると以下のようにエラーを吐きます。

4. Vim 機能の呼び出し

Denops プラグインから Vim の機能を呼び出すには渡されるdenops インスタンスを利用します。
先ほどのecho API を Vim のコマンドとして登録してみるので、以下のようにmain.ts を変更してください。

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 を参照してください。

5. 実用的なプラグインの開発

ここまでで、基本的なプラグインの作り方は説明したので、次は実用的なプラグインを作ってみます。

突然ですが、皆様はプログラミングしているときに突然迷路を解きたくなったことはありませんか?
僕はありません。
ただ、世の中には迷路が好きで好きでたまらない人もいるはずなので Vim からいつでも迷路を生成して表示できるプラグインを作ってみます。

迷路生成アルゴリズムから自作しても良いのですが、せっかく Deno を利用しているのでサードパーティの迷路生成ライブラリであるmaze_generator を使います。
まずHelloWorldEcho コマンドと同様にしてMaze コマンドを定義し、内部では迷路を生成してconsole.log() で出力します。

main.ts
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

これで完成でもいいのですが、少し味気がないのでバッファに出力してみましょう。以下のようにプログラムを書き換えてください。

main.ts
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 以外のコマンドを外部から与えられるようにしたり、現在の表示領域から迷路を生成したりなどいろいろ改良を加えて以下のようにしてみました。

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{ 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 を利用すると、かなり簡単に 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 双方で動作し、開発も簡単なエコシステムができるのではないか?と思い開発に踏み切りました。

Alisue

It's me

バッジを贈って著者を応援しよう

バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。


[8]ページ先頭

©2009-2025 Movatter.jp