VS Codeのエージェントモードを拡張するには、を試してください!

ノートブック API

ノートブック API を使用すると、Visual Studio Code 拡張機能はファイルをノートブックとして開き、ノートブックのコードセルを実行し、さまざまなリッチでインタラクティブな形式でノートブックの出力をレンダリングできます。Jupyter Notebook や Google Colab のような人気のあるノートブックインターフェースをご存知かもしれません。ノートブック API を使用すると、Visual Studio Code 内で同様の体験が可能です。

ノートブックの構成要素

ノートブックは、セルのシーケンスとその出力で構成されます。ノートブックのセルは、Markdown セルまたはコードセルのいずれかであり、VS Code のコア内でレンダリングされます。出力はさまざまな形式にすることができます。プレーンテキスト、JSON、画像、HTML などの一部の出力形式は、VS Code コアによってレンダリングされます。アプリケーション固有のデータやインタラクティブなアプレットなどの他の形式は、拡張機能によってレンダリングされます。

ノートブック内のセルは、NotebookSerializer によってファイルシステムから読み書きされます。NotebookSerializer は、ファイルシステムからデータを読み取り、セルの記述に変換するだけでなく、ノートブックへの変更をファイルシステムに永続化する役割も担います。ノートブックのコードセルは、NotebookController によって実行できます。NotebookController は、セルの内容を受け取り、そこからプレーンテキストからフォーマットされたドキュメントやインタラクティブなアプレットまで、さまざまな形式で 0 個以上の出力を生成します。アプリケーション固有の出力形式とインタラクティブなアプレット出力は、NotebookRenderer によってレンダリングされます。

視覚的に

Overview of 3 components of notebooks: NotebookSerializer, NotebookController, and NotebookRenderer, and how they interact. Described textually above and in following sections.

シリアライザー

NotebookSerializer API リファレンス

NotebookSerializer は、ノートブックのシリアル化されたバイトを受け取り、それらのバイトを Markdown およびコードセルのリストを含む NotebookData に逆シリアル化する役割を担います。また、その逆の変換、つまり NotebookData を受け取り、そのデータをシリアル化されたバイトに変換して保存する役割も担います。

サンプル

この例では、従来のファイル拡張子 .ipynb の代わりに .notebook 拡張子を使用して、Jupyter Notebook 形式のファイルを表示するための簡略化されたノートブックプロバイダー拡張機能を構築します。

ノートブックシリアライザーは、package.jsoncontributes.notebooks セクションに次のように宣言されます。

{
    ...
    "contributes": {
        ...
        "notebooks": [
            {
                "type": "my-notebook",
                "displayName": "My Notebook",
                "selector": [
                    {
                        "filenamePattern": "*.notebook"
                    }
                ]
            }
        ]
    }
}

ノートブックシリアライザーは、拡張機能のアクティベーションイベントで登録されます。

import { TextDecoder, TextEncoder } from 'util';
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.workspace.registerNotebookSerializer('my-notebook', new SampleSerializer())
  );
}

interface RawNotebook {
  cells: RawNotebookCell[];
}

interface RawNotebookCell {
  source: string[];
  cell_type: 'code' | 'markdown';
}

class SampleSerializer implements vscode.NotebookSerializer {
  async deserializeNotebook(
    content: Uint8Array,
    _token: vscode.CancellationToken
  ): Promise<vscode.NotebookData> {
    var contents = new TextDecoder().decode(content);

    let raw: RawNotebookCell[];
    try {
      raw = (<RawNotebook>JSON.parse(contents)).cells;
    } catch {
      raw = [];
    }

    const cells = raw.map(
      item =>
        new vscode.NotebookCellData(
          item.cell_type === 'code'
            ? vscode.NotebookCellKind.Code
            : vscode.NotebookCellKind.Markup,
          item.source.join('\n'),
          item.cell_type === 'code' ? 'python' : 'markdown'
        )
    );

    return new vscode.NotebookData(cells);
  }

  async serializeNotebook(
    data: vscode.NotebookData,
    _token: vscode.CancellationToken
  ): Promise<Uint8Array> {
    let contents: RawNotebookCell[] = [];

    for (const cell of data.cells) {
      contents.push({
        cell_type: cell.kind === vscode.NotebookCellKind.Code ? 'code' : 'markdown',
        source: cell.value.split(/\r?\n/g)
      });
    }

    return new TextEncoder().encode(JSON.stringify(contents));
  }
}

次に、拡張機能を実行し、.notebook 拡張子で保存された Jupyter Notebook 形式のファイルを開いてみてください。

Notebook showing contents of a Jupyter Notebook formatted file

Jupyter 形式のノートブックを開き、そのセルをプレーンテキストとレンダリングされた Markdown の両方で表示し、セルを編集できます。ただし、出力はディスクに永続化されません。出力を保存するには、セルの出力を NotebookData からシリアル化および逆シリアル化する必要があります。

セルを実行するには、NotebookController を実装する必要があります。

コントローラー

NotebookController API リファレンス

NotebookController は、コードセルを受け取り、そのコードを実行して 0 個または複数の出力を生成する役割を担います。

コントローラーは、コントローラーの作成時に NotebookController#notebookType プロパティを設定することにより、ノートブックシリアライザーとノートブックのタイプに直接関連付けられます。その後、コントローラーは、拡張機能のアクティベート時にコントローラーを拡張機能のサブスクリプションにプッシュすることにより、グローバルに登録されます。

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(new Controller());
}

class Controller {
  readonly controllerId = 'my-notebook-controller-id';
  readonly notebookType = 'my-notebook';
  readonly label = 'My Notebook';
  readonly supportedLanguages = ['python'];

  private readonly _controller: vscode.NotebookController;
  private _executionOrder = 0;

  constructor() {
    this._controller = vscode.notebooks.createNotebookController(
      this.controllerId,
      this.notebookType,
      this.label
    );

    this._controller.supportedLanguages = this.supportedLanguages;
    this._controller.supportsExecutionOrder = true;
    this._controller.executeHandler = this._execute.bind(this);
  }

  private _execute(
    cells: vscode.NotebookCell[],
    _notebook: vscode.NotebookDocument,
    _controller: vscode.NotebookController
  ): void {
    for (let cell of cells) {
      this._doExecution(cell);
    }
  }

  private async _doExecution(cell: vscode.NotebookCell): Promise<void> {
    const execution = this._controller.createNotebookCellExecution(cell);
    execution.executionOrder = ++this._executionOrder;
    execution.start(Date.now()); // Keep track of elapsed time to execute cell.

    /* Do some execution here; not implemented */

    execution.replaceOutput([
      new vscode.NotebookCellOutput([
        vscode.NotebookCellOutputItem.text('Dummy output text!')
      ])
    ]);
    execution.end(true, Date.now());
  }
}

NotebookController を提供する拡張機能をそのシリアライザーとは別に公開する場合は、package.jsonkeywordsnotebookKernel<ViewTypeUpperCamelCased> のようなエントリを追加します。たとえば、github-issues ノートブックタイプ用の代替カーネルを公開した場合、拡張機能に notebookKernelGithubIssues キーワードを追加する必要があります。これにより、Visual Studio Code 内から <ViewTypeUpperCamelCased> タイプのノートブックを開く際の拡張機能の発見性が向上します。

サンプル

出力タイプ

出力は、テキスト出力、エラー出力、リッチ出力の 3 つの形式のいずれかである必要があります。カーネルは、セルの単一の実行に対して複数の出力を提供する場合があります。その場合、それらはリストとして表示されます。

テキスト出力、エラー出力、またはリッチ出力の「単純な」バリアント (HTML、Markdown、JSON など) のような単純な形式は VS Code コアによってレンダリングされますが、アプリケーション固有のリッチ出力タイプは NotebookRenderer によってレンダリングされます。拡張機能は、オプションで「単純な」リッチ出力を自分でレンダリングすることを選択できます。たとえば、Markdown 出力に LaTeX サポートを追加する場合などです。

Diagram of the different output types described above

テキスト出力

テキスト出力は最も単純な出力形式であり、使い慣れた多くの REPL と非常によく似ています。これらは text フィールドのみで構成され、セルの出力要素にプレーンテキストとしてレンダリングされます。

vscode.NotebookCellOutputItem.text('This is the output...');

Cell with simple text output

エラー出力

エラー出力は、ランタイムエラーを一貫したわかりやすい方法で表示するのに役立ちます。標準の Error オブジェクトをサポートしています。

try {
  /* Some code */
} catch (error) {
  vscode.NotebookCellOutputItem.error(error);
}

Cell with error output showing error name and message, as well as a stack trace with magenta text

リッチ出力

リッチ出力は、セル出力を表示する最も高度な形式です。出力データの多くの異なる表現を、MIME タイプをキーとして提供できます。たとえば、セル出力が GitHub Issue を表す場合、カーネルはその data フィールドにいくつかのプロパティを持つリッチ出力を生成する可能性があります。

  • Issue のフォーマットされたビューを含む text/html フィールド。
  • マシンで読み取り可能なビューを含む text/x-json フィールド。
  • NotebookRenderer が Issue の完全にインタラクティブなビューを作成するために使用できる application/github-issue フィールド。

この場合、text/html および text/x-json ビューは VS Code によってネイティブにレンダリングされますが、その MIME タイプに NotebookRenderer が登録されていない場合、application/github-issue ビューはエラーを表示します。

execution.replaceOutput([new vscode.NotebookCellOutput([
                            vscode.NotebookCellOutputItem.text('<b>Hello</b> World', 'text/html'),
                            vscode.NotebookCellOutputItem.json({ hello: 'world' }),
                            vscode.NotebookCellOutputItem.json({ custom-data-for-custom-renderer: 'data' }, 'application/custom'),
                        ])]);

Cell with rich output showing switching between formatted HTML, a JSON editor, and an error message showing no renderer is available (application/hello-world)

デフォルトでは、VS Code は次の MIME タイプをレンダリングできます。

  • application/javascript
  • text/html
  • image/svg+xml
  • text/markdown
  • image/png
  • image/jpeg
  • text/plain

VS Code は、これらの MIME タイプを組み込みエディターでコードとしてレンダリングします。

  • text/x-json
  • text/x-javascript
  • text/x-html
  • text/x-rust
  • ...その他の組み込みまたはインストールされた言語の text/x-LANGUAGE_ID。

このノートブックは、組み込みエディターを使用して Rust コードを表示しています: 組み込みの Monaco エディターで Rust コードを表示するノートブック

代替の MIME タイプをレンダリングするには、その MIME タイプに対して NotebookRenderer を登録する必要があります。

ノートブックレンダラー

ノートブックレンダラーは、特定の MIME タイプの出力データを受け取り、そのデータのレンダリングされたビューを提供する役割を担います。出力セルで共有されるレンダラーは、これらのセル間でグローバル状態を維持できます。レンダリングされるビューの複雑さは、単純な静的 HTML から動的な完全にインタラクティブなアプレットまで多岐にわたります。このセクションでは、GitHub Issue を表す出力をレンダリングするためのさまざまな手法を探ります。

Yeoman ジェネレーターからボイラープレートを使用すると、すぐに始めることができます。これを行うには、まず Yeoman と VS Code ジェネレーターを次のようにインストールします。

npm install -g yo generator-code

次に、yo code を実行し、New Notebook Renderer (TypeScript) を選択します。

このテンプレートを使用しない場合は、拡張機能の package.jsonkeywordsnotebookRenderer を追加し、拡張機能の名前または説明のどこかにその MIME タイプを記述して、ユーザーがレンダラーを見つけられるようにしてください。

シンプルな非対話型レンダラー

レンダラーは、拡張機能の package.jsoncontributes.notebookRenderer プロパティに貢献することで、一連の MIME タイプに対して宣言されます。このレンダラーは、ms-vscode.github-issue-notebook/github-issue 形式の入力で動作します。これは、インストールされた何らかのコントローラーが提供できると仮定します。

{
  "activationEvents": ["...."],
  "contributes": {
    ...
    "notebookRenderer": [
      {
        "id": "github-issue-renderer",
        "displayName": "GitHub Issue Renderer",
        "entrypoint": "./out/renderer.js",
        "mimeTypes": [
          "ms-vscode.github-issue-notebook/github-issue"
        ]
      }
    ]
  }
}

出力レンダラーは、常に VS Code の他の UI から分離された単一の iframe でレンダリングされ、誤って VS Code に干渉したり、速度低下を引き起こしたりしないようにします。貢献は「エントリポイント」スクリプトを参照しており、これは出力がレンダリングされる直前にノートブックの iframe にロードされます。エントリポイントは単一のファイルである必要があり、自分で記述するか、Webpack、Rollup、Parcel などのバンドラーを使用して作成できます。

ロードされると、エントリポイントスクリプトは vscode-notebook-renderer から ActivationFunction をエクスポートし、VS Code がレンダラーをレンダリングする準備ができたときに UI をレンダリングします。たとえば、これによりすべての GitHub Issue データが JSON としてセル出力に配置されます。

import type { ActivationFunction } from 'vscode-notebook-renderer';

export const activate: ActivationFunction = context => ({
  renderOutputItem(data, element) {
    element.innerText = JSON.stringify(data.json());
  }
});

完全な API 定義はこちらを参照してください。TypeScript を使用している場合は、@types/vscode-notebook-renderer をインストールし、tsconfig.jsontypes 配列に vscode-notebook-renderer を追加すると、これらの型をコードで利用できます。

より豊富なコンテンツを作成するには、DOM 要素を手動で作成するか、Preact などのフレームワークを使用して出力要素にレンダリングできます。たとえば、

import type { ActivationFunction } from 'vscode-notebook-renderer';
import { h, render } from 'preact';

const Issue: FunctionComponent<{ issue: GithubIssue }> = ({ issue }) => (
  <div key={issue.number}>
    <h2>
      {issue.title}
      (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>)
    </h2>
    <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} />
    <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div>
  </div>
);

const GithubIssues: FunctionComponent<{ issues: GithubIssue[]; }> = ({ issues }) => (
  <div>{issues.map(issue => <Issue key={issue.number} issue={issue} />)}</div>
);

export const activate: ActivationFunction = (context) => ({
    renderOutputItem(data, element) {
        render(<GithubIssues issues={data.json()} />, element);
    }
});

ms-vscode.github-issue-notebook/github-issue データフィールドを持つ出力セルでこのレンダラーを実行すると、次の静的 HTML ビューが得られます。

Cell output showing rendered HTML view of issue

コンテナ外の要素やその他の非同期プロセスがある場合は、disposeOutputItem を使用してそれらを破棄できます。このイベントは、出力がクリアされたとき、セルが削除されたとき、および既存のセルに新しい出力がレンダリングされる前に発生します。たとえば、

const intervals = new Map();

export const activate: ActivationFunction = (context) => ({
    renderOutputItem(data, element) {
        render(<GithubIssues issues={data.json()} />, element);

        intervals.set(data.mime, setInterval(() => {
            if(element.querySelector('h2')) {
                element.querySelector('h2')!.style.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
            }
        }, 1000));
    },
    disposeOutputItem(id) {
        clearInterval(intervals.get(id));
        intervals.delete(id);
    }
});

ノートブックのすべての出力は、同じ iframe 内の異なる要素でレンダリングされることを覚えておくことが重要です。document.querySelector などの関数を使用する場合は、他の出力との競合を避けるために、関心のある特定の出力にスコープを限定してください。この例では、この問題を回避するために element.querySelector を使用しています。

対話型ノートブック (コントローラーとの通信)

レンダリングされた出力のボタンをクリックした後、Issue のコメントを表示する機能を追加したいと想像してください。コントローラーが ms-vscode.github-issue-notebook/github-issue-with-comments MIME タイプでコメント付きの Issue データを提供できると仮定すると、すべてのコメントを事前に取得して次のように実装しようとするかもしれません。

const Issue: FunctionComponent<{ issue: GithubIssueWithComments }> = ({ issue }) => {
  const [showComments, setShowComments] = useState(false);

  return (
    <div key={issue.number}>
      <h2>
        {issue.title}
        (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>)
      </h2>
      <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} />
      <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div>
      <button onClick={() => setShowComments(true)}>Show Comments</button>
      {showComments && issue.comments.map(comment => <div>{comment.text}</div>)}
    </div>
  );
};

これはすぐにいくつかの問題を引き起こします。1つは、ボタンをクリックする前から、すべてのIssueのコメントデータを完全に読み込んでいます。さらに、もう少しデータを見たいだけなのに、まったく異なるMIMEタイプのコントローラーサポートが必要です。

代わりに、コントローラーは、VS Code が iframe 内にロードするプリロードスクリプトを含めることで、レンダラーに追加機能を提供できます。このスクリプトは、コントローラーと通信するために使用できるグローバル関数 postKernelMessage および onDidReceiveKernelMessage にアクセスできます。

Diagram showing how controllers interact with renderers through the NotebookRendererScript

たとえば、コントローラーの rendererScripts を変更して、Extension Host への接続を作成し、レンダラーが使用するためのグローバル通信スクリプトを公開する新しいファイルを参照するようにすることができます。

コントローラー内

class Controller {
  // ...

  readonly rendererScriptId = 'my-renderer-script';

  constructor() {
    // ...

    this._controller.rendererScripts.push(
      new vscode.NotebookRendererScript(
        vscode.Uri.file(/* path to script */),
        rendererScriptId
      )
    );
  }
}

package.json で、スクリプトをレンダラーの依存関係として指定します。

{
  "activationEvents": ["...."],
  "contributes": {
    ...
    "notebookRenderer": [
      {
        "id": "github-issue-renderer",
        "displayName": "GitHub Issue Renderer",
        "entrypoint": "./out/renderer.js",
        "mimeTypes": [...],
        "dependencies": [
            "my-renderer-script"
        ]
      }
    ]
  }
}

スクリプトファイルで、コントローラーと通信するための通信関数を宣言できます。

import 'vscode-notebook-renderer/preload';

globalThis.githubIssueCommentProvider = {
  loadComments(issueId: string, callback: (comments: GithubComment[]) => void) {
    postKernelMessage({ command: 'comments', issueId });

    onDidReceiveKernelMessage(event => {
      if (event.data.type === 'comments' && event.data.issueId === issueId) {
        callback(event.data.comments);
      }
    });
  }
};

そして、それをレンダラーで利用できます。他の開発者が githubIssueCommentProvider を実装していない他のノートブックやコントローラーで GitHub Issue の出力を作成する可能性があるため、コントローラーのレンダリングスクリプトによって公開されたグローバルが利用可能かどうかを確認する必要があります。この場合、グローバルが利用可能な場合にのみコメントを読み込むボタンを表示します。

const canLoadComments = globalThis.githubIssueCommentProvider !== undefined;
const Issue: FunctionComponent<{ issue: GithubIssue }> = ({ issue }) => {
  const [comments, setComments] = useState([]);
  const loadComments = () =>
    globalThis.githubIssueCommentProvider.loadComments(issue.id, setComments);

  return (
    <div key={issue.number}>
      <h2>
        {issue.title}
        (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>)
      </h2>
      <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} />
      <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div>
      {canLoadComments && <button onClick={loadComments}>Load Comments</button>}
      {comments.map(comment => <div>{comment.text}</div>)}
    </div>
  );
};

最後に、コントローラーへの通信を設定します。レンダラーがグローバルな postKernelMessage 関数を使用してメッセージを投稿すると、NotebookController.onDidReceiveMessage メソッドが呼び出されます。このメソッドを実装するには、onDidReceiveMessage にアタッチしてメッセージをリッスンします。

class Controller {
  // ...

  constructor() {
    // ...

    this._controller.onDidReceiveMessage(event => {
      if (event.message.command === 'comments') {
        _getCommentsForIssue(event.message.issueId).then(
          comments =>
            this._controller.postMessage({
              type: 'comments',
              issueId: event.message.issueId,
              comments
            }),
          event.editor
        );
      }
    });
  }
}

対話型ノートブック (拡張ホストとの通信)

出力項目を別のエディターで開く機能を追加したいと想像してください。これを可能にするには、レンダラーが拡張ホストにメッセージを送信できる必要があり、拡張ホストがエディターを起動します。

これは、レンダラーとコントローラーが2つの異なる拡張機能であるシナリオで役立ちます。

レンダラー拡張機能の package.json で、requiresMessaging の値を optional に指定します。これにより、レンダラーは拡張ホストへのアクセスがある場合とない場合のどちらの状況でも機能します。

{
  "activationEvents": ["...."],
  "contributes": {
    ...
    "notebookRenderer": [
      {
        "id": "output-editor-renderer",
        "displayName": "Output Editor Renderer",
        "entrypoint": "./out/renderer.js",
        "mimeTypes": [...],
        "requiresMessaging": "optional"
      }
    ]
  }
}

requiresMessaging の可能な値には、次のものがあります。

  • always: メッセージングが必要です。レンダラーは、拡張ホストで実行できる拡張機能の一部である場合にのみ使用されます。
  • optional: 拡張ホストが利用可能な場合、レンダラーはメッセージングがある方が優れていますが、レンダラーをインストールして実行するために必須ではありません。
  • never: レンダラーはメッセージングを必要としません。

後者の2つのオプションが推奨されます。これにより、レンダラー拡張機能の移植性が確保され、拡張ホストが必ずしも利用可能ではない他のコンテキストでも利用できるようになります。

レンダラースクリプトファイルは次のように通信を設定できます。

import { ActivationFunction } from 'vscode-notebook-renderer';

export const activate: ActivationFunction = (context) => ({
  renderOutputItem(data, element) {
    // Render the output using the output `data`
    ....
    // The availability of messaging depends on the value in `requiresMessaging`
    if (!context.postMessage){
      return;
    }

    // Upon some user action in the output (such as clicking a button),
    // send a message to the extension host requesting the launch of the editor.
    document.querySelector('#openEditor').addEventListener('click', () => {
      context.postMessage({
        request: 'showEditor',
        data: '<custom data>'
      })
    });
  }
});

そして、そのメッセージを拡張ホストで次のように利用できます。

const messageChannel = notebooks.createRendererMessaging('output-editor-renderer');
messageChannel.onDidReceiveMessage(e => {
  if (e.message.request === 'showEditor') {
    // Launch the editor for the output identified by `e.message.data`
  }
});

  • メッセージが配信される前に拡張機能が拡張ホストで実行されていることを確認するには、activationEventsonRenderer:<your renderer id> を追加し、拡張機能の activate 関数で通信を設定します。
  • レンダラー拡張機能から拡張ホストに送信されたすべてのメッセージが配信されるとは限りません。ユーザーは、レンダラーからのメッセージが配信される前にノートブックを閉じることがあります。

デバッグのサポート

プログラミング言語を実装するような一部のコントローラーでは、セルの実行をデバッグできるようにすることが望ましい場合があります。デバッグサポートを追加するには、ノートブックカーネルは、デバッグアダプターを実装できます。これは、デバッグアダプタープロトコル (DAP) を直接実装するか、既存のノートブックデバッガーにプロトコルを委譲および変換するか(「vscode-simple-jupyter-notebook」サンプルで行われているように)、または既存の未修正のデバッグ拡張機能を使用し、ノートブックのニーズに合わせてDAPをその場で変換する(「vscode-nodebook」で行われているように)ことによって行われます。

サンプル

  • vscode-nodebook: VS Code の組み込み JavaScript デバッガーといくつかの簡単なプロトコル変換によって提供されるデバッグサポートを備えた Node.js ノートブック
  • vscode-simple-jupyter-notebook: 既存の Xeus デバッガーによって提供されるデバッグサポートを備えた Jupyter ノートブック