Visual Studio Code

VSCode: Custom Editorサンプルを読む

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);
  }
}

コメントを残す