空いた時間にVSCodeのRuby用拡張を進捗させたい。
Promise<T>
の代わりに Thenable<T>
と記載している例をVSCodeのコード例でよく見るが、これはjQuery.Deferredなど他の各種Promise系のやつでも受け入れるための型クラスらしい。
DocumentSelector
その拡張がどういうドキュメント (VSCodeがタブとかで開くテキストの単位) で有効になるかを絞り込むための条件を指定する単位。次の2つの情報を持てるらしい
- どのファイルタイプか
- e.g.
ruby
- e.g.
- どのschemeか
- e.g.
file
,untitled
- 現実的にはファイルが永続化されているかどうか (適当に新規タブで開いたやつはuntitled) を判別するのに使われることがほとんどっぽい?
- e.g.
https://code.visualstudio.com/api/references/document-selector
Example from lsp-example:
[
{ scheme: "file", language: "plaintext" }
]
Example from vscode-ruby-client:
[
{ scheme: "file", language: "ruby" },
{ scheme: "untitled", language: "ruby" },
]
vscode packageのこれをClientOptionsに指定したものの、詳しく調べていない。
workspace.createFileSystemWatcher
vscode-ruby-clientだと、clientOptionsのmiddleware.workspaceを指定している。これはmulti workspaceのための何かだろうか。
default import/export おさらいのコーナー。
import * as path from "path";
例えば、上のコードを下のコードのように書き換えてみる。
import path from "path";
すると、次のようなエラーが確認できる。
Module '"path"' can only be default-imported using the 'esModuleInterop' flagts(1259)
path.d.ts(167, 5): This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
これは、JavaScriptにおける幾つかのモジュールシステムのdefault import/exportに関する挙動が異なることが原因で……
TypeScriptは何も設定していない場合、ES6風のモジュールシステムを前提に動くので、
import a from "b"
に対しては、
const a = require("b").default
のようなコードを生成し、
import * as a from "b"
に対しては、
const a = require("b")
のようなコードを生成する。
path moduleは export default してはおらず普通に export しているので、後者の方法を使う必要があるという話。
このTypeScriptの挙動は
{ "esModuleInterop": true }
のように設定すると変えることもできる。
lsp-exampleは、トップレベルのパッケージがVSCode extensionとして配布される形になっている。
というのも、次のようにサーバープロセスを起動するためのパスが記載されているので、拡張のルートディレクトリから見て ./server/out/server.js というのがあることが分かる。
const serverModule = context.asAbsolutePath(
path.join("server", "out", "server.js")
);
vscode-ruby-clientが次のようになっているので
client = new LanguageClient('ruby', 'Ruby', serverOptions, clientOptions);
client.registerProposedFeatures();
registerProposedFeaturesの定義を見に行った。現在は空の配列が返ってくるだけだが、過去には
[
new pd.DiagnosticFeature(client),
new nb.NotebookDocumentSyncFeature(client)
]
のような配列が返ることもあったらしい。要はβ版の新機能を取り込むための処理だろうか。
vscode-ruby-client/srcを見るとlinterやらcompletionやらが転がっているが、これは過去にクライアント側で正規表現を用いて機能が実装されていた頃の名残で、現在はどうやら参照されていないように見える。
つまりクライアント側はほぼ何も実装しなくても良い感じっぽい。
lsp-exampleのサーバー側の実装を少しずつ読んでいる。
- onInitializeで、clientのcapabilitiesを見ながらserverのcapabilitiesを返す
- onInitializedで、必要な初期化処理をやっていく。connection.listenとか
この拡張の機能に関するデフォルトの設定があり、これが各テキストドキュメントごとのデフォルトの設定になる。テキストドキュメント単位で設定値を上書きすることが可能なので、こうなっている。テキストドキュメントのURIをIDとして、Mapオブジェクトを利用して設定値のキャッシュを持っている。
設定値はクライアントが保持していて、サーバーがそれを知るには connection.workspace.getConfigurationで問い合わせる必要がある。この問い合わせ結果を上述のキャッシュに保存しておいているという仕組み。既に問い合わせ済みであればそのキャッシュから取得する。設定値変更イベントがクライアントで発生した場合、それがconnection経由で伝わってくるので、その場合サーバーはキャッシュを全部消す。これにより、次回の設定値取得時には新たにクライアントから取得しにいくようになるという寸法。
テキストドキュメントが閉じられる際には、キャッシュからも設定値を消す。現在開かれているテキストドキュメントの設定値しか保持しないことで、サイズを小さく抑えるため?
設定が変更された場合は、diagnosticsの検査をいちからやり直す。設定値の変化により検査結果が変わることがあるため。
試しにhighlight機能を追加してみる。
まずonInitializeで返すcapabilitiesにプロパティを追加して、documentHighlightProviderに対応していることを示す。
diff --git a/server/src/server.ts b/server/src/server.ts
index 8e1ddf8..5b83ad7 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -54,6 +54,7 @@ connection.onInitialize((params: InitializeParams) => {
completionProvider: {
resolveProvider: true,
},
+ documentHighlightProvider: true,
},
};
if (hasWorkspaceFolderCapability) {
Provider.register(connection) を呼び出すタイミングとして、onInitializeとonInitializedの2パターンがあるけれど、どのProviderではどのタイミングを選べばいいのだろうか。
vscode-rubyでは、DocumentHighlightProviderはonInitializeで登録しているようだ。
https://github.com/microsoft/vscode-extension-samples にはdocumentHighlightProvider: trueとしている例は見当たらなかったので、実装例が無い。とりあえずvscode-rubyのやっている方法に従ってみることにする?
カーソルを合わせるとconnection.onDocumentHighlightで登録したコールバックが実行されることが無事確認できた。
Language Serverで出しているconsole.logは、Extension Development HostのOutputでセレクトボックスから「Ruby」を選ぶことで表示できた。これは……Clientで指定したnameが使われているっぽい?
自前実装で, endに対してDocumentHighlightが動くようになった。
until, for, while についても対応しようとしたら親子関係というか木の構造が違うらしいことが分かった。それから、moduleもAもどちらもtypeはmoduleであるということも分かり、判定が大変そう。本当にキーワードかどうかはtoStringすれば判定できる。