に参加して、VS Code の AI 支援開発について学びましょう。

Notebook API

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

ノートブックの構成要素

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

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

サンプル

  • JSON Notebook Serializer: JSON 入力を受け取り、カスタム NotebookRenderer で整形された JSON を出力するシンプルなノートブックの例。
  • Markdown Serializer: Markdown ファイルをノートブックとして開いて編集する。

この例では、従来のファイル拡張子 .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 は、コードセルを受け取り、コードを実行して何らかの、または何の出力も生成しない役割を担います。

コントローラーは、作成時に 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> タイプのノートブックを開く際の拡張機能の発見性が向上します。

サンプル

出力タイプ

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

テキスト出力、エラー出力、またはリッチ出力の「単純な」バリアント(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 フィールドにいくつかのプロパティを持つリッチ出力を生成する可能性があります。

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

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

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 コードを表示するノートブック

代替の mimetype をレンダリングするには、その mimetype 用の 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"
        ]
      }
    ]
  }
}

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

VS Code がレンダラーをレンダリングする準備が整うと、ロードされたエントリポイントスクリプトは vscode-notebook-renderer から ActivationFunction をエクスポートして 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 を使用しています。

インタラクティブノートブック(コントローラーとの通信)

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

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つは、ボタンをクリックする前から、すべての問題のコメントデータをすべてロードしていることです。さらに、少し多くのデータを表示したいだけなのに、まったく異なるMIMEタイプのコントローラーサポートが必要になります。

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

Diagram showing how controllers interact with renderers through the NotebookRendererScript

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

コントローラー内

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

最後に、コントローラーへの通信を設定します。NotebookController.onDidReceiveMessage メソッドは、レンダラーがグローバルな postKernelMessage 関数を使用してメッセージを投稿したときに呼び出されます。このメソッドを実装するには、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
        );
      }
    });
  }
}

インタラクティブなノートブック (拡張機能ホストとの通信)

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

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

レンダラー拡張機能の 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 ノートブック
© . This site is unofficial and not affiliated with Microsoft.