本站点文档内容均翻译自code.visualstudio.com,仅供个人学习,如有差异请以官网为准。

笔记本 API

Notebook API 允许 Visual Studio Code 扩展将文件打开为笔记本,执行笔记本代码单元格,并以各种丰富和交互式格式渲染笔记本输出。您可能听说过像 Jupyter Notebook 或 Google Colab 这样的流行笔记本界面——Notebook API 允许在 Visual Studio Code 内部实现类似体验。

笔记本的组成部分

笔记本由一系列单元格及其输出组成。笔记本的单元格可以是Markdown单元格代码单元格,并在VS Code的核心中渲染。输出可以是各种格式。一些输出格式,如纯文本、JSON、图像和HTML,由VS Code核心渲染。其他格式,如特定应用的数据或交互式小应用程序,则由扩展渲染。

笔记本中的单元格由一个组件读取并写入到文件系统。笔记本序列化器,负责从文件系统读取数据并将其转换为细胞描述,以及将笔记本中的修改持久化回到文件系统。 代码细胞 可以由 笔记本控制器, 它接受单元格的内容,并从中产生零个或多个输出,格式从纯文本到格式化文档或交互式小程序不等。特定应用的输出格式和交互式小程序的输出由一个 笔记本渲染器输入:.

视觉上:

笔记本的三个组件:NotebookSerializer、NotebookController 和 NotebookRenderer 的概述,以及它们如何相互作用。在上面和后续部分中用文字描述。

序列化器

NotebookSerializer API 参考

一个笔记本序列化器负责将笔记本的序列化字节转换回笔记本数据,其中包含Markdown和代码单元格的列表。它还负责相反的转换:接受笔记本数据并把数据转换成序列化的字节以供保存。

样本:

示例

在这个例子中,我们构建了一个简化的笔记本提供者扩展,用于在Jupyter Notebook格式中查看文件.笔记本扩展名(而不是其传统的文件扩展名.ipynb)。

在 中声明了一个笔记本序列化器package.json贡献.笔记本章节如下:

{
    ...
    "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)
      });
    }

    返回 新的 TextEncoder().编码(JSON.串化(内容));
  }
}

现在尝试运行你的扩展并打开一个用Jupyter Notebook格式保存的文件。.笔记本扩展:

显示Jupyter Notebook格式文件内容的笔记本

你应该能够打开Jupyter格式的笔记本,并以纯文本和渲染的Markdown查看它们的单元格,同时编辑单元格。然而,输出不会持久化到磁盘;要保存输出,你需要将单元格的输出序列化和反序列化。笔记本数据输入:.

要运行一个单元格,您需要实现一个笔记本控制器输入:.

控制器

NotebookController API 参考

一个笔记本控制器 负责处理 代码单元 并执行代码以产生某些或没有输出。

控制器通过设置直接与笔记本序列化器和笔记本的一种类型相关联NotebookController#notebookType在控制器创建时设置属性。然后在扩展激活时将控制器推送到扩展订阅的全局注册中。

导出 函数 激活(上下文: vscode.扩展上下文) {
  上下文.订阅.推送(新的 控制器());
}

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 */

    执行.替换输出([
 vscode.笔记本单元格输出([
        vscode.笔记本单元格输出项.文本('虚拟输出文本!')
      ])
    ]);
    执行.结束(true, 日期.现在());
  }
}

如果你正在发布一个笔记本控制器- 提供扩展与它的序列化器分开,然后添加一个像这样的条目notebook内核<视图类型大写首字母拼接>关键词在其package.json例如,如果你发布了一个替代内核github-问题笔记本类型,你应该添加一个关键词笔记本内核Github问题将关键词添加到您的扩展中。 这提高了在打开此类笔记本时扩展的可发现性。<视图类型大写首字母表示>从 Visual Studio Code 内部。

样本:

输出类型

输出必须是以下三种格式之一:文本输出、错误输出或丰富输出。内核可以为单元格的单次执行提供多个输出,在这种情况下,它们将显示为一个列表。

简单的格式,如文本输出、错误输出或“简单”版本的富文本输出(HTML、Markdown、JSON 等),由 VS Code 核心渲染,而特定应用的富文本输出类型则由一个 NotebookRenderer 渲染。扩展程序可以选择自己渲染“简单”的富文本输出,例如为 Markdown 输出添加 LaTeX 支持。

上述不同输出类型的图表

文本输出

文本输出是最简单的输出格式,工作方式与您可能熟悉的许多REPL非常相似。它们仅由一个文本字段,在单元格的输出元素中以纯文本形式呈现:

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

单元格带有简单文本输出

错误输出

错误输出有助于以一致且易于理解的方式显示运行时错误。它们支持标准错误对象。

尝试 {
  /* 一些代码 */
}捕获 (错误) {
  vscode.NotebookCellOutputItem.错误(错误);
}

显示错误名称和消息的错误输出单元,以及带有洋红色文本的堆栈跟踪

丰富输出

Rich outputs 是显示单元格输出的最先进形式。它们允许提供许多不同表示形式的输出数据,通过mimetype进行键值映射。例如,如果一个单元格输出要表示一个 GitHub 问题,内核可能会生成一个具有多个属性的 rich 输出。数据领域:

  • 一个文本/HTML包含问题格式化视图的字段。
  • 一个文本/JSON包含机器可读视图的字段。
  • 一个申请/GitHub问题领域中一个笔记本渲染器可以用来创建一个完全互动的问题视图。

在这种情况下,文本/HTML文本/JSON视图将由 VS Code 原生渲染,但申请/GitHub问题视图将显示错误,如果未笔记本渲染器已注册到该mimetype。

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')
                        ]);//

单元格 rich 输出显示在 formatted HTML、一个 JSON 编辑器和一个错误消息之间切换,显示没有可用的渲染器 (application/hello-world)

默认情况下,VS Code 可以渲染以下媒体类型:

  • 应用/JavaScript
  • 文本/HTML
  • 图像/svg+xml
  • 文本/标记
  • 图像/PNG
  • 图像/jpeg
  • 文本/纯文本

VS Code将使用内置编辑器将这些mimetypes渲染为代码:

  • 文本/JSON
  • 文本/ x-JavaScript
  • 文本/ x-HTML
  • 文本/x-rust
  • ... text/x-LANGUAGE_ID 用于任何其他内置或已安装的语言。

这个笔记本使用内置编辑器显示一些 Rust 代码: 笔记本在内置的Monaco编辑器中显示Rust代码

要渲染其他 mimetype,请使用笔记本渲染器必须注册该mimetype。

笔记本渲染器

笔记本渲染器负责将特定mimetype的输出数据呈现为一种渲染视图。输出单元共享的渲染器可以在这些单元之间保持全局状态。渲染视图的复杂性可以从简单的静态HTML到动态的完全交互式的小应用程序。在本节中,我们将探讨各种渲染GitHub问题表示的输出的技术。

你可以使用我们Yeoman生成器的模板快速开始。要这样做,首先使用以下命令安装Yeoman和VS Code生成器:

npm install -g yo generator-code

然后,运行你的代码并选择新的笔记本渲染器(TypeScript)输入:.

如果你不使用这个模板,你只需确保添加笔记本渲染器关键词在你的扩展中package.json,并在扩展名称或描述中的某个地方提及其mimetype,以便用户可以找到您的渲染器。

一个简单、非交互式的渲染器

渲染器通过贡献来声明一组mimetypes。贡献者.笔记本渲染器扩展的属性package.json此渲染器将处理输入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 的其余用户界面分开,以确保它们不会意外干扰或导致 VS Code 的性能下降。贡献指的是一个“入口点”脚本,该脚本被加载到笔记本中内嵌框架在任何输出需要呈现之前。您的入口点需要是一个单一的文件,您可以自己编写,或者使用 Webpack、Rollup 或 Parcel 等打包器来创建。

当它加载时,你的入口脚本应该导出激活函数vscode 笔记本渲染器一旦 VS Code 准备好渲染你的渲染器,它将渲染你的用户界面。例如,这将把你的所有 GitHub 问题数据作为 JSON 放入单元格输出中:

导入 类型 { 激活函数 }  'vscode-notebook-renderer';

导出 const 激活激活函数 = 上下文 => ({
  渲染输出项(数据, 元素) {
    元素.innerText = JSON..stringify(数据.json());
  }
});

你可以在这里查看完整的API定义。如果你使用TypeScript,你可以安装@类型/vscode-笔记本渲染器然后添加vscode 笔记本渲染器类型数组在你的tsconfig.json以便在您的代码中使用这些类型。

为了创建更丰富的内容,您可以手动创建 DOM 元素,或者使用像 Preact 这样的框架并将其渲染到输出元素中,例如:

导入 类型 { 激活函数 }  'vscode-notebook-renderer';
导入 { h, 渲染 }  '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>
);

导出 const 激活: 激活函数 = (上下文) => ({
    渲染输出项(数据, 元素) {
        渲染(<GithubIssues issues={数据.json()} />, 元素);
    }
});

在这个输出单元格上运行此渲染器ms-vscode.github-issue-notebook/github-issue数据字段为我们提供了以下静态HTML视图:

单元格输出显示问题的渲染HTML视图

如果您有容器外部的元素或其他异步过程,您可以使用处理输出项撕毁它们。此事件将在输出被清除、单元格被删除以及现有单元格的新输出被渲染之前触发。例如:

常量 区间 = 新的 Map;

导出 const 激活激活函数 = (上下文) => ({
    渲染输出项(数据, 元素) {
        渲染(<GithubIssues issues={数据.json()} />, 元素);

        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 的不同元素中呈现。如果你使用像这样的函数文档选择器确保将其限制在您感兴趣的特定输出范围内,以避免与其他输出发生冲突。在这个例子中,我们使用元素.querySelector避免该问题。

交互式笔记本(与控制器通信)

想象一下,我们希望在点击渲染输出中的一个按钮后,能够查看问题的评论。假设控制器可以提供带有评论的问题数据ms-vscode.github-issue-notebook/github-issue-with-commentsmimetype,我们可能会尝试一次性检索所有评论并实现如下:

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

这立刻引起了一些警报。首先,即使在我们点击按钮之前,我们就开始加载所有问题的完整评论数据。此外,尽管我们只是想显示更多的数据,但我们还需要控制器支持完全不同的mimetype。

相反,控制器可以通过包含一个预加载脚本来为渲染器提供额外的功能,VS Code 会将这个脚本加载到 iframe 中。这个脚本可以访问全局函数后内核消息onDidReceiveKernelMessage可以用来与控制器通信。

图表显示控制器如何通过NotebookRendererScript与渲染器互动

例如,你可能会修改你的控制器渲染器脚本引用一个新文件,在该文件中创建一个回接到扩展主机的连接,并暴露出渲染器可以使用的全局通信脚本。

在你的控制器中:

 控制器 {
  // ...

  只读 rendererScriptId = 'my-renderer-script';

  构造函数() {
    // ...

    this._controller.rendererScripts.push(
      new vscode.NotebookRendererScript(
        vscode.Uri.file(/* 脚本的路径 */),
        rendererScriptId
      )
    );
  }
}

在你的package.json将您的脚本指定为您渲染器的依赖项:

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

在你的脚本文件中,你可以声明通信函数以与控制器进行通信:

导入 'vscode-notebook-renderer/预加载';

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 问题输出。github问题评论提供者在这种情况下,我们只会显示加载评论按钮,如果全局可用:

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收听消息:

 控制器 {
  // ...

  构造函数() {
    // ...

    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渲染器扩展中指定的值为需要消息传递可选这使得你的渲染器在两种情况下都能工作:当它有访问扩展主机的权限时,以及没有访问扩展主机的权限时。

{
  "activationEvents": ["...."],
  "contributes": {
    ...
    "notebookRenderer": [
      {
        "id": "output-editor-renderer",
        "displayName": "输出编辑器渲染器",
        "entrypoint": "./out/renderer.js",
        "mimeTypes": [...],
        "requiresMessaging": "可选"
      }
    ]
  }
}

可能的值为需要消息传递包含:

  • 总是 : 需要消息。渲染器仅在它是扩展的一部分且可以在扩展主机中运行时使用。
  • 可选渲染器在扩展主机可用时通过消息进行通信会更好,但不是安装和运行渲染器所必需的。
  • 从不 渲染器不需要消息。

最后两个选项是首选,因为这确保了渲染器扩展在其他上下文中的可移植性,而扩展主机可能不一定可用。

渲染器脚本文件可以如下设置通信:

导入 { 激活函数 } 来自 '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;
    }

    // 当用户在输出中执行某些操作(例如点击按钮)时,
    // 向扩展主机发送消息请求启动编辑器。
    document.querySelector('#openEditor').addEventListener('click', () => {
      context.postMessage({
        request: 'showEditor',
        data: '
      })
    });
  }
});

然后,你可以在扩展主机中消费该消息,如下所示:

const messageChannel = notebooks.createRendererMessaging('output-editor-renderer');
messageChannel.onDidReceiveMessage(e => {
  if (e.message.request === 'showEditor') {
    // 根据 `e.message.data` 启动输出编辑器
  }
});

注意:

  • 为了确保在消息发送之前,你的扩展在扩展主机中运行,请添加在渲染器:<你的渲染器ID>到你的激活事件并设置在你的扩展中的通信激活功能。
  • 渲染器扩展发送到扩展主机的所有消息并不保证都能送达。用户可能在渲染器消息送达之前关闭笔记本。

支持调试

对于某些控制器,例如实现编程语言的控制器,允许调试单元格的执行可能是有用的。为了添加调试支持,笔记本内核可以实现一个调试适配器,既可以直接实现调试适配器协议(DAP),也可以将协议委托并转换为现有的笔记本内核调试器(如在'vscode-simple-jupyter-notebook'示例中所做的那样)。一个更简单的方法是使用现有的未修改的调试扩展,并在运行时根据笔记本的需求转换DAP(如在'vscode-nodebook'中所做的那样)。

样本:

  • vscode-nodebook:Node.js 笔记本,由 VS Code 内置的 JavaScript 调试器提供调试支持,并进行一些简单的协议转换
  • vscode简单Jupyter笔记本:由现有的Xeus调试器提供的调试支持的Jupyter notebook