參加你附近的 ,瞭解 VS Code 中的 AI 輔助開發。

筆記本 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.

序列化器

NotebookSerializer API 參考

NotebookSerializer 負責接收筆記本的序列化位元組並將其反序列化為 NotebookData,其中包含 Markdown 和程式碼單元格列表。它也負責相反的轉換:接收 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

控制器

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> 型別的筆記本時擴充套件的可發現性。

示例

輸出型別

輸出必須是以下三種格式之一:文字輸出、錯誤輸出或富文字輸出。核心可以為單元格的單次執行提供多個輸出,在這種情況下,它們將顯示為列表。

簡單格式,如文字輸出、錯誤輸出或富文字輸出的“簡單”變體(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

富文字輸出

富文字輸出是顯示單元格輸出最先進的形式。它們允許提供許多不同的輸出資料表示,以 mimetype 為鍵。例如,如果單元格輸出要表示 GitHub Issue,則核心可能會在其 data 欄位上生成具有多個屬性的富文字輸出。

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

在這種情況下,text/htmltext/x-json 檢視將由 VS Code 本機渲染,但如果未註冊 NotebookRenderer 到該 mimetype,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 可以渲染以下 mimetype:

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

VS Code 將在內建編輯器中將這些 mimetype 渲染為程式碼:

  • text/x-json
  • text/x-javascript
  • text/x-html
  • text/x-rust
  • ... 任何其他內建或已安裝語言的 text/x-LANGUAGE_ID。

此筆記本正在使用內建編輯器顯示一些 Rust 程式碼:在內建 Monaco 編輯器中顯示 Rust 程式碼的筆記本

要渲染替代 mimetype,必須為該 mimetype 註冊 NotebookRenderer

筆記本渲染器

筆記本渲染器負責接收特定 mimetype 的輸出資料並提供該資料的渲染檢視。由輸出單元格共享的渲染器可以在這些單元格之間保持全域性狀態。渲染檢視的複雜性可以從簡單的靜態 HTML 到動態的完全互動式小程式。在本節中,我們將探討渲染表示 GitHub Issue 的輸出的各種技術。

你可以使用 Yeoman 生成器中的樣板檔案快速入門。為此,首先安裝 Yeoman 和 VS Code 生成器,使用:

npm install -g yo generator-code

然後,執行 yo code 並選擇 New Notebook Renderer (TypeScript)

如果你不使用此模板,你只需確保將 notebookRenderer 新增到你的擴充套件的 package.json 中的 keywords 中,並在副檔名稱或描述中某處提及其 mimetype,以便使用者可以找到你的渲染器。

一個簡單的非互動式渲染器

渲染器透過向擴充套件的 package.jsoncontributes.notebookRenderer 屬性貢獻來宣告一組 mimetype。此渲染器將使用 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 等打包工具建立。

載入後,你的入口點指令碼應從 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.json 中的 types 陣列中,以使這些型別在你的程式碼中可用。

為了建立更豐富的內容,你可以手動建立 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 mimetype 提供帶有評論的 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>
  );
};

這立即引起了一些問題。首先,即使在我們點選按鈕之前,我們也正在載入所有 issue 的完整評論資料。此外,即使我們只想顯示更多資料,我們也需要控制器支援一個全新的 mimetype。

相反,控制器可以透過包含一個預載入指令碼來向渲染器提供附加功能,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);
      }
    });
  }
};

然後你可以在渲染器中使用它。你需要確保檢查控制器渲染指令碼公開的全域性是否可用,因為其他開發人員可能會在其他筆記本和控制器中建立 GitHub issue 輸出,這些筆記本和控制器未實現 githubIssueCommentProvider。在這種情況下,我們只在全域性可用時顯示載入評論按鈕:

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:<your renderer id> 新增到你的 activationEvents 中,並在你的擴充套件的 activate 函式中設定通訊。
  • 並非所有從渲染器擴充套件傳送到擴充套件主機的訊息都保證會送達。使用者可能會在渲染器傳送訊息之前關閉筆記本。

支援除錯

對於某些控制器(例如實現程式語言的控制器),允許除錯單元格的執行可能很受歡迎。為了新增除錯支援,筆記本核心可以實現一個除錯介面卡,無論是透過直接實現除錯介面卡協議 (DAP),還是透過委託和轉換協議到現有筆記本偵錯程式(如 'vscode-simple-jupyter-notebook' 示例中所做)。一種更簡單的方法是使用現有未修改的除錯擴充套件並即時轉換 DAP 以滿足筆記本需求(如 'vscode-nodebook' 中所做)。

示例

  • vscode-nodebook:Node.js 筆記本,透過 VS Code 內建的 JavaScript 偵錯程式和一些簡單的協議轉換提供除錯支援。
  • vscode-simple-jupyter-notebook:Jupyter 筆記本,透過現有 Xeus 偵錯程式提供除錯支援。