Open5

denops.vim plugin 開発 tips

tennashitennashi

依存ライブラリ管理

denops plugin は https://github.com/vim-denops/deno-denops-std で定義された main() 関数に渡す引数として定義される
つまり必ず

import { main } from "https://deno.land/x/denops_std@v0.14/mod.ts";

のような import を書く
@v0.14 と書いてる通りここで import するライブラリの version を指定できる。
version 指定は無くとも latest 指定として動くが deno LSP サーバから怒られるし、なによりそもそも指定しておいた方が安心である

が、このように使う側のコードに version 指定が埋められてしまうと、いざ version を上げようと思ったときに修正すべき箇所が分散してしまい困ってしまう

deno には標準で import maps という機能があるが、これは deno コマンドの実行時に --import_map=<FILE> という形式で指定しなければならない
一方 denops.vim が起動する deno プロセスは恐らく plugin 共通で 1 つであるため、全ての plugin の import map をかき集める必要があり、使うのは難しい

そこで JavaScript(正確には ECMAScript かな?) の re-export を使って依存ライブラリを deps.ts という 1 ファイルに纏める手法が使われているようだ

次の場所で実際に使われている

deps.ts を以下のように記述し、

export { 
  main,
} "https://deno.land/x/denops_std/mod.ts";

app.ts では次のように import して使う

import { main } from "./deps.ts";

main(async ({ vim }) => { /* ... */ });
tennashitennashi

ちなみに import maps を denops で使えるようにするのであれば...
ユーザがインストールした denops plugin の中の import maps を全て取ってきて minimal version selection みたいな方法で 1つの import maps にまとめた上で deno 実行時に渡す、みたいなことをしないといけなそう...かな...

tennashitennashi

vim.register() の引数

https://doc.deno.land/https/deno.land/x/denops_std/mod.ts#Vim
vim.register()Dispatcher 型を引数に取るが、この型は re-export を辿っていくと次で定義されている

https://github.com/lambdalisue/deno-msgpack-rpc/blob/fcde5d39520fa6f72f6e0b18328b4d2387944c38/session.ts#L9-L11

export interface Dispatcher {
  [key: string]: (this: Session, ...args: unknown[]) => Promise<unknown>;
}

第一引数は this の型定義なので、通常のユースケースでは無視してよく、vim.register() に渡す Dispatcher 型のオブジェクトは例えば次のように書く

vim.register({
  async hoge(arg0: unknown, arg1: unknown): Promise<unknown> { /* */ },
});

TypeScript の話

Dispatcher の型定義は先に伸べた通りだが、TypeScript 初心者の私には読むのに一苦労だったのでポインタくらいを残しておく

最初の [key: string] の部分は index signatures と呼ばれるもので、map っぽい型を作るための記法である
(this: Session, ...) の部分は先にも書いた通り this への型付け である

vim.resolve() で登録された関数は、実行の際に thisSession 型で指定されるので、ユーザとしては単に this を無視するように (...args: unknown[]) => Promise<unknown> 型の関数を適当に書いていけばよい
// 恐らく推奨はされないだろうがこの Session 型をどうしても直接操作したい場合は function(this: Session, arg: unknown): Promise<unknown> { /* */ } のように this を明記して利用すればよいだろう

    return await this.#dispatcher[method].apply(this, params);

あとは Promise<T> が返り値として期待されているので

async hoge() { /* */ }
"hoge": async () => { /* */ }
"hoge": () => { return Promise.resolve( /* */ ); }

などなど、好みの記法で処理を書いていけばよい

tennashitennashi

constructor() の呼び出しを遅延する

React.createElement(Hoge, ...) のように引数に class/function 名(コンストラクタ自体)を入れて、オブジェクトの生成(new) は createElement() の中でやりたいとき

Abstruct Context Signatures にあるようにコンストラクタのシグネチャを型として指定する
具体的には

class A {
  constructor() {
    console.log("constructor()");
  }

  hoge() {
    console.log("hoge()");
  }
}

const hoge = (T: new () => A) => {
  const t = new T();
  t.hoge();
}

class A のように具体的な型の場合は T: typeof A というシグネチャも使えるが、interface 型のような抽象型の場合は恐らくこの方法しか使えない
// そして恐らくこういう書き方をしたいときは interface 型であることが多いであろう