Notebook API
Notebook API 允許 Visual Studio Code 擴充功能將檔案開啟為 notebook、執行 notebook 程式碼儲存格,並以各種豐富且互動式的格式轉譯 notebook 輸出。您可能知道熱門的 notebook 介面,例如 Jupyter Notebook 或 Google Colab – Notebook API 允許在 Visual Studio Code 內獲得類似的體驗。
Notebook 的各部分
Notebook 由一連串的儲存格及其輸出組成。Notebook 的儲存格可以是Markdown 儲存格或程式碼儲存格,並在 VS Code 的核心中轉譯。輸出可以是各種格式。某些輸出格式(例如純文字、JSON、影像和 HTML)由 VS Code 核心轉譯。其他輸出格式(例如應用程式特定的資料或互動式小程式)則由擴充功能轉譯。
Notebook 中的儲存格由 NotebookSerializer
讀取和寫入檔案系統,NotebookSerializer
處理從檔案系統讀取資料並將其轉換為儲存格描述,以及將對 notebook 的修改持久儲存回檔案系統。Notebook 的程式碼儲存格可以由 NotebookController
執行,NotebookController
接收儲存格的內容,並從中產生零或多個輸出,格式範圍從純文字到格式化文件或互動式小程式不等。應用程式特定的輸出格式和互動式小程式輸出由 NotebookRenderer
轉譯。
視覺化呈現
序列化程式
NotebookSerializer
負責取得 notebook 的序列化位元組,並將這些位元組反序列化為 NotebookData
,其中包含 Markdown 和程式碼儲存格的清單。它也負責相反的轉換:取得 NotebookData
並將資料轉換為要儲存的序列化位元組。
範例
- JSON Notebook 序列化程式:簡單的 notebook 範例,它接受 JSON 輸入,並在自訂
NotebookRenderer
中輸出美化的 JSON。 - Markdown 序列化程式:將 Markdown 檔案開啟並編輯為 notebook。
範例
在此範例中,我們建置一個簡化的 notebook 提供者擴充功能,用於檢視 Jupyter Notebook 格式的檔案,副檔名為 .notebook
(而不是其傳統的檔案副檔名 .ipynb
)。
Notebook 序列化程式在 package.json
中的 contributes.notebooks
區段中宣告如下
{
...
"contributes": {
...
"notebooks": [
{
"type": "my-notebook",
"displayName": "My Notebook",
"selector": [
{
"filenamePattern": "*.notebook"
}
]
}
]
}
}
然後,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 格式檔案
您應該能夠開啟 Jupyter 格式的 notebook,並將其儲存格檢視為純文字和轉譯的 Markdown,以及編輯儲存格。但是,輸出將不會持久儲存到磁碟;若要儲存輸出,您也需要從 NotebookData
序列化和反序列化儲存格的輸出。
若要執行儲存格,您需要實作 NotebookController
。
控制器
NotebookController
負責取得程式碼儲存格並執行程式碼以產生一些或不產生輸出。
控制器透過在建立控制器時設定 NotebookController#notebookType
屬性,直接與 notebook 序列化程式和 notebook 類型相關聯。然後,透過在擴充功能啟動時將控制器推送至擴充功能訂閱,在全域註冊控制器。
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.json
中的 keywords
中新增類似 notebookKernel<ViewTypeUpperCamelCased>
的項目。例如,如果您發佈了 github-issues
notebook 類型的替代核心,您應該將關鍵字 notebookKernelGithubIssues
關鍵字新增至您的擴充功能。這會在從 Visual Studio Code 內開啟 <ViewTypeUpperCamelCased>
類型的 notebook 時,改善擴充功能的探索性。
範例
- GitHub Issues Notebook:用於執行 GitHub Issues 查詢的控制器
- REST Book:用於執行 REST 查詢的控制器。
- Regexper notebooks:用於視覺化呈現規則運算式的控制器。
輸出類型
輸出必須是三種格式之一:文字輸出、錯誤輸出或豐富輸出。核心可以為單次儲存格執行提供多個輸出,在這種情況下,它們將顯示為清單。
簡單格式(例如文字輸出、錯誤輸出或「簡單」變體的豐富輸出(HTML、Markdown、JSON 等))由 VS Code 核心轉譯,而應用程式特定的豐富輸出類型則由 NotebookRenderer 轉譯。擴充功能可以選擇性地自行轉譯「簡單」豐富輸出,例如為 Markdown 輸出新增 LaTeX 支援。
文字輸出
文字輸出是最簡單的輸出格式,其運作方式與您可能熟悉的許多 REPL 類似。它們僅包含一個 text
欄位,該欄位在儲存格的輸出元素中轉譯為純文字
vscode.NotebookCellOutputItem.text('This is the output...');
錯誤輸出
錯誤輸出有助於以一致且易於理解的方式顯示執行階段錯誤。它們支援標準 Error
物件。
try {
/* Some code */
} catch (error) {
vscode.NotebookCellOutputItem.error(error);
}
豐富輸出
豐富輸出是顯示儲存格輸出的最進階形式。它們允許提供輸出資料的許多不同表示法,並以 mimetype 作為索引鍵。例如,如果儲存格輸出是要表示 GitHub Issue,則核心可能會產生豐富輸出,其 data
欄位上具有多個屬性
- 包含 Issue 格式化檢視的
text/html
欄位。 - 包含機器可讀檢視的
text/x-json
欄位。 application/github-issue
欄位,NotebookRenderer
可以使用該欄位來建立 Issue 的完整互動式檢視。
在這種情況下,text/html
和 text/x-json
檢視將由 VS Code 原生轉譯,但如果沒有為該 mimetype 註冊 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'),
])]);
依預設,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。
此 notebook 正在使用內建編輯器來顯示一些 Rust 程式碼:
若要轉譯替代的 mimetype,必須為該 mimetype 註冊 NotebookRenderer
。
Notebook 轉譯器
Notebook 轉譯器負責取得特定 mimetype 的輸出資料,並提供該資料的轉譯檢視。輸出儲存格共用的轉譯器可以在這些儲存格之間維護全域狀態。轉譯檢視的複雜性範圍可以從簡單的靜態 HTML 到動態的完整互動式小程式。在本節中,我們將探索用於轉譯代表 GitHub Issue 的輸出的各種技術。
您可以使用來自我們 Yeoman 產生器的樣板快速開始。若要這麼做,請先使用下列命令安裝 Yeoman 和 VS Code Generators
npm install -g yo generator-code
然後,執行 yo code
並選擇 New Notebook Renderer (TypeScript)
。
如果您不使用此範本,您只需要確保將 notebookRenderer
新增至擴充功能 package.json
中的 keywords
,並在擴充功能名稱或描述中的某處提及其 mimetype,以便使用者可以找到您的轉譯器。
簡單、非互動式轉譯器
轉譯器是透過貢獻擴充功能 package.json
的 contributes.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 速度變慢。此貢獻指的是「進入點」指令碼,該指令碼會在需要轉譯任何輸出之前載入 notebook 的 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 檢視
如果您有容器外部的元素或其他非同步處理序,您可以使用 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);
}
});
請務必記住,notebook 的所有輸出都轉譯在相同 iframe 中的不同元素中。如果您使用 document.querySelector
等函式,請務必將其範圍限定為您感興趣的特定輸出,以避免與其他輸出衝突。在此範例中,我們使用 element.querySelector
來避免該問題。
互動式 Notebook (與控制器通訊)
假設我們想要在按一下轉譯輸出中的按鈕後,新增檢視 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 中載入該指令碼。此指令碼具有全域函式 postKernelMessage
和 onDidReceiveKernelMessage
的存取權,可用於與控制器通訊。
例如,您可以修改控制器 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);
}
});
}
};
然後,您可以在轉譯器中使用它。您想要確保檢查控制器轉譯指令碼公開的全域是否可用,因為其他開發人員可能會在其他 notebook 和未實作 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
);
}
});
}
}
互動式 Notebook (與擴充功能主機通訊)
假設我們想要新增在個別編輯器中開啟輸出項目的功能。為了實現此目的,轉譯器需要能夠將訊息傳送至擴充功能主機,然後擴充功能主機會啟動編輯器。
在轉譯器和控制器是兩個不同的擴充功能的情況下,這會很有用。
在轉譯器擴充功能的 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
函式中設定通訊。 - 並非轉譯器擴充功能傳送至擴充功能主機的所有訊息都保證會傳遞。使用者可能會在傳遞轉譯器的訊息之前關閉 notebook。
支援偵錯
對於某些控制器(例如實作程式設計語言的控制器),允許偵錯儲存格的執行可能很理想。若要新增偵錯支援,notebook 核心可以實作偵錯配接器,方法是直接實作偵錯配接器協定 (DAP),或委派協定並將其轉換為現有的 notebook 偵錯工具(如 'vscode-simple-jupyter-notebook' 範例中所做的那樣)。更簡單的方法是使用現有的未修改偵錯擴充功能,並即時轉換 notebook 需求的 DAP(如 'vscode-nodebook' 中所做的那樣)。
範例
- vscode-nodebook:Node.js notebook,其偵錯支援由 VS Code 的內建 JavaScript 偵錯工具和一些簡單的協定轉換提供
- vscode-simple-jupyter-notebook:Jupyter notebook,其偵錯支援由現有的 Xeus 偵錯工具提供