Skip to content

Commit 79a3586

Browse files
authored
Add cell_ids for ipynb with nbformat >= 4.5 (microsoft#134835)
1 parent f391253 commit 79a3586

File tree

6 files changed

+111
-11
lines changed

6 files changed

+111
-11
lines changed

extensions/ipynb/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"engines": {
99
"vscode": "^1.57.0"
1010
},
11+
"enableProposedApi": true,
1112
"activationEvents": [
1213
"onNotebook:jupyter-notebook"
1314
],
@@ -64,10 +65,12 @@
6465
},
6566
"dependencies": {
6667
"@enonic/fnv-plus": "^1.3.0",
67-
"detect-indent": "^6.0.0"
68+
"detect-indent": "^6.0.0",
69+
"uuid": "^8.3.2"
6870
},
6971
"devDependencies": {
70-
"@jupyterlab/coreutils": "^3.1.0"
72+
"@jupyterlab/coreutils": "^3.1.0",
73+
"@types/uuid": "^8.3.1"
7174
},
7275
"repository": {
7376
"type": "git",

extensions/ipynb/src/cellIdService.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { ExtensionContext, NotebookCellsChangeEvent, NotebookDocument, notebooks, workspace, WorkspaceEdit } from 'vscode';
7+
import { v4 as uuid } from 'uuid';
8+
import { getCellMetadata } from './serializers';
9+
import { CellMetadata } from './common';
10+
import { getNotebookMetadata } from './notebookSerializer';
11+
import { nbformat } from '@jupyterlab/coreutils';
12+
13+
/**
14+
* Ensure all new cells in notebooks with nbformat >= 4.5 have an id.
15+
* Details of the spec can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#
16+
*/
17+
export function ensureAllNewCellsHaveCellIds(context: ExtensionContext) {
18+
notebooks.onDidChangeNotebookCells(onDidChangeNotebookCells, undefined, context.subscriptions);
19+
}
20+
21+
function onDidChangeNotebookCells(e: NotebookCellsChangeEvent) {
22+
const nbMetadata = getNotebookMetadata(e.document);
23+
if (!isCellIdRequired(nbMetadata)) {
24+
return;
25+
}
26+
e.changes.forEach(change => {
27+
change.items.forEach(cell => {
28+
const cellMetadata = getCellMetadata(cell);
29+
if (cellMetadata?.id) {
30+
return;
31+
}
32+
const id = generateCellId(e.document);
33+
const edit = new WorkspaceEdit();
34+
// Don't edit the metadata directly, always get a clone (prevents accidental singletons and directly editing the objects).
35+
const updatedMetadata: CellMetadata = { ...JSON.parse(JSON.stringify(cellMetadata || {})) };
36+
updatedMetadata.id = id;
37+
edit.replaceNotebookCellMetadata(cell.notebook.uri, cell.index, { ...(cell.metadata), custom: updatedMetadata });
38+
workspace.applyEdit(edit);
39+
});
40+
});
41+
}
42+
43+
/**
44+
* Cell ids are required in notebooks only in notebooks with nbformat >= 4.5
45+
*/
46+
function isCellIdRequired(metadata: Pick<Partial<nbformat.INotebookContent>, 'nbformat' | 'nbformat_minor'>) {
47+
if ((metadata.nbformat || 0) >= 5) {
48+
return true;
49+
}
50+
if ((metadata.nbformat || 0) === 4 && (metadata.nbformat_minor || 0) >= 5) {
51+
return true;
52+
}
53+
return false;
54+
}
55+
56+
function generateCellId(notebook: NotebookDocument) {
57+
while (true) {
58+
// Details of the id can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#adding-an-id-field,
59+
// & here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#updating-older-formats
60+
const id = uuid().replace(/-/g, '').substring(0, 8);
61+
let duplicate = false;
62+
for (let index = 0; index < notebook.cellCount; index++) {
63+
const cell = notebook.cellAt(index);
64+
const existingId = getCellMetadata(cell)?.id;
65+
if (!existingId) {
66+
continue;
67+
}
68+
if (existingId === id) {
69+
duplicate = true;
70+
break;
71+
}
72+
}
73+
if (!duplicate) {
74+
return id;
75+
}
76+
}
77+
}

extensions/ipynb/src/ipynbMain.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7+
import { ensureAllNewCellsHaveCellIds } from './cellIdService';
78
import { NotebookSerializer } from './notebookSerializer';
89

910
// From {nbformat.INotebookMetadata} in @jupyterlab/coreutils
@@ -27,6 +28,7 @@ type NotebookMetadata = {
2728

2829
export function activate(context: vscode.ExtensionContext) {
2930
const serializer = new NotebookSerializer(context);
31+
ensureAllNewCellsHaveCellIds(context);
3032
context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jupyter-notebook', serializer, {
3133
transientOutputs: false,
3234
transientCellMetadata: {

extensions/ipynb/src/notebookSerializer.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,7 @@ export class NotebookSerializer implements vscode.NotebookSerializer {
7878
}
7979

8080
public serializeNotebookToString(data: vscode.NotebookData): string {
81-
const notebookContent: Partial<nbformat.INotebookContent> = data.metadata?.custom || {};
82-
notebookContent.cells = notebookContent.cells || [];
83-
notebookContent.nbformat = notebookContent.nbformat || 4;
84-
notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? 2;
85-
notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 };
81+
const notebookContent = getNotebookMetadata(data);
8682

8783
notebookContent.cells = data.cells
8884
.map(cell => createJupyterCellFromNotebookCell(cell))
@@ -95,3 +91,12 @@ export class NotebookSerializer implements vscode.NotebookSerializer {
9591
return JSON.stringify(sortObjectPropertiesRecursively(notebookContent), undefined, indentAmount) + '\n';
9692
}
9793
}
94+
95+
export function getNotebookMetadata(document: vscode.NotebookDocument | vscode.NotebookData) {
96+
const notebookContent: Partial<nbformat.INotebookContent> = document.metadata?.custom || {};
97+
notebookContent.cells = notebookContent.cells || [];
98+
notebookContent.nbformat = notebookContent.nbformat || 4;
99+
notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? 2;
100+
notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 };
101+
return notebookContent;
102+
}

extensions/ipynb/src/serializers.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { nbformat } from '@jupyterlab/coreutils';
7-
import { NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode';
7+
import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode';
88
import { CellMetadata, CellOutputMetadata } from './common';
99
import { textMimeTypes } from './deserializers';
1010

@@ -53,8 +53,11 @@ export function sortObjectPropertiesRecursively(obj: any): any {
5353
return obj;
5454
}
5555

56+
export function getCellMetadata(cell: NotebookCell | NotebookCellData) {
57+
return cell.metadata?.custom as CellMetadata | undefined;
58+
}
5659
function createCodeCellFromNotebookCell(cell: NotebookCellData): nbformat.ICodeCell {
57-
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
60+
const cellMetadata = getCellMetadata(cell);
5861
const codeCell: any = {
5962
cell_type: 'code',
6063
execution_count: cell.executionSummary?.executionOrder ?? null,
@@ -69,7 +72,7 @@ function createCodeCellFromNotebookCell(cell: NotebookCellData): nbformat.ICodeC
6972
}
7073

7174
function createRawCellFromNotebookCell(cell: NotebookCellData): nbformat.IRawCell {
72-
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
75+
const cellMetadata = getCellMetadata(cell);
7376
const rawCell: any = {
7477
cell_type: 'raw',
7578
source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')),
@@ -319,7 +322,7 @@ function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) {
319322
}
320323

321324
function createMarkdownCellFromNotebookCell(cell: NotebookCellData): nbformat.IMarkdownCell {
322-
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
325+
const cellMetadata = getCellMetadata(cell);
323326
const markdownCell: any = {
324327
cell_type: 'markdown',
325328
source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')),

extensions/ipynb/yarn.lock

+10
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@
7676
dependencies:
7777
"@phosphor/algorithm" "^1.2.0"
7878

79+
"@types/uuid@^8.3.1":
80+
version "8.3.1"
81+
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
82+
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
83+
7984
ajv@^6.5.5:
8085
version "6.12.6"
8186
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -157,3 +162,8 @@ url-parse@~1.4.3:
157162
dependencies:
158163
querystringify "^2.1.1"
159164
requires-port "^1.0.0"
165+
166+
uuid@^8.3.2:
167+
version "8.3.2"
168+
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
169+
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==

0 commit comments

Comments
 (0)