Visual Studio CodeのExtention開発をするために色々調べる。Custom Editorのサンプルを読みながら、基本的な作りやパターンを整理する。
イベント
VSCodeではイベントを Event
という関数で表現する。Observableやコールバックとは少し違う、その中間の印象がある。
https://code.visualstudio.com/api/references/vscode-api#Event
引数にイベントリスナとなる関数や、必要なら this
となるオブジェクトを与えると、イベントの購読を止めるための Disposable
を返す。引数に Disposable
の配列を与えると、戻り値のDisposable
に付け加えることができる。
例えばCustom Editorのサンプルでは、以下のように workspace
内のテキストが変更されるたびに、更新されたテキストファイルが何かをURIを使って判定して、Webviewの更新をしている。WebviewPanel(Webviewの入れ物)が破棄されたら、購読を停止(破棄)している。
const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() === document.uri.toString()) {
updateWebview();
}
});
// Make sure we get rid of the listener when our editor is closed.
webviewPanel.onDidDispose(() => {
changeDocumentSubscription.dispose();
});
Webview
Custom Editorのように、自分で作った画面を作りたい場合に使うのが Webview という機能。Webviewの中にはHTMLで作ったUIを作ることができて、HTML内でスクリプトを実行することで、VSCodeとやりとりをすることができる。
Webviewは視覚的にもオブジェクト的にも、以下のように階層化されている。
WebviewPanel
まずWindow
の中にはWebviewPanel
というWebview
の入れ物がある。このWebviewPanel
という入れ物には表題や「×ボタン」があり、表示の操作を受け付けることができるようになっている。例えば、onDispose
イベントで閉じられたことを検出することができる。
https://code.visualstudio.com/api/references/vscode-api#WebviewPanel
WebviewPanelは自分で作っても良いし、Custom Editorの場合は resolveCustomTextEditor メソッドがVSCodeから呼ばれる際に引数で与えてもらえるようになっている。(以下はCustom Editorのサンプル)
public async resolveCustomTextEditor(
document: vscode.TextDocument,
webviewPanel: vscode.WebviewPanel, // ここで与えてもらう
_token: vscode.CancellationToken
): Promise<void> {
// Setup initial content for the webview
webviewPanel.webview.options = {
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'view' ))]
};
webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview);
Webview
WebviewPanel
の中には、Webview
というHTML
を表示するビュー(その名の通り)がある。Webviewに表示するUI(HTML)は、html
プロパティに文字列で保持する。UIへのメッセージはpostMessage
メソッドで送信でき、UIからのメッセージは onDidReceiveMessage
で受信できるようになっている。
// Webviewからのメッセージ受信
webviewPanel.webview.onDidReceiveMessage(e => {
switch (e.type) {
case 'add':
this.addNewScratch(document);
return;
case 'delete':
this.deleteScratch(document, e.id);
return;
}
} );
// Webviewへのメッセージ送信
webviewPanel.webview.postMessage({
type: 'update',
text: document.getText(),
});
Webview内スクリプトからVSCodeを呼ぶ
Webview内に表示するHTMLでもスクリプト(Webview内スクリプトと呼ぶ)を実行することができる。Webview内スクリプトはVSCodeとの間で①メッセージの送受信と②状態(任意のオブジェクト)のset/getができるようになっている。(以下はCustom Editorのサンプル抜粋)
// VSCodeにアクセスするためのオブジェクト取得
const vscode = acquireVsCodeApi();
// ①メッセージの送受信
// メッセージ送信: Webview -> VSCode
vscode.postMessage({
type: 'add'
});
// メッセージ受信: Webview <- VSCode
window.addEventListener('message', event => {
const message = event.data; // The json data that the extension sent
// メッセージの処理
});
// ②状態(任意のオブジェクト)のset/get
// set: Webview -> VSCode
vscode.setState({ text });
// get: Webview <- VSCode
const state = vscode.getState();
AngularからVSCodeを呼ぶ
Angularは独自の変更検知をトリガとして画面の更新を行っているので、ネイティブのイベントに対してリスナを直接付けてしまうと、なぜか画面が更新されないという痛い目を見ることになる。
そのため、以下のようにサービスを作成してサービス経由でVSCodeとメッセージの送受信や状態のset/getをする必要がある。(APIのインターフェースは文書化されていない?)
import { Injectable } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Observable, Subject, Subscription } from 'rxjs';
// acquireVsCodeApi()の戻り値の型情報が無いのを補う
interface VsCodeApi {
postMessage(message: any): void;
getState(): any;
setState(state: any);
}
declare function acquireVsCodeApi(): VsCodeApi;
// VSCodeAPIを公開するサービス
@Injectable()
export class VsCodeService {
private api: VsCodeApi;
private messageSubject = new Subject<any>();
constructor(private eventManager: EventManager) {
this.api = acquireVsCodeApi();
this.eventManager.addGlobalEventListener('window', 'message', (msg:any)=>{ this.messageSubject.next(msg.data)} );
}
getState(): any {
return this.api.getState();
}
setState(state: any) {
this.api.setState(state);
}
get message$(): Observable<any> {
return this.messageSubject;
}
post(message: any) {
this.api.postMessage(message);
}
}