denops.vim plugin 開発 tips
deno に関する tips と混ざるかもしれないけどまぁそれはそれ
依存ライブラリ管理
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 ファイルに纏める手法が使われているようだ
次の場所で実際に使われている
- https://github.com/uki00a/denops-pomodoro.vim/blob/main/denops/pomodoro/deps.ts
- https://github.com/vim-denops/deno-denops-std/blob/main/deps.ts
deps.ts
を以下のように記述し、
export {
main,
} "https://deno.land/x/denops_std/mod.ts";
app.ts
では次のように import して使う
import { main } from "./deps.ts";
main(async ({ vim }) => { /* ... */ });
ちなみに import maps を denops で使えるようにするのであれば...
ユーザがインストールした denops plugin の中の import maps を全て取ってきて minimal version selection みたいな方法で 1つの import maps にまとめた上で deno
実行時に渡す、みたいなことをしないといけなそう...かな...
vim.register()
の引数
vim.register()
は Dispatcher
型を引数に取るが、この型は re-export を辿っていくと次で定義されている
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()
で登録された関数は、実行の際に this
が Session
型で指定されるので、ユーザとしては単に 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( /* */ ); }
などなど、好みの記法で処理を書いていけばよい
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
型であることが多いであろう