Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/notebook/test/node/alternativeContent.spec.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 { EOL } from 'os';
7
import { describe, expect, test } from 'vitest';
8
import type { NotebookDocument } from 'vscode';
9
import { DiffServiceImpl } from '../../../../platform/diff/node/diffServiceImpl';
10
import { ILogger, ILogService } from '../../../../platform/log/common/logService';
11
import { IAlternativeNotebookContentService } from '../../common/alternativeContent';
12
13
import { AlternativeNotebookContentEditGenerator, textToAsyncIterableLines } from '../../common/alternativeContentEditGenerator';
14
15
import { BaseAlternativeNotebookContentProvider } from '../../common/alternativeContentProvider';
16
17
import { AlternativeJsonNotebookContentProvider } from '../../common/alternativeContentProvider.json';
18
19
import { AlternativeTextNotebookContentProvider } from '../../common/alternativeContentProvider.text';
20
21
import { AlternativeXmlNotebookContentProvider } from '../../common/alternativeContentProvider.xml';
22
23
import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService';
24
import { SimulationWorkspace } from '../../../../platform/test/node/simulationWorkspace';
25
import { ExtHostNotebookDocumentData } from '../../../../util/common/test/shims/notebookDocument';
26
import { AsyncIterableObject } from '../../../../util/vs/base/common/async';
27
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
28
import { ResourceMap } from '../../../../util/vs/base/common/map';
29
import * as path from '../../../../util/vs/base/common/path';
30
import { NotebookCellData, NotebookCellKind, NotebookData, NotebookEdit, NotebookRange, Position, Range, TextEdit, Uri } from '../../../../vscodeTypes';
31
import { LineOfText, notebookCellToCellData, summarize } from '../../common/helpers';
32
import { fixture, loadFile, loadNotebook } from './utils';
33
34
describe('Alternative Content for Notebooks', () => {
35
[
36
new AlternativeXmlNotebookContentProvider(),
37
new AlternativeTextNotebookContentProvider(),
38
new AlternativeJsonNotebookContentProvider()
39
].forEach((provider) => {
40
const mockLogger: ILogger = {
41
error: () => { /* no-op */ },
42
warn: () => { /* no-op */ },
43
info: () => { /* no-op */ },
44
debug: () => { /* no-op */ },
45
trace: () => { /* no-op */ },
46
show: () => { /* no-op */ },
47
createSubLogger(): ILogger { return mockLogger; },
48
withExtraTarget(): ILogger { return mockLogger; }
49
};
50
function getEditGenerator(provider: BaseAlternativeNotebookContentProvider) {
51
return new AlternativeNotebookContentEditGenerator(new class implements IAlternativeNotebookContentService {
52
declare readonly _serviceBrand: undefined;
53
create(_format: any) {
54
return provider;
55
}
56
getFormat() {
57
return provider.kind;
58
}
59
}(), new DiffServiceImpl(), new class implements ILogService {
60
_serviceBrand: undefined;
61
internal = mockLogger;
62
logger = mockLogger;
63
trace = mockLogger.trace;
64
debug = mockLogger.debug;
65
info = mockLogger.info;
66
warn = mockLogger.warn;
67
error = mockLogger.error;
68
show(preserveFocus?: boolean): void {
69
//
70
}
71
createSubLogger(): ILogger {
72
return this;
73
}
74
withExtraTarget(): ILogger {
75
return this;
76
}
77
}(), new NullTelemetryService());
78
}
79
[true, false].forEach(applyEditsImmediately => {
80
describe(`${provider.kind} Content Parser`, () => {
81
test(`Generate a single Notebook Edit (insert md cell)`, async () => {
82
if (provider.kind !== 'xml') {
83
return;
84
}
85
const alternativeFile = await loadFile({ filePath: `${fixture('insert.1.ipynb')}.xml` });
86
const file = await loadFile({ filePath: fixture('insert.ipynb') });
87
const notebook = await loadNotebook(file);
88
89
let alternativeContents = alternativeFile.contents;
90
const cellSummary = notebook.getCells().map(summarize);
91
cellSummary.forEach(cell => {
92
const toReplace = provider.kind === 'xml' ? `<CELL_ID_${cell.index}>` : `CELL_ID_${cell.index}`;
93
alternativeContents = alternativeContents.replace(toReplace, cell.id);
94
});
95
const alternativeContentLines = AsyncIterableObject.fromArray(alternativeContents.split(/\r?\n/)).map(l => new LineOfText(l));
96
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, alternativeContentLines, undefined, CancellationToken.None);
97
const notebookEdits: NotebookEdit[] = [];
98
for await (const edit of edits) {
99
if (!Array.isArray(edit)) {
100
notebookEdits.push(edit);
101
}
102
}
103
expect(notebookEdits.length).toBe(1);
104
expect(notebookEdits[0].newCells.length).toBe(1);
105
expect(notebookEdits[0].newCells[0].kind).toBe(NotebookCellKind.Markup);
106
expect(notebookEdits[0].newCells[0].value.split(/\r?\n/g)).toEqual([`# DataFrame Details`, ``, `This DataFrame contains two columns: 'Name' and 'Gender'. The 'Name' column has three entries: 'Hello', 'World', and 'Baz'. The 'Gender' column has three entries: 'F', 'M', and 'F'.`]);
107
expect(notebookEdits[0].range.start).toBe(1);
108
expect(notebookEdits[0].range.end).toBe(1);
109
110
// Generate edits as though this is a branch new notebook.
111
const newEdits = await getEditGenerator(provider).generateNotebookEdits(Uri.file('newNotebook.ipynb'), alternativeContentLines, undefined, CancellationToken.None);
112
notebookEdits.length = 0;
113
for await (const edit of newEdits) {
114
if (!Array.isArray(edit)) {
115
notebookEdits.push(edit);
116
}
117
}
118
expect(notebookEdits.length).toBe(notebook.cellCount + 1);
119
});
120
test(`Generate a single Notebook Edit (insert Python cell)`, async () => {
121
// This test focuses on generating as Notebook edit where LLM hallucinates
122
// & generates Python content instead of a structured Jupytext content.
123
// In such cases the python code should be inserted as is in a single cell.
124
// Previously nothing would be inserted.
125
if (provider.kind !== 'text') {
126
return;
127
}
128
const alternativeContents = 'import math\n\ndef circle_area(radius):\n return math.pi * radius**2\n';
129
const alternativeContentLines = AsyncIterableObject.fromArray(alternativeContents.split(/\r?\n/)).map(l => new LineOfText(l));
130
const edits = await getEditGenerator(provider).generateNotebookEdits(Uri.file('newFile.ipynb'), alternativeContentLines, undefined, CancellationToken.None);
131
const notebookEdits: NotebookEdit[] = [];
132
for await (const edit of edits) {
133
if (!Array.isArray(edit)) {
134
notebookEdits.push(edit);
135
}
136
}
137
expect(notebookEdits.length).toBe(1);
138
expect(notebookEdits[0].newCells.length).toBe(1);
139
expect(notebookEdits[0].newCells[0].kind).toBe(NotebookCellKind.Code);
140
expect(notebookEdits[0].newCells[0].value.split(/\r?\n/g)).toEqual(alternativeContents.split(/\r?\n/));
141
});
142
143
[
144
{
145
file: `${fixture('insert.2.ipynb')}.xml`,
146
notebookEdits: [
147
NotebookEdit.insertCells(1, [new NotebookCellData(NotebookCellKind.Markup, '', 'markdown')]),
148
NotebookEdit.insertCells(2, [new NotebookCellData(NotebookCellKind.Markup, '', 'markdown')]),
149
NotebookEdit.insertCells(7, [new NotebookCellData(NotebookCellKind.Code, '', 'python')])
150
]
151
},
152
{
153
file: `${fixture('insert.3.ipynb')}.xml`,
154
notebookEdits: [
155
NotebookEdit.insertCells(5, [new NotebookCellData(NotebookCellKind.Code, '', 'python')])
156
]
157
},
158
{
159
file: `${fixture('insert.4.ipynb')}.xml`,
160
notebookEdits: [
161
NotebookEdit.deleteCells(new NotebookRange(1, 2))
162
]
163
}
164
].forEach(testInfo => {
165
test(`Generate ${testInfo.notebookEdits.length} Notebook Edits from ${path.basename(testInfo.file)}`, async () => {
166
// This test focuses on generating as few Notebook edits as possible.
167
// If a user deletes a cell in the middle there's no need to generate any other edits, but just the delete edit.
168
if (provider.kind !== 'xml') {
169
return;
170
}
171
172
const simulation = new SimulationWorkspace();
173
const beforeIPynb = await loadFile({ filePath: fixture('insert.ipynb') });
174
const notebook = await loadNotebook(beforeIPynb, simulation);
175
176
const alternativeFile = await loadFile({ filePath: testInfo.file });
177
let alternativeContents = alternativeFile.contents;
178
const cellSummary = notebook.getCells().map(summarize);
179
cellSummary.forEach(cell => {
180
const toReplace = provider.kind === 'xml' ? `<CELL_ID_${cell.index}>` : `CELL_ID_${cell.index}`;
181
alternativeContents = alternativeContents.replace(toReplace, cell.id);
182
});
183
const alternativeContentLines = AsyncIterableObject.fromArray(alternativeContents.split(/\r?\n/)).map(l => new LineOfText(l));
184
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, alternativeContentLines, undefined, CancellationToken.None);
185
186
187
const notebookEdits: NotebookEdit[] = [];
188
for await (const edit of edits) {
189
if (Array.isArray(edit)) {
190
simulation.applyEdits(edit[0], edit[1]);
191
} else {
192
notebookEdits.push(edit);
193
simulation.applyNotebookEdits(notebook.uri, [edit]);
194
}
195
}
196
197
expect(normatlizeContent(provider.getAlternativeDocument(notebook).getText())).toBe(normatlizeContent(alternativeFile.contents));
198
expect(notebookEdits.length).toBe(testInfo.notebookEdits.length);
199
200
testInfo.notebookEdits.forEach((edit, i) => {
201
expect(notebookEdits[i].newCells.length).toBe(edit.newCells.length);
202
edit.newCells.forEach((c, j) => {
203
expect(notebookEdits[i].newCells[j].kind).toBe(c.kind);
204
expect(notebookEdits[i].newCells[j].languageId).toBe(c.languageId);
205
});
206
expect(notebookEdits[i].range.start).toBe(edit.range.start);
207
expect(notebookEdits[i].range.end).toBe(edit.range.end);
208
});
209
});
210
});
211
212
describe(`${provider.kind} Position Translator`, () => {
213
test(`Translate position in notebook cell to Alternative Document & back`, async () => {
214
const notebook = await loadNotebook(loadFile({ filePath: fixture('sample.ipynb') }));
215
const altDoc = provider.getAlternativeDocument(notebook);
216
217
const positions = [
218
{ cellIndex: 0, start: new Position(0, 9), end: new Position(0, 17) },
219
{ cellIndex: 1, start: new Position(0, 0), end: new Position(0, 34) },
220
{ cellIndex: 1, start: new Position(0, 0), end: new Position(0, 33) },
221
{ cellIndex: 2, start: new Position(0, 0), end: new Position(0, 6) },
222
{ cellIndex: 2, start: new Position(1, 7), end: new Position(1, 9) },
223
{ cellIndex: 3, start: new Position(1, 10), end: new Position(2, 9) },
224
{ cellIndex: 5, start: new Position(1, 10), end: new Position(1, 20) },
225
];
226
227
for (const pos of positions) {
228
const cell = notebook.cellAt(pos.cellIndex);
229
const startTranslation = [pos.start, pos.end].map(p => altDoc.fromCellPosition(cell, p));
230
const textFromCell = cell.document.getText(new Range(pos.start, pos.end));
231
const textFromAltDoc = altDoc.getText(new Range(startTranslation[0], startTranslation[1]));
232
if (provider.kind !== 'json' || pos.start.line === pos.end.line) {
233
expect(normatlizeContent(textFromAltDoc)).toBe(normatlizeContent(textFromCell));
234
} else {
235
expect(normatlizeContent(textFromAltDoc).split(/\r?\n/).join(EOL)).toBe([`\\"Hello from Python!\\")",`, ` " print`].join(EOL));
236
}
237
238
// Now try the reverse translation.
239
if (provider.kind !== 'json') {
240
const cellPosition = altDoc.toCellPosition(startTranslation[0]);
241
expect(cellPosition).toBeDefined();
242
expect(cellPosition?.cell).toBe(cell);
243
expect(cellPosition?.position.line).toBe(pos.start.line);
244
expect(cellPosition?.position.character).toBe(pos.start.character);
245
}
246
}
247
});
248
249
test(`getAlternativeDocumentFromText rebuilds cell offset map correctly`, async () => {
250
if (provider.kind === 'json') {
251
// JSON format doesn't use getAlternativeDocumentFromText
252
return;
253
}
254
255
const simulation = new SimulationWorkspace();
256
const cells = [
257
new NotebookCellData(NotebookCellKind.Code, 'import sys', 'python'),
258
new NotebookCellData(NotebookCellKind.Code, 'print(sys.executable)', 'python'),
259
new NotebookCellData(NotebookCellKind.Markup, '# Hello World', 'markdown'),
260
new NotebookCellData(NotebookCellKind.Code, 'import os\nprint(os.path)', 'python'),
261
];
262
const notebook = ExtHostNotebookDocumentData.fromNotebookData(
263
Uri.file('test.ipynb'),
264
new NotebookData(cells),
265
'jupyter-notebook',
266
simulation
267
).document;
268
269
// Get the alternative document
270
const altDoc = provider.getAlternativeDocument(notebook);
271
const originalText = altDoc.getText();
272
273
// Rebuild from text
274
const rebuiltDoc = provider.getAlternativeDocumentFromText(originalText, notebook);
275
276
// Test that the rebuilt document has the same text
277
expect(rebuiltDoc.getText()).toBe(originalText);
278
279
// Test position translation works correctly
280
const positions = [
281
{ cellIndex: 0, position: new Position(0, 0) },
282
{ cellIndex: 0, position: new Position(0, 6) },
283
{ cellIndex: 1, position: new Position(0, 0) },
284
{ cellIndex: 1, position: new Position(0, 10) },
285
{ cellIndex: 2, position: new Position(0, 0) },
286
{ cellIndex: 3, position: new Position(0, 0) },
287
{ cellIndex: 3, position: new Position(1, 5) },
288
];
289
290
for (const pos of positions) {
291
const cell = notebook.cellAt(pos.cellIndex);
292
293
// Translate from cell to alternative document
294
const altPosition = rebuiltDoc.fromCellPosition(cell, pos.position);
295
296
// Translate back from alternative document to cell
297
const cellPosition = rebuiltDoc.toCellPosition(altPosition);
298
299
expect(cellPosition).toBeDefined();
300
expect(cellPosition?.cell).toBe(cell);
301
expect(cellPosition?.position.line).toBe(pos.position.line);
302
expect(cellPosition?.position.character).toBe(pos.position.character);
303
}
304
});
305
306
test(`getAlternativeDocumentFromText handles cells without IDs`, async () => {
307
if (provider.kind === 'json') {
308
return;
309
}
310
311
const simulation = new SimulationWorkspace();
312
const cells = [
313
new NotebookCellData(NotebookCellKind.Code, 'x = 1', 'python'),
314
new NotebookCellData(NotebookCellKind.Code, 'y = 2', 'python'),
315
new NotebookCellData(NotebookCellKind.Code, 'z = 3', 'python'),
316
];
317
const notebook = ExtHostNotebookDocumentData.fromNotebookData(
318
Uri.file('test.ipynb'),
319
new NotebookData(cells),
320
'jupyter-notebook',
321
simulation
322
).document;
323
324
// Get alternative document text
325
const altDoc = provider.getAlternativeDocument(notebook);
326
let text = altDoc.getText();
327
328
// Strip cell IDs to simulate LLM-generated content without IDs
329
if (provider.kind === 'xml') {
330
text = text.replace(/id="[^"]+"/g, 'id=""');
331
} else if (provider.kind === 'text') {
332
text = text.replace(/\[id=[^\]]+\]/g, '');
333
}
334
335
// Rebuild from text without IDs
336
const rebuiltDoc = provider.getAlternativeDocumentFromText(text, notebook);
337
338
// Verify position translation still works by matching language
339
for (let i = 0; i < notebook.cellCount; i++) {
340
const cell = notebook.cellAt(i);
341
const position = new Position(0, 0);
342
343
const altPosition = rebuiltDoc.fromCellPosition(cell, position);
344
const cellPosition = rebuiltDoc.toCellPosition(altPosition);
345
346
expect(cellPosition).toBeDefined();
347
expect(cellPosition?.cell.document.languageId).toBe('python');
348
}
349
});
350
351
test(`getAlternativeDocumentFromText handles markdown cells correctly`, async () => {
352
if (provider.kind === 'json') {
353
return;
354
}
355
356
const simulation = new SimulationWorkspace();
357
const cells = [
358
new NotebookCellData(NotebookCellKind.Markup, '# Title\nSome content', 'markdown'),
359
new NotebookCellData(NotebookCellKind.Code, 'print("hello")', 'python'),
360
new NotebookCellData(NotebookCellKind.Markup, '## Subtitle\nMore text', 'markdown'),
361
];
362
const notebook = ExtHostNotebookDocumentData.fromNotebookData(
363
Uri.file('test.ipynb'),
364
new NotebookData(cells),
365
'jupyter-notebook',
366
simulation
367
).document;
368
369
const altDoc = provider.getAlternativeDocument(notebook);
370
const text = altDoc.getText();
371
const rebuiltDoc = provider.getAlternativeDocumentFromText(text, notebook);
372
373
// Test markdown cell position translation
374
const markdownCell1 = notebook.cellAt(0);
375
const markdownCell2 = notebook.cellAt(2);
376
377
const pos1 = new Position(0, 2); // Inside "# Title"
378
const pos2 = new Position(0, 3); // Inside "## Subtitle"
379
380
const altPos1 = rebuiltDoc.fromCellPosition(markdownCell1, pos1);
381
const altPos2 = rebuiltDoc.fromCellPosition(markdownCell2, pos2);
382
383
const backToCell1 = rebuiltDoc.toCellPosition(altPos1);
384
const backToCell2 = rebuiltDoc.toCellPosition(altPos2);
385
386
expect(backToCell1?.cell).toBe(markdownCell1);
387
expect(backToCell1?.position.line).toBe(0);
388
expect(backToCell1?.position.character).toBe(2);
389
390
expect(backToCell2?.cell).toBe(markdownCell2);
391
expect(backToCell2?.position.line).toBe(0);
392
expect(backToCell2?.position.character).toBe(3);
393
});
394
});
395
396
test(`Parse with leading empty lines`, async () => {
397
const txt = `
398
399
#%% vscode.cell [language=python]
400
import math
401
402
def circle_area(radius):
403
return math.pi * radius**2
404
`;
405
const xml = `
406
407
<VSCode.Cell id="f18c8b6e" language="python">
408
import math
409
410
def circle_area(radius):
411
return math.pi * radius**2
412
</VSCode.Cell>
413
`;
414
const json = `
415
416
{
417
"cells": [
418
{
419
"cell_type": "code",
420
"metadata": {
421
"id": "f18c8b6e",
422
"language": "python"
423
},
424
"source": [
425
"import math",
426
"",
427
"def circle_area(radius):",
428
" return math.pi * radius**2"
429
]
430
}
431
]
432
}
433
`;
434
const content = provider.kind === 'xml' ? xml : (provider.kind === 'text' ? txt : json);
435
const uri = Uri.file('single_before.ipynb');
436
const notebook = ExtHostNotebookDocumentData.createJupyterNotebook(uri, JSON.stringify({ cells: [] })).document;
437
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(content), undefined, CancellationToken.None);
438
const notebookEdits = [];
439
for await (const edit of edits) {
440
notebookEdits.push(edit);
441
}
442
expect(notebookEdits.length).toBe(1);
443
expect(notebookEdits[0]).toBeInstanceOf(NotebookEdit);
444
expect((notebookEdits[0] as NotebookEdit).newCells.length).toBe(1);
445
expect(normatlizeContent((notebookEdits[0] as NotebookEdit).newCells[0].value)).toBe(normatlizeContent(`import math
446
447
def circle_area(radius):
448
return math.pi * radius**2
449
`));
450
});
451
test(`Parse with empty lines between cell markers`, async () => {
452
if (provider.kind !== 'xml') {
453
return;
454
}
455
const content = `<VSCode.Cell id="feb4cb5e" language="julia">
456
function circleArea(r::Float64)
457
return pi * r * r
458
end
459
</VSCode.Cell>
460
461
462
<VSCode.Cell language="julia">
463
function calculateCircleArea(radius::Float64)
464
return pi * radius^2
465
end
466
</VSCode.Cell>`;
467
const uri = Uri.file('single_before.ipynb');
468
const notebook = ExtHostNotebookDocumentData.createJupyterNotebook(uri, JSON.stringify({ cells: [] })).document;
469
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(content), undefined, CancellationToken.None);
470
const notebookEdits = [];
471
for await (const edit of edits) {
472
notebookEdits.push(edit);
473
}
474
expect(notebookEdits.length).toBe(2);
475
expect(notebookEdits[0]).toBeInstanceOf(NotebookEdit);
476
expect((notebookEdits[0] as NotebookEdit).newCells.length).toBe(1);
477
expect(normatlizeContent((notebookEdits[0] as NotebookEdit).newCells[0].value)).toBe(normatlizeContent(`function circleArea(r::Float64)
478
return pi * r * r
479
end
480
`));
481
expect(normatlizeContent((notebookEdits[1] as NotebookEdit).newCells[0].value)).toBe(normatlizeContent(`function calculateCircleArea(radius::Float64)
482
return pi * radius^2
483
end
484
`));
485
});
486
test('Handle duplicate ids', async () => {
487
if (provider.kind === 'text' || provider.kind === 'json') {
488
return;
489
}
490
const simulation = new SimulationWorkspace();
491
const file = await loadFile({ filePath: fixture('duplicateCellIds.xml') });
492
const notebook = await loadNotebook(await loadFile({ filePath: fixture('duplicateCellIds.ipynb') }), simulation);
493
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(file.contents), undefined, CancellationToken.None);
494
for await (const edit of edits) {
495
if (!Array.isArray(edit)) {
496
simulation.applyNotebookEdits(notebook.uri, [edit]);
497
}
498
}
499
500
expect(notebook.cellCount).toBe(11);
501
expect(notebook.getCells()[0].kind).toBe(NotebookCellKind.Markup);
502
expect(notebook.getCells()[1].kind).toBe(NotebookCellKind.Code);
503
expect(notebook.getCells()[2].kind).toBe(NotebookCellKind.Code);
504
expect(notebook.getCells()[3].kind).toBe(NotebookCellKind.Markup);
505
expect(notebook.getCells()[4].kind).toBe(NotebookCellKind.Code);
506
expect(notebook.getCells()[5].kind).toBe(NotebookCellKind.Markup);
507
expect(notebook.getCells()[6].kind).toBe(NotebookCellKind.Code);
508
expect(notebook.getCells()[7].kind).toBe(NotebookCellKind.Markup);
509
expect(notebook.getCells()[8].kind).toBe(NotebookCellKind.Code);
510
expect(notebook.getCells()[9].kind).toBe(NotebookCellKind.Markup);
511
expect(notebook.getCells()[10].kind).toBe(NotebookCellKind.Code);
512
});
513
});
514
describe(`${provider.kind} Edit Generation`, () => {
515
[
516
'circle_area_edits',
517
'delete_1_line_in_cell',
518
'data_processing',
519
'data_processing_2',
520
'data_visualization',
521
'data_visualization_2',
522
'datacleansing',
523
'dataframe',
524
'edit',
525
'empty',
526
'imports',
527
'large_cell',
528
'multicells',
529
'plot',
530
'plotly_to_matplotlib',
531
'refactor',
532
'reorder',
533
'single',
534
'variables'
535
].forEach((filePath) => {
536
test(`Apply Edits for ${path.basename(filePath)}`, async () => {
537
if ((filePath === 'plotly_to_matplotlib' || filePath === 'matplotlib_to_plotly') && provider.kind === 'json') {
538
// generating text edits for JSON format and ensuring the final output is the same as that generated for text/xml is difficult.
539
return;
540
}
541
if (provider.kind === 'json' && ['delete_1_line_in_cell'].includes(filePath)) {
542
// Incorrectly genrated edits for JSON format.
543
return;
544
}
545
const simulation = new SimulationWorkspace();
546
const [atlContent, beforeIPynb, afterIPynb] = await Promise.all([loadFile({ filePath: fixture(`${filePath}.altContent.${provider.kind}`) }), loadFile({ filePath: fixture(`${filePath}_before.ipynb`) }), loadFile({ filePath: fixture(`${filePath}_after.ipynb`) })]);
547
const notebook = await loadNotebook(beforeIPynb, simulation);
548
const cellSummary = notebook.getCells().map(summarize);
549
cellSummary.forEach(cell => {
550
const toReplace = provider.kind === 'xml' ? `<CELL_ID_${cell.index}>` : `CELL_ID_${cell.index}`;
551
atlContent.contents = atlContent.contents.replace(toReplace, cell.id);
552
});
553
554
const notebookEdits: (NotebookEdit | [Uri, TextEdit[]])[] = [];
555
for await (const edit of getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(atlContent.contents), undefined, CancellationToken.None)) {
556
notebookEdits.push(edit);
557
}
558
559
const notebookData = applyNotebookEdits(notebook, notebookEdits, simulation);
560
const expectedNotebook = await loadNotebook(afterIPynb, simulation);
561
if (filePath === 'plotly_to_matplotlib' && provider.kind === 'text') {
562
// The edits generated for text version is slightly different, hence the result notebook is not the same as we'd expect when using xml.
563
// Hence we need to skip the failing cell (due to differences in LLM outputs)
564
notebookData.cells[8].value = expectedNotebook.getCells()[8].document.getText();
565
notebookData.cells[10].value = expectedNotebook.getCells()[10].document.getText();
566
}
567
if (filePath === 'multicells' && provider.kind === 'text') {
568
// The edits generated for text version is slightly different, hence the result notebook is not the same as we'd expect when using xml.
569
// Hence we need to skip the failing cell (due to differences in LLM outputs)
570
notebookData.cells[1].value = expectedNotebook.getCells()[1].document.getText();
571
notebookData.cells[3].value = expectedNotebook.getCells()[3].document.getText();
572
}
573
assertDocumentsAreEqual(expectedNotebook, notebookData, provider.kind);
574
});
575
test(`Generate Edits for New Document for ${path.basename(filePath)}`, async () => {
576
if ((filePath === 'plotly_to_matplotlib' || filePath === 'matplotlib_to_plotly') && provider.kind === 'json') {
577
// generating text edits for JSON format and ensuring the final output is the same as that generated for text/xml is difficult.
578
return;
579
}
580
const ipynb = await loadFile({ filePath: fixture(`${filePath}_before.ipynb`) });
581
const notebook = await loadNotebook(ipynb);
582
const altContent = provider.getAlternativeDocument(notebook).getText();
583
const alternativeContentLines = textToAsyncIterableLines(altContent);
584
const newEdits = await getEditGenerator(provider).generateNotebookEdits(Uri.file('newNotebook.ipynb'), alternativeContentLines, undefined, CancellationToken.None);
585
const notebookEdits: NotebookEdit[] = [];
586
for await (const edit of newEdits) {
587
if (!Array.isArray(edit)) {
588
notebookEdits.push(edit);
589
}
590
}
591
expect(notebookEdits.length).toBe(notebook.cellCount);
592
notebook.getCells().forEach((cell, i) => {
593
const expectedCell = notebook.cellAt(i);
594
expect(normatlizeContent(cell.document.getText())).toBe(normatlizeContent(expectedCell.document.getText()));
595
expect(cell.document.languageId).toBe(expectedCell.document.languageId);
596
expect(cell.kind).toBe(expectedCell.kind);
597
});
598
});
599
});
600
});
601
602
/**
603
* In realworld, notebook gets edited asynchronously.
604
* I.e. when we stream the edits, the edits are not applied immediately.
605
* In tests, they get applied immediately.
606
*
607
* Lets cover both cases.
608
*/
609
async function applyEditsSyncOrAsync(simulation: SimulationWorkspace, notebook: NotebookDocument, edits: AsyncIterable<NotebookEdit | [Uri, TextEdit[]]>, applyEditsImmediately: boolean) {
610
const notebookEdits = [];
611
if (applyEditsImmediately) {
612
for await (const edit of edits) {
613
if (Array.isArray(edit)) {
614
simulation.applyEdits(edit[0], edit[1]);
615
} else {
616
simulation.applyNotebookEdits(notebook.uri, [edit]);
617
notebookEdits.push(edit);
618
}
619
}
620
621
} else {
622
const collectedEdits = [];
623
for await (const edit of edits) {
624
collectedEdits.push(edit);
625
}
626
for (const edit of collectedEdits) {
627
if (Array.isArray(edit)) {
628
simulation.applyEdits(edit[0], edit[1]);
629
} else {
630
simulation.applyNotebookEdits(notebook.uri, [edit]);
631
notebookEdits.push(edit);
632
}
633
}
634
}
635
return notebookEdits;
636
}
637
638
describe(`${provider.kind} Generate Edits (insert/delete/swap`, () => {
639
async function applyEditsAndVerify(cells: { index: number; contents: number }[]) {
640
const simulation = new SimulationWorkspace();
641
const notebook = await loadNotebook(await loadFile({ filePath: fixture('swapping_cells.ipynb') }), simulation);
642
let altContent = cells.map(item => {
643
return [
644
`<VSCode.Cell id="<CELL_ID_${item.index}>" language="python">`,
645
`${item.contents}`,
646
`</VSCode.Cell>`
647
].join(EOL);
648
}).join(EOL);
649
const cellSummary = notebook.getCells().map(summarize);
650
cellSummary.forEach(cell => {
651
const toReplace = provider.kind === 'xml' ? `<CELL_ID_${cell.index}>` : `CELL_ID_${cell.index}`;
652
altContent = altContent.replace(toReplace, cell.id);
653
});
654
655
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(altContent), undefined, CancellationToken.None);
656
const notebookEdits = await applyEditsSyncOrAsync(simulation, notebook, edits, applyEditsImmediately);
657
658
expect(notebook.getCells().map(c => c.document.getText()).join()).toBe(cells.map(i => `${i.contents}`).join());
659
return { notebook, notebookEdits };
660
}
661
test('Insert 1 cell at the top', async () => {
662
if (provider.kind !== 'xml') {
663
return;
664
}
665
const cells = [10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => ({ index: i, contents: i }));
666
const { notebookEdits } = await applyEditsAndVerify(cells);
667
expect(notebookEdits.length).toBe(1);
668
expect(notebookEdits[0].newCells.length).toBe(1);
669
expect(notebookEdits[0].newCells[0].value).toBe('10');
670
expect(notebookEdits[0].range.start).toBe(0);
671
});
672
test('Insert 1 cell at the end', async () => {
673
if (provider.kind !== 'xml') {
674
return;
675
}
676
const cells = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => ({ index: i, contents: i }));
677
const { notebookEdits } = await applyEditsAndVerify(cells);
678
expect(notebookEdits.length).toBe(1);
679
expect(notebookEdits[0].newCells.length).toBe(1);
680
expect(notebookEdits[0].newCells[0].value).toBe('10');
681
});
682
test('Swap 2 Cells', async () => {
683
if (provider.kind !== 'xml') {
684
return;
685
}
686
const cells = [0, 1, 2, 3, 4, 5, 6, 7, 9, 8].map(i => ({ index: i, contents: i }));
687
await applyEditsAndVerify(cells);
688
});
689
test('Moving 2 Cells', async () => {
690
if (provider.kind !== 'xml') {
691
return;
692
}
693
const cells = [0, 1, 2, 3, 4, 5, 6, 9, 7, 8].map(i => ({ index: i, contents: i }));
694
await applyEditsAndVerify(cells);
695
});
696
test('Delete 1 Cell', async () => {
697
if (provider.kind !== 'xml') {
698
return;
699
}
700
const cells = [0, 1, 2, 3, 4, 5, 6, 7, 8].map(i => ({ index: i, contents: i }));
701
const { notebookEdits } = await applyEditsAndVerify(cells);
702
703
expect(notebookEdits.length).toBe(1);
704
expect(notebookEdits[0].range.start).toBe(9);
705
});
706
test('Move last Cell to top', async () => {
707
if (provider.kind !== 'xml') {
708
return;
709
}
710
const cells = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8].map(i => ({ index: i, contents: i }));
711
const { notebookEdits } = await applyEditsAndVerify(cells);
712
713
expect(notebookEdits.length).toBe(2);
714
expect(notebookEdits[0].range.start).toBe(9);
715
expect(notebookEdits[1].range.start).toBe(0);
716
expect(notebookEdits[1].newCells[0].value).toBe('9');
717
});
718
test('Swap and insert', async () => {
719
if (provider.kind !== 'xml') {
720
return;
721
}
722
const cells = [9, 0, 1, 2, 3, 14, 15, 6, 7, 8].map(i => ({ index: i, contents: i }));
723
await applyEditsAndVerify(cells);
724
});
725
test('Swap multiple and insert', async () => {
726
if (provider.kind !== 'xml') {
727
return;
728
}
729
const cells = [1, 2, 3, 4, 6, 5, 9, 0].map(i => ({ index: i, contents: i }));
730
await applyEditsAndVerify(cells);
731
});
732
test('Swap multiple and delete', async () => {
733
if (provider.kind !== 'xml') {
734
return;
735
}
736
const cells = [1, 2, 3, 5, 6, 4, 0, 9].map(i => ({ index: i, contents: i }));
737
await applyEditsAndVerify(cells);
738
});
739
test('Move top Cell to bottom', async () => {
740
if (provider.kind !== 'xml') {
741
return;
742
}
743
const cells = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map(i => ({ index: i, contents: i }));
744
const { notebookEdits } = await applyEditsAndVerify(cells);
745
746
expect(notebookEdits.length).toBe(2);
747
expect(notebookEdits[0].range.start).toBe(0);
748
expect(notebookEdits[1].range.start).toBe(9);
749
expect(notebookEdits[1].newCells[0].value).toBe('0');
750
});
751
test('Insert 2 Cell at the top', async () => {
752
if (provider.kind !== 'xml') {
753
return;
754
}
755
const cells = [10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => ({ index: i, contents: i }));
756
const { notebookEdits } = await applyEditsAndVerify(cells);
757
758
expect(notebookEdits.length).toBe(2);
759
expect(notebookEdits[0].newCells[0].value).toBe('10');
760
expect(notebookEdits[0].range.start).toBe(0);
761
expect(notebookEdits[1].newCells[0].value).toBe('11');
762
expect(notebookEdits[1].range.start).toBe(1);
763
});
764
test('Insert 8 Cell in the middle', async () => {
765
if (provider.kind !== 'xml') {
766
return;
767
}
768
const cells = [0, 1, 2, 3, 4, 15, 16, 17, 18, 19, 20, 21, 22, 5, 6, 7, 8, 9].map(i => ({ index: i, contents: i }));
769
const { notebookEdits } = await applyEditsAndVerify(cells);
770
771
expect(notebookEdits.length).toBe(8);
772
});
773
test('Delete 3 cells from the middle', async () => {
774
if (provider.kind !== 'xml') {
775
return;
776
}
777
const cells = [0, 1, 2, 3, 7, 8, 9].map(i => ({ index: i, contents: i }));
778
const { notebookEdits } = await applyEditsAndVerify(cells);
779
780
expect(notebookEdits.length).toBe(3);
781
});
782
test('Delete 3 Cell', async () => {
783
if (provider.kind !== 'xml') {
784
return;
785
}
786
const cells = [{ index: 1, contents: 1 }, { index: 2, contents: 2 }, { index: 3, contents: 3 }, { index: 4, contents: 4 }, { index: 5, contents: 5 }, { index: 6, contents: 6 }, { index: 7, contents: 7 }];
787
const { notebookEdits } = await applyEditsAndVerify(cells);
788
789
// We should only have 3 deletes
790
expect(notebookEdits.length).toBe(3);
791
expect(notebookEdits[0].range.start).toBe(9);
792
expect(notebookEdits[1].range.start).toBe(8);
793
expect(notebookEdits[2].range.start).toBe(0);
794
});
795
test('Delete 3 Cell (from middle as well)', async () => {
796
if (provider.kind !== 'xml') {
797
return;
798
}
799
const cells = [{ index: 1, contents: 1 }, { index: 2, contents: 2 }, { index: 3, contents: 3 }, { index: 4, contents: 4 }, { index: 6, contents: 6 }, { index: 7, contents: 7 }, { index: 8, contents: 8 }];
800
const { notebookEdits } = await applyEditsAndVerify(cells);
801
802
// We should only have 3 deletes
803
expect(notebookEdits.length).toBe(3);
804
expect(notebookEdits[0].range.start).toBe(9);
805
expect(notebookEdits[1].range.start).toBe(5);
806
expect(notebookEdits[2].range.start).toBe(0);
807
});
808
test('Delete first and update second', async () => {
809
if (provider.kind !== 'xml') {
810
return;
811
}
812
const cells = [{ index: 1, contents: 2 }, { index: 2, contents: 2 }, { index: 3, contents: 3 }, { index: 4, contents: 4 }, { index: 6, contents: 6 }, { index: 7, contents: 7 }, { index: 8, contents: 8 }];
813
await applyEditsAndVerify(cells);
814
});
815
test('Delete first, last and update few in middle', async () => {
816
if (provider.kind !== 'xml') {
817
return;
818
}
819
const cells = [{ index: 1, contents: 1 }, { index: 2, contents: 2222 }, { index: 3, contents: 999 }, { index: 4, contents: 4 }, { index: 6, contents: 6 }, { index: 7, contents: 7 }];
820
await applyEditsAndVerify(cells);
821
});
822
});
823
824
describe(`${provider.kind} Generate Edits instead of inserting and deleteing a cell (where id is missing)`, () => {
825
test('Do not insert and delete the same cell if id is missing', async () => {
826
const simulation = new SimulationWorkspace();
827
const cells = [
828
[`import sys`, `import os`],
829
[`print(sys.executable)`]
830
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
831
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
832
833
const newNotebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test2.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
834
let alternativeContent = provider.getAlternativeDocument(newNotebook).getText();
835
const id = summarize(newNotebook.getCells()[0]).id;
836
alternativeContent = alternativeContent.replace(id, '');
837
838
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
839
const notebookEdits = await applyEditsSyncOrAsync(simulation, notebook, edits, applyEditsImmediately);
840
841
expect(notebookEdits.length).toBe(0);
842
notebook.getCells().forEach((cell, i) => {
843
expect(cell.document.getText()).toBe(newNotebook.getCells()[i].document.getText());
844
});
845
});
846
test('Do not insert and delete the same two cell if id is missing, just insert the new 3rd cell', async () => {
847
const simulation = new SimulationWorkspace();
848
let cells = [
849
[`import sys`, `import os`],
850
[`print(sys.executable)`],
851
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
852
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
853
854
cells = [
855
[`import sys`, `import os`],
856
[`print(sys.executable)`],
857
[`print("Hello World")`]
858
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
859
const newNotebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test2.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
860
let alternativeContent = provider.getAlternativeDocument(newNotebook).getText();
861
newNotebook.getCells().forEach(cell => {
862
const id = summarize(cell).id;
863
alternativeContent = alternativeContent.replace(id, '');
864
});
865
866
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
867
const notebookEdits = await applyEditsSyncOrAsync(simulation, notebook, edits, applyEditsImmediately);
868
869
expect(notebookEdits.length).toBe(1);
870
expect(notebookEdits[0].range.start).toBe(2);
871
expect(notebookEdits[0].newCells[0].value).toBe('print("Hello World")');
872
expect(notebookEdits[0].newCells.length).toBe(1);
873
notebook.getCells().forEach((cell, i) => {
874
expect(cell.document.getText()).toBe(newNotebook.getCells()[i].document.getText());
875
});
876
});
877
test('Insert new cell, instead of deleting the inserted cell', async () => {
878
const simulation = new SimulationWorkspace();
879
let cells = [
880
[``],
881
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
882
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
883
884
cells = [
885
[`import sys`],
886
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
887
const newNotebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test2.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
888
let alternativeContent = provider.getAlternativeDocument(newNotebook).getText();
889
newNotebook.getCells().forEach(cell => {
890
const id = summarize(cell).id;
891
alternativeContent = alternativeContent.replace(id, '');
892
});
893
alternativeContent = alternativeContent.replace(`id=""`, '');
894
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
895
896
await applyEditsSyncOrAsync(simulation, notebook, edits, applyEditsImmediately);
897
898
notebook.getCells().forEach((cell, i) => {
899
expect(cell.document.getText()).toBe(newNotebook.getCells()[i].document.getText());
900
});
901
});
902
});
903
});
904
describe('Malformed XML', () => {
905
test('Missing line breaks in one cell', async () => {
906
if (provider.kind !== 'xml') {
907
return;
908
}
909
const simulation = new SimulationWorkspace();
910
const cells = [
911
[`import sys`],
912
[`print(sys.executable)`],
913
[`import os`],
914
[`print(os.path)`],
915
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
916
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
917
918
let alternativeContent = provider.getAlternativeDocument(notebook).getText();
919
alternativeContent = alternativeContent.replace('sys.executable', '"Hello World"');
920
alternativeContent = alternativeContent.split(/\r?\n/).join(EOL);
921
// Remove the line break and ensure end cell tag is on the same line as the last line of code.
922
alternativeContent = alternativeContent.replace(`print("Hello World")${EOL}</VSCode.Cell>`, `print("Hello World")</VSCode.Cell>`);
923
924
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
925
const notebookEdits = [];
926
for await (const edit of edits) {
927
if (Array.isArray(edit)) {
928
simulation.applyEdits(edit[0], edit[1]);
929
} else {
930
simulation.applyNotebookEdits(notebook.uri, [edit]);
931
notebookEdits.push(edit);
932
}
933
}
934
935
expect(notebook.cellAt(0).document.getText()).toBe('import sys');
936
expect(notebook.cellAt(1).document.getText()).toBe('print("Hello World")');
937
expect(notebook.cellAt(2).document.getText()).toBe('import os');
938
expect(notebook.cellAt(3).document.getText()).toBe('print(os.path)');
939
});
940
test('Missing line breaks in all cells', async () => {
941
if (provider.kind !== 'xml') {
942
return;
943
}
944
const simulation = new SimulationWorkspace();
945
const cells = [
946
[`import sys`],
947
[`print(sys.executable)`],
948
[`import os`],
949
[`print(os.path)`],
950
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
951
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
952
953
let alternativeContent = provider.getAlternativeDocument(notebook).getText();
954
alternativeContent = alternativeContent.replace('sys.executable', '"Hello World"');
955
alternativeContent = alternativeContent.split(/\r?\n/).join(EOL);
956
// Remove the line break and ensure end cell tag is on the same line as the last line of code.
957
alternativeContent = alternativeContent.replace(`${EOL}</VSCode.Cell>`, `</VSCode.Cell>`);
958
959
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
960
const notebookEdits = [];
961
for await (const edit of edits) {
962
if (Array.isArray(edit)) {
963
simulation.applyEdits(edit[0], edit[1]);
964
} else {
965
simulation.applyNotebookEdits(notebook.uri, [edit]);
966
notebookEdits.push(edit);
967
}
968
}
969
970
expect(notebook.cellAt(0).document.getText()).toBe('import sys');
971
expect(notebook.cellAt(1).document.getText()).toBe('print("Hello World")');
972
expect(notebook.cellAt(2).document.getText()).toBe('import os');
973
expect(notebook.cellAt(3).document.getText()).toBe('print(os.path)');
974
});
975
test('Deliberately include EndCell marker in a cell', async () => {
976
if (provider.kind !== 'xml') {
977
return;
978
}
979
const simulation = new SimulationWorkspace();
980
const cells = [
981
[`import sys`],
982
[`print(sys.executable)`],
983
[`import os</VSCode.Cell>`],
984
[`print(os.path)`],
985
].map(contents => new NotebookCellData(NotebookCellKind.Code, contents.join(EOL), 'python'));
986
const notebook = ExtHostNotebookDocumentData.fromNotebookData(Uri.file('test.ipynb'), new NotebookData(cells), 'jupyter-notebook', simulation).document;
987
988
let alternativeContent = provider.getAlternativeDocument(notebook).getText();
989
alternativeContent = alternativeContent.replace('sys.executable', '"Hello World"');
990
alternativeContent = alternativeContent.split(/\r?\n/).join(EOL);
991
// Remove the line break and ensure end cell tag is on the same line as the last line of code.
992
alternativeContent = alternativeContent.replace(`${EOL}</VSCode.Cell>`, `</VSCode.Cell>`);
993
994
const edits = await getEditGenerator(provider).generateNotebookEdits(notebook, textToAsyncIterableLines(alternativeContent), undefined, CancellationToken.None);
995
const notebookEdits = [];
996
for await (const edit of edits) {
997
if (Array.isArray(edit)) {
998
simulation.applyEdits(edit[0], edit[1]);
999
} else {
1000
simulation.applyNotebookEdits(notebook.uri, [edit]);
1001
notebookEdits.push(edit);
1002
}
1003
}
1004
1005
expect(notebook.cellAt(0).document.getText()).toBe('import sys');
1006
expect(notebook.cellAt(1).document.getText()).toBe('print("Hello World")');
1007
expect(notebook.cellAt(2).document.getText()).toBe('import os</VSCode.Cell>');
1008
expect(notebook.cellAt(3).document.getText()).toBe('print(os.path)');
1009
});
1010
});
1011
});
1012
});
1013
1014
function applyNotebookEdits(notebook: NotebookDocument, edits: (NotebookEdit | [Uri, TextEdit[]])[], simulationWorkspace: SimulationWorkspace) {
1015
const notebookEdits: NotebookEdit[] = [];
1016
for (const edit of edits) {
1017
if (Array.isArray(edit)) {
1018
simulationWorkspace.applyEdits(edit[0], edit[1]);
1019
} else {
1020
notebookEdits.push(edit);
1021
}
1022
}
1023
1024
simulationWorkspace.applyNotebookEdits(notebook.uri, notebookEdits);
1025
return notebookDocumentToData(notebook);
1026
}
1027
1028
function notebookDocumentToData(notebook: NotebookDocument): NotebookData {
1029
const newCells = notebook.getCells().map(notebookCellToCellData);
1030
const newCellMap = new ResourceMap<NotebookCellData>();
1031
notebook.getCells().forEach((cell, i) => {
1032
newCellMap.set(cell.document.uri, newCells[i]);
1033
});
1034
1035
return new NotebookData(newCells);
1036
}
1037
1038
function assertDocumentsAreEqual(notebook: NotebookDocument, data: NotebookData, kind: 'xml' | 'text' | 'json') {
1039
expect(notebook.cellCount).toBe(data.cells.length);
1040
for (let i = 0; i < notebook.cellCount; i++) {
1041
const cell = notebook.cellAt(i);
1042
const cellData = data.cells[i];
1043
// LLMs retun empty new lines for jupytext cells. Check the case of `reorder.ipynb`
1044
if (kind === 'text') {
1045
expect(normatlizeContent(cell.document.getText())).toBe(normatlizeContent(cellData.value));
1046
} else if (kind === 'json') {
1047
// With JSON with get extra padding and thats wrong.
1048
// E.g. doc string in python will have extra padding.
1049
// Before
1050
/**
1051
"source": [
1052
"import math",
1053
"",
1054
"def circle_area(radius):",
1055
" print(\"HELLO WORLD\")",
1056
" return math.pi * radius**2"
1057
]
1058
*/
1059
// Response from LLM, notice how the empty lines in docstrings are indented.
1060
/**
1061
"source": [
1062
"import math",
1063
"",
1064
"def circle_area(radius):",
1065
" \"\"\"",
1066
" Calculate the area of a circle given its radius.",
1067
" ",
1068
" Args:",
1069
" radius (float): The radius of the circle.",
1070
" ",
1071
" Returns:",
1072
" float: The area of the circle.",
1073
" \"\"\"",
1074
" print(\"HELLO WORLD\")",
1075
" return math.pi * radius**2"
1076
]
1077
*/
1078
expect(normatlizeContent(cell.document.getText().split(/\r?\n/g).map(l => l.trim()).join('\n'))).toBe(normatlizeContent(cellData.value.split(/\r?\n/g).map(l => l.trim()).join('\n')));
1079
} else {
1080
expect(normatlizeContent(cell.document.getText())).toBe(normatlizeContent(cellData.value));
1081
}
1082
expect(cell.document.languageId).toBe(cellData.languageId);
1083
expect(cell.kind).toBe(cellData.kind);
1084
}
1085
}
1086
1087
1088
/**
1089
* Strip the id value from the string `id="2ce940c2"` to `id=""`.
1090
*/
1091
function normatlizeContent(content: string) {
1092
return content.
1093
replace(/id="[^"]+"/g, 'id=""'). // xml id
1094
replace(/id=[^"]+/g, 'id='). // jupytext id
1095
replace(/"id": "[^"]+"/g, '"id": ""'). // json id
1096
replace(/\r\n/g, '\n'). // windows/unix newlines
1097
trim();
1098
}
1099
1100