Notebook API

筆記本 API 允許 Visual Studio Code 擴充功能將檔案開啟為筆記本,執行筆記本中的程式碼儲存格,並以各種豐富且具互動性的格式呈現筆記本輸出。您可能熟悉 Jupyter Notebook 或 Google Colab 等熱門的筆記本介面——筆記本 API 讓您能在 Visual Studio Code 內享有類似的體驗。

筆記本的組成部分

筆記本由一系列儲存格及其輸出所組成。筆記本的儲存格可以是 Markdown 儲存格程式碼儲存格,並由 VS Code 核心進行渲染。輸出可以有多種格式。部分輸出格式(如純文字、JSON、圖片和 HTML)由 VS Code 核心直接渲染。其他格式(如應用程式特定的資料或互動式小工具)則由擴充功能進行渲染。

筆記本中的儲存格透過 NotebookSerializer 讀取並寫入檔案系統,它負責處理從檔案系統讀取資料並將其轉換為儲存格描述,以及將筆記本的變更持久保存回檔案系統。筆記本的程式碼儲存格可由 NotebookController 執行,它會獲取儲存格內容並產生零個或多個輸出,格式從純文字到格式化文件或互動式小工具不等。應用程式特定的輸出格式和互動式小工具輸出則由 NotebookRenderer 進行渲染。

視覺化說明

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

序列化器 (Serializer)

NotebookSerializer API 參考

NotebookSerializer 負責接收筆記本序列化後的位元組,並將其反序列化為包含 Markdown 和程式碼儲存格列表的 NotebookData。它也負責相反的轉換:將 NotebookData 轉換為序列化後的位元組以便儲存。

範例

範例

在此範例中,我們建立了一個簡化的筆記本提供者擴充功能,用於檢視副檔名為 .notebook(而非傳統的 .ipynb)的 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

控制器 (Controller)

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.jsonkeywords 中加入類似 notebookKernel<ViewTypeUpperCamelCased> 的項目。例如,如果您為 github-issues 筆記本類型發布了替代核心,則應在擴充功能中加入 notebookKernelGithubIssues 關鍵字。這能提升在 Visual Studio Code 內開啟 <ViewTypeUpperCamelCased> 類型筆記本時,擴充功能的被發現機率。

範例

輸出類型

輸出必須採用三種格式之一:文字輸出、錯誤輸出或豐富輸出 (Rich Output)。核心可以為單次儲存格執行提供多個輸出,在這種情況下,它們將以列表形式顯示。

如文字輸出、錯誤輸出或「簡單」類型的豐富輸出(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

豐富輸出 (Rich Output)

豐富輸出是顯示儲存格輸出最進階的形式。它們允許提供多種不同呈現方式的輸出資料,並以 MIME 類型作為索引鍵。例如,如果儲存格輸出代表一個 GitHub Issue,核心可能會產生一個在其 data 欄位中包含多個屬性的豐富輸出:

  • 一個包含議題格式化檢視的 text/html 欄位。
  • 一個包含機器可讀檢視的 text/x-json 欄位。
  • 一個 application/github-issue 欄位,NotebookRenderer 可利用它來建立議題的完全互動式檢視。

在此情況下,text/htmltext/x-json 檢視將由 VS Code 原生渲染,但如果沒有為該 MIME 類型註冊 NotebookRendererapplication/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

筆記本渲染器 (Notebook Renderer)

筆記本渲染器負責接收特定 MIME 類型的輸出資料,並提供該資料的渲染檢視。輸出儲存格之間共用的渲染器可以在這些儲存格之間維護全域狀態。渲染檢視的複雜度範圍從簡單的靜態 HTML 到動態且完全互動的小工具不等。在本節中,我們將探討渲染 GitHub Issue 輸出的各種技術。

您可以使用我們的 Yeoman 產生器中的範本快速入門。若要這樣做,請先安裝 Yeoman 和 VS Code 產生器:

npm install -g yo generator-code

接著,執行 yo code 並選擇 New Notebook Renderer (TypeScript)

如果您不使用此範本,請確保將 notebookRenderer 加入到您擴充功能 package.jsonkeywords 中,並在擴充功能名稱或描述中提及它的 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 變慢。此貢獻參照了一個「入口點」(entrypoint) 指令碼,該指令碼會在需要渲染任何輸出之前載入到筆記本的 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,然後將 vscode-notebook-renderer 加入到 tsconfig.jsontypes 陣列中,讓這些型別在您的程式碼中可用。

若要建立更豐富的內容,您可以手動建立 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>
  );
};

這立即引起了一些警示。首先,我們在點擊按鈕之前就載入所有議題的完整留言資料。此外,儘管我們只是想顯示更多資料,卻要求控制器支援一個完全不同的 MIME 類型。

相反地,控制器可以透過包含一個預載 (preload) 指令碼來為渲染器提供額外功能,VS Code 也會將此指令碼載入到 iframe 中。此指令碼可存取全域函式 postKernelMessageonDidReceiveKernelMessage,可用於與控制器通訊。

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 輸出。在此情況下,我們只有在該全域變數可用時才會顯示「載入留言」(Load Comments) 按鈕:

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

互動式筆記本(與擴充功能主機通訊)

假設我們想要增加在獨立編輯器中開啟輸出項目的功能。為此,渲染器需要能夠向擴充功能主機發送訊息,接著由主機啟動編輯器。

這在渲染器和控制器為兩個獨立擴充功能的情境下非常有用。

在渲染器擴充功能的 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:此渲染器不需要訊息傳遞功能。

最後兩個選項較為推薦,因為這確保了渲染器擴充功能在擴充功能主機未必可用的其他環境中的可攜性。

渲染器指令碼檔案可依照以下方式設定通訊:

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

注意

  • 若要確保您的擴充功能在訊息傳遞前已在擴充功能主機中執行,請將 onRenderer:<您的渲染器 ID> 加入到您的 activationEvents 中,並在擴充功能的 activate 函式中設定通訊。
  • 並非所有由渲染器擴充功能發送至擴充功能主機的訊息都能保證送達。使用者可能會在來自渲染器的訊息傳遞前就關閉筆記本。

支援偵錯

對於某些控制器(例如實作程式語言的控制器),允許對儲存格執行進行偵錯是很理想的。若要加入偵錯支援,筆記本核心可以實作一個偵錯轉接器 (debug adapter),既可以直接實作 偵錯轉接器協定 (DAP),也可以將協定委派並轉換為現有的筆記本偵錯工具(如 'vscode-simple-jupyter-notebook' 範例中所做的那樣)。一個更簡單的方法是使用現有的未修改偵錯擴充功能,並即時轉換適用於筆記本需求的 DAP(如 'vscode-nodebook' 所做的那樣)。

範例

  • vscode-nodebook:Node.js 筆記本,由 VS Code 內建的 JavaScript 偵錯工具提供偵錯支援,並進行了簡單的協定轉換。
  • vscode-simple-jupyter-notebook:Jupyter 筆記本,由現有的 Xeus 偵錯工具提供偵錯支援。
© . This site is unofficial and not affiliated with Microsoft.