Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/edit/issue-6059/serializers.ts
13405 views
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 type * as nbformat from '@jupyterlab/nbformat';
7
import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode';
8
import { CellOutputMetadata, useCustomPropertyInMetadata, type CellMetadata } from './common';
9
import { textMimeTypes } from './deserializers';
10
11
const textDecoder = new TextDecoder();
12
13
enum CellOutputMimeTypes {
14
error = 'application/vnd.code.notebook.error',
15
stderr = 'application/vnd.code.notebook.stderr',
16
stdout = 'application/vnd.code.notebook.stdout'
17
}
18
19
export function createJupyterCellFromNotebookCell(
20
vscCell: NotebookCellData,
21
preferredLanguage: string | undefined
22
): nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell {
23
let cell: nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell;
24
if (vscCell.kind === NotebookCellKind.Markup) {
25
cell = createMarkdownCellFromNotebookCell(vscCell);
26
} else if (vscCell.languageId === 'raw') {
27
cell = createRawCellFromNotebookCell(vscCell);
28
} else {
29
cell = createCodeCellFromNotebookCell(vscCell, preferredLanguage);
30
}
31
return cell;
32
}
33
34
35
/**
36
* Sort the JSON to minimize unnecessary SCM changes.
37
* Jupyter notbeooks/labs sorts the JSON keys in alphabetical order.
38
* https://github.com/microsoft/vscode-python/issues/13155
39
*/
40
export function sortObjectPropertiesRecursively(obj: any): any {
41
if (Array.isArray(obj)) {
42
return obj.map(sortObjectPropertiesRecursively);
43
}
44
if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) {
45
return (
46
Object.keys(obj)
47
.sort()
48
.reduce<Record<string, any>>((sortedObj, prop) => {
49
sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]);
50
return sortedObj;
51
}, {}) as any
52
);
53
}
54
return obj;
55
}
56
57
export function getCellMetadata(options: { cell: NotebookCell | NotebookCellData } | { metadata?: { [key: string]: any } }): CellMetadata {
58
if ('cell' in options) {
59
const cell = options.cell;
60
if (useCustomPropertyInMetadata()) {
61
const metadata: CellMetadata = {
62
// it contains the cell id, and the cell metadata, along with other nb cell metadata
63
...(cell.metadata?.custom ?? {})
64
};
65
// promote the cell attachments to the top level
66
const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments;
67
if (attachments) {
68
metadata.attachments = attachments;
69
}
70
return metadata;
71
}
72
const metadata = {
73
// it contains the cell id, and the cell metadata, along with other nb cell metadata
74
...(cell.metadata ?? {})
75
};
76
77
return metadata;
78
} else {
79
const cell = options;
80
if (useCustomPropertyInMetadata()) {
81
const metadata: CellMetadata = {
82
// it contains the cell id, and the cell metadata, along with other nb cell metadata
83
...(cell.metadata?.custom ?? {})
84
};
85
// promote the cell attachments to the top level
86
const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments;
87
if (attachments) {
88
metadata.attachments = attachments;
89
}
90
return metadata;
91
}
92
const metadata = {
93
// it contains the cell id, and the cell metadata, along with other nb cell metadata
94
...(cell.metadata ?? {})
95
};
96
97
return metadata;
98
}
99
}
100
101
export function getVSCodeCellLanguageId(metadata: CellMetadata): string | undefined {
102
return metadata.metadata?.vscode?.languageId;
103
}
104
export function setVSCodeCellLanguageId(metadata: CellMetadata, languageId: string) {
105
metadata.metadata = metadata.metadata || {};
106
metadata.metadata.vscode = { languageId };
107
}
108
export function removeVSCodeCellLanguageId(metadata: CellMetadata) {
109
if (metadata.metadata?.vscode) {
110
delete metadata.metadata.vscode;
111
}
112
}
113
114
function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguage: string | undefined): nbformat.ICodeCell {
115
const cellMetadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata({ cell })));
116
cellMetadata.metadata = cellMetadata.metadata || {}; // This cannot be empty.
117
if (cell.languageId !== preferredLanguage) {
118
setVSCodeCellLanguageId(cellMetadata, cell.languageId);
119
} else {
120
// cell current language is the same as the preferred cell language in the document, flush the vscode custom language id metadata
121
removeVSCodeCellLanguageId(cellMetadata);
122
}
123
124
const codeCell: any = {
125
cell_type: 'code',
126
// Possible the metadata was edited as part of diff view
127
// In diff view we display execution_count as part of metadata, hence when execution count changes in metadata,
128
// We need to change that here as well, i.e. give preference to any execution_count value in metadata.
129
execution_count: cellMetadata.execution_count ?? cell.executionSummary?.executionOrder ?? null,
130
source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')),
131
outputs: (cell.outputs || []).map(translateCellDisplayOutput),
132
metadata: cellMetadata.metadata
133
};
134
if (cellMetadata?.id) {
135
codeCell.id = cellMetadata.id;
136
}
137
return codeCell;
138
}
139
140
function createRawCellFromNotebookCell(cell: NotebookCellData): nbformat.IRawCell {
141
const cellMetadata = getCellMetadata({ cell });
142
const rawCell: any = {
143
cell_type: 'raw',
144
source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')),
145
metadata: cellMetadata?.metadata || {} // This cannot be empty.
146
};
147
if (cellMetadata?.attachments) {
148
rawCell.attachments = cellMetadata.attachments;
149
}
150
if (cellMetadata?.id) {
151
rawCell.id = cellMetadata.id;
152
}
153
return rawCell;
154
}
155
156
function splitMultilineString(source: nbformat.MultilineString): string[] {
157
if (Array.isArray(source)) {
158
return source as string[];
159
}
160
const str = source.toString();
161
if (str.length > 0) {
162
// Each line should be a separate entry, but end with a \n if not last entry
163
const arr = str.split('\n');
164
return arr
165
.map((s, i) => {
166
if (i < arr.length - 1) {
167
return `${s}\n`;
168
}
169
return s;
170
})
171
.filter(s => s.length > 0); // Skip last one if empty (it's the only one that could be length 0)
172
}
173
return [];
174
}
175
176
function translateCellDisplayOutput(output: NotebookCellOutput): JupyterOutput {
177
const customMetadata = output.metadata as CellOutputMetadata | undefined;
178
let result: JupyterOutput;
179
// Possible some other extension added some output (do best effort to translate & save in ipynb).
180
// In which case metadata might not contain `outputType`.
181
const outputType = customMetadata?.outputType as nbformat.OutputType;
182
switch (outputType) {
183
case 'error': {
184
result = translateCellErrorOutput(output);
185
break;
186
}
187
case 'stream': {
188
result = convertStreamOutput(output);
189
break;
190
}
191
case 'display_data': {
192
result = {
193
output_type: 'display_data',
194
data: output.items.reduce((prev: any, curr) => {
195
prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);
196
return prev;
197
}, {}),
198
metadata: customMetadata?.metadata || {} // This can never be undefined.
199
};
200
break;
201
}
202
case 'execute_result': {
203
result = {
204
output_type: 'execute_result',
205
data: output.items.reduce((prev: any, curr) => {
206
prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);
207
return prev;
208
}, {}),
209
metadata: customMetadata?.metadata || {}, // This can never be undefined.
210
execution_count:
211
typeof customMetadata?.executionCount === 'number' ? customMetadata?.executionCount : null // This can never be undefined, only a number or `null`.
212
};
213
break;
214
}
215
case 'update_display_data': {
216
result = {
217
output_type: 'update_display_data',
218
data: output.items.reduce((prev: any, curr) => {
219
prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);
220
return prev;
221
}, {}),
222
metadata: customMetadata?.metadata || {} // This can never be undefined.
223
};
224
break;
225
}
226
default: {
227
const isError =
228
output.items.length === 1 && output.items.every((item) => item.mime === CellOutputMimeTypes.error);
229
const isStream = output.items.every(
230
(item) => item.mime === CellOutputMimeTypes.stderr || item.mime === CellOutputMimeTypes.stdout
231
);
232
233
if (isError) {
234
return translateCellErrorOutput(output);
235
}
236
237
// In the case of .NET & other kernels, we need to ensure we save ipynb correctly.
238
// Hence if we have stream output, save the output as Jupyter `stream` else `display_data`
239
// Unless we already know its an unknown output type.
240
const outputType: nbformat.OutputType =
241
<nbformat.OutputType>customMetadata?.outputType || (isStream ? 'stream' : 'display_data');
242
let unknownOutput: nbformat.IUnrecognizedOutput | nbformat.IDisplayData | nbformat.IStream;
243
if (outputType === 'stream') {
244
// If saving as `stream` ensure the mandatory properties are set.
245
unknownOutput = convertStreamOutput(output);
246
} else if (outputType === 'display_data') {
247
// If saving as `display_data` ensure the mandatory properties are set.
248
const displayData: nbformat.IDisplayData = {
249
data: {},
250
metadata: {},
251
output_type: 'display_data'
252
};
253
unknownOutput = displayData;
254
} else {
255
unknownOutput = {
256
output_type: outputType
257
};
258
}
259
if (customMetadata?.metadata) {
260
unknownOutput.metadata = customMetadata.metadata;
261
}
262
if (output.items.length > 0) {
263
unknownOutput.data = output.items.reduce((prev: any, curr) => {
264
prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);
265
return prev;
266
}, {});
267
}
268
result = unknownOutput;
269
break;
270
}
271
}
272
273
// Account for transient data as well
274
// `transient.display_id` is used to update cell output in other cells, at least thats one use case we know of.
275
if (result && customMetadata && customMetadata.transient) {
276
result.transient = customMetadata.transient;
277
}
278
return result;
279
}
280
281
function translateCellErrorOutput(output: NotebookCellOutput): nbformat.IError {
282
// it should have at least one output item
283
const firstItem = output.items[0];
284
// Bug in VS Code.
285
if (!firstItem.data) {
286
return {
287
output_type: 'error',
288
ename: '',
289
evalue: '',
290
traceback: []
291
};
292
}
293
const originalError: undefined | nbformat.IError = output.metadata?.originalError;
294
const value: Error = JSON.parse(textDecoder.decode(firstItem.data));
295
return {
296
output_type: 'error',
297
ename: value.name,
298
evalue: value.message,
299
// VS Code needs an `Error` object which requires a `stack` property as a string.
300
// Its possible the format could change when converting from `traceback` to `string` and back again to `string`
301
// When .NET stores errors in output (with their .NET kernel),
302
// stack is empty, hence store the message instead of stack (so that somethign gets displayed in ipynb).
303
traceback: originalError?.traceback || splitMultilineString(value.stack || value.message || '')
304
};
305
}
306
307
308
function getOutputStreamType(output: NotebookCellOutput): string | undefined {
309
if (output.items.length > 0) {
310
return output.items[0].mime === CellOutputMimeTypes.stderr ? 'stderr' : 'stdout';
311
}
312
313
return;
314
}
315
316
type JupyterOutput =
317
| nbformat.IUnrecognizedOutput
318
| nbformat.IExecuteResult
319
| nbformat.IDisplayData
320
| nbformat.IStream
321
| nbformat.IError;
322
323
function convertStreamOutput(output: NotebookCellOutput): JupyterOutput {
324
const outputs: string[] = [];
325
output.items
326
.filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout)
327
.map((opit) => textDecoder.decode(opit.data))
328
.forEach(value => {
329
// Ensure each line is a separate entry in an array (ending with \n).
330
const lines = value.split('\n');
331
// If the last item in `outputs` is not empty and the first item in `lines` is not empty, then concate them.
332
// As they are part of the same line.
333
if (outputs.length && lines.length && lines[0].length > 0) {
334
outputs[outputs.length - 1] = `${outputs[outputs.length - 1]}${lines.shift()!}`;
335
}
336
for (const line of lines) {
337
outputs.push(line);
338
}
339
});
340
341
for (let index = 0; index < (outputs.length - 1); index++) {
342
outputs[index] = `${outputs[index]}\n`;
343
}
344
345
// Skip last one if empty (it's the only one that could be length 0)
346
if (outputs.length && outputs[outputs.length - 1].length === 0) {
347
outputs.pop();
348
}
349
350
const streamType = getOutputStreamType(output) || 'stdout';
351
352
return {
353
output_type: 'stream',
354
name: streamType,
355
text: outputs
356
};
357
}
358
359
function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) {
360
if (!value) {
361
return '';
362
}
363
try {
364
if (mime === CellOutputMimeTypes.error) {
365
const stringValue = textDecoder.decode(value);
366
return JSON.parse(stringValue);
367
} else if (mime.startsWith('text/') || textMimeTypes.includes(mime)) {
368
const stringValue = textDecoder.decode(value);
369
return splitMultilineString(stringValue);
370
} else if (mime.startsWith('image/') && mime !== 'image/svg+xml') {
371
// Images in Jupyter are stored in base64 encoded format.
372
// VS Code expects bytes when rendering images.
373
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
374
return Buffer.from(value).toString('base64');
375
} else {
376
return btoa(value.reduce((s: string, b: number) => s + String.fromCharCode(b), ''));
377
}
378
} else if (mime.toLowerCase().includes('json')) {
379
const stringValue = textDecoder.decode(value);
380
return stringValue.length > 0 ? JSON.parse(stringValue) : stringValue;
381
} else if (mime === 'image/svg+xml') {
382
return splitMultilineString(textDecoder.decode(value));
383
} else {
384
return textDecoder.decode(value);
385
}
386
} catch (ex) {
387
return '';
388
}
389
}
390
391
export function createMarkdownCellFromNotebookCell(cell: NotebookCellData): nbformat.IMarkdownCell {
392
const cellMetadata = getCellMetadata({ cell });
393
const markdownCell: any = {
394
cell_type: 'markdown',
395
source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')),
396
metadata: cellMetadata?.metadata || {} // This cannot be empty.
397
};
398
if (cellMetadata?.attachments) {
399
markdownCell.attachments = cellMetadata.attachments;
400
}
401
if (cellMetadata?.id) {
402
markdownCell.id = cellMetadata.id;
403
}
404
return markdownCell;
405
}
406
407
export function pruneCell(cell: nbformat.ICell): nbformat.ICell {
408
// Source is usually a single string on input. Convert back to an array
409
const result = {
410
...cell,
411
source: splitMultilineString(cell.source)
412
} as nbformat.ICell;
413
414
// Remove outputs and execution_count from non code cells
415
if (result.cell_type !== 'code') {
416
delete (<any>result).outputs;
417
delete (<any>result).execution_count;
418
} else {
419
// Clean outputs from code cells
420
result.outputs = result.outputs ? (result.outputs as nbformat.IOutput[]).map(fixupOutput) : [];
421
}
422
423
return result;
424
}
425
const dummyStreamObj: nbformat.IStream = {
426
output_type: 'stream',
427
name: 'stdout',
428
text: ''
429
};
430
const dummyErrorObj: nbformat.IError = {
431
output_type: 'error',
432
ename: '',
433
evalue: '',
434
traceback: ['']
435
};
436
const dummyDisplayObj: nbformat.IDisplayData = {
437
output_type: 'display_data',
438
data: {},
439
metadata: {}
440
};
441
const dummyExecuteResultObj: nbformat.IExecuteResult = {
442
output_type: 'execute_result',
443
name: '',
444
execution_count: 0,
445
data: {},
446
metadata: {}
447
};
448
const AllowedCellOutputKeys = {
449
['stream']: new Set(Object.keys(dummyStreamObj)),
450
['error']: new Set(Object.keys(dummyErrorObj)),
451
['display_data']: new Set(Object.keys(dummyDisplayObj)),
452
['execute_result']: new Set(Object.keys(dummyExecuteResultObj))
453
};
454
455
function fixupOutput(output: nbformat.IOutput): nbformat.IOutput {
456
let allowedKeys: Set<string>;
457
switch (output.output_type) {
458
case 'stream':
459
case 'error':
460
case 'execute_result':
461
case 'display_data':
462
allowedKeys = AllowedCellOutputKeys[output.output_type];
463
break;
464
default:
465
return output;
466
}
467
const result = { ...output };
468
for (const k of Object.keys(output)) {
469
if (!allowedKeys.has(k)) {
470
delete result[k];
471
}
472
}
473
return result;
474
}
475
476