Skip to content

Commit ff1d4b0

Browse files
authored
Ability to clear cell output in NativeNotebook (#12307)
For #10496 For #12274 Fixing #12274 only for native notebooks here. Was an easy fix.
1 parent aeab323 commit ff1d4b0

13 files changed

+227
-188
lines changed

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export function isUnitTestExecution(): boolean {
111111

112112
// Temporary constant, used to indicate whether we're using custom editor api or not.
113113
export const UseCustomEditorApi = Symbol('USE_CUSTOM_EDITOR');
114+
export const UseNativeEditorApi = Symbol('USE_NATIVEEDITOR');
114115
export const UseProposedApi = Symbol('USE_VSC_PROPOSED_API');
115116

116117
export * from '../constants';

src/client/datascience/interactive-ipynb/nativeEditorStorage.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { CellState, ICell, IJupyterExecution, IJupyterKernelSpec, INotebookModel
2121
import detectIndent = require('detect-indent');
2222
// tslint:disable-next-line:no-require-imports no-var-requires
2323
import cloneDeep = require('lodash/cloneDeep');
24+
import { UseNativeEditorApi } from '../../common/constants';
2425
import { isFileNotFoundError } from '../../common/platform/errors';
2526
import { sendTelemetryEvent } from '../../telemetry';
2627
import { pruneCell } from '../common';
@@ -106,6 +107,7 @@ export class NativeEditorNotebookModel implements INotebookModel {
106107
private _id = uuid();
107108

108109
constructor(
110+
private readonly useNativeEditorApi: boolean,
109111
file: Uri,
110112
cells: ICell[],
111113
json: Partial<nbformat.INotebookContent> = {},
@@ -131,6 +133,7 @@ export class NativeEditorNotebookModel implements INotebookModel {
131133
}
132134
public clone(file: Uri) {
133135
return new NativeEditorNotebookModel(
136+
this.useNativeEditorApi,
134137
file,
135138
cloneDeep(this._state.cells),
136139
cloneDeep(this._state.notebookJson),
@@ -213,11 +216,19 @@ export class NativeEditorNotebookModel implements INotebookModel {
213216
break;
214217
case 'save':
215218
this._state.saveChangeCount = this._state.changeCount;
219+
// Trigger event.
220+
if (this.useNativeEditorApi) {
221+
changed = true;
222+
}
216223
break;
217224
case 'saveAs':
218225
this._state.saveChangeCount = this._state.changeCount;
219226
this._state.changeCount = this._state.saveChangeCount = 0;
220227
this._state.file = change.target;
228+
// Trigger event.
229+
if (this.useNativeEditorApi) {
230+
changed = true;
231+
}
221232
break;
222233
default:
223234
break;
@@ -361,6 +372,11 @@ export class NativeEditorNotebookModel implements INotebookModel {
361372
}
362373

363374
private clearOutputs(): boolean {
375+
if (this.useNativeEditorApi) {
376+
// Do not create new cells when using native editor.
377+
// We'll update the cells in place (cuz undo/redo is handled by VS Code).
378+
return true;
379+
}
364380
const newCells = this.cells.map((c) =>
365381
this.asCell({ ...c, data: { ...c.data, execution_count: null, outputs: [] } })
366382
);
@@ -496,7 +512,8 @@ export class NativeEditorStorage implements INotebookStorage {
496512
@inject(ICryptoUtils) private crypto: ICryptoUtils,
497513
@inject(IExtensionContext) private context: IExtensionContext,
498514
@inject(IMemento) @named(GLOBAL_MEMENTO) private globalStorage: Memento,
499-
@inject(IMemento) @named(WORKSPACE_MEMENTO) private localStorage: Memento
515+
@inject(IMemento) @named(WORKSPACE_MEMENTO) private localStorage: Memento,
516+
@inject(UseNativeEditorApi) private readonly useNativeEditorApi: boolean
500517
) {}
501518
private static isUntitledFile(file: Uri) {
502519
return isUntitledFile(file);
@@ -669,7 +686,7 @@ export class NativeEditorStorage implements INotebookStorage {
669686
} catch (ex) {
670687
// May not exist at this time. Should always have a single cell though
671688
traceError(`Failed to load notebook file ${file.toString()}`, ex);
672-
return new NativeEditorNotebookModel(file, []);
689+
return new NativeEditorNotebookModel(this.useNativeEditorApi, file, []);
673690
}
674691
}
675692

@@ -720,7 +737,15 @@ export class NativeEditorStorage implements INotebookStorage {
720737
remapped.splice(0, 0, this.createEmptyCell(uuid()));
721738
}
722739
const pythonNumber = json ? await this.extractPythonMainVersion(json) : 3;
723-
return new NativeEditorNotebookModel(file, remapped, json, indentAmount, pythonNumber, isInitiallyDirty);
740+
return new NativeEditorNotebookModel(
741+
this.useNativeEditorApi,
742+
file,
743+
remapped,
744+
json,
745+
indentAmount,
746+
pythonNumber,
747+
isInitiallyDirty
748+
);
724749
}
725750

726751
private getStorageKey(file: Uri): string {

src/client/datascience/notebook/contentProvider.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,6 @@ export class NotebookContentProvider implements VSCodeNotebookContentProvider {
4444
}
4545
if (model.isUntitled) {
4646
await this.commandManager.executeCommand('workbench.action.files.saveAs', document.uri);
47-
// tslint:disable-next-line: no-suspicious-comment
48-
//TODO: VSC doesn't handle this well.
49-
// What if user doesn't save it.
50-
if (model.isUntitled) {
51-
throw new Error('Not saved');
52-
}
5347
} else {
5448
await this.notebookStorage.save(model, cancellation);
5549
}

src/client/datascience/notebook/executionService.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { NotebookCell, NotebookDocument } from 'vscode-proposed';
1111
import { ICommandManager } from '../../common/application/types';
1212
import { wrapCancellationTokens } from '../../common/cancellation';
1313
import '../../common/extensions';
14+
import { IDisposable } from '../../common/types';
1415
import { createDeferred } from '../../common/utils/async';
1516
import { noop } from '../../common/utils/misc';
1617
import { StopWatch } from '../../common/utils/stopWatch';
@@ -152,6 +153,7 @@ export class NotebookExecutionService implements INotebookExecutionService {
152153
cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running;
153154

154155
let subscription: Subscription | undefined;
156+
let modelClearedEventHandler: IDisposable | undefined;
155157
try {
156158
nb.clear(cell.uri.fsPath); // NOSONAR
157159
const observable = nb.executeObservable(
@@ -163,6 +165,15 @@ export class NotebookExecutionService implements INotebookExecutionService {
163165
);
164166
subscription = observable?.subscribe(
165167
(cells) => {
168+
if (!modelClearedEventHandler) {
169+
modelClearedEventHandler = model.changed((e) => {
170+
if (e.kind === 'clear') {
171+
// If cell output has been cleared, then clear the output in the observed executable cell.
172+
// Else if user clears output while execuitng a cell, we add it back.
173+
cells.forEach((c) => (c.data.outputs = []));
174+
}
175+
});
176+
}
166177
const rawCellOutput = cells
167178
.filter((item) => item.id === cell.uri.fsPath)
168179
.flatMap((item) => (item.data.outputs as unknown) as nbformat.IOutput[])
@@ -198,7 +209,6 @@ export class NotebookExecutionService implements INotebookExecutionService {
198209
const notebookCellModel = findMappedNotebookCellModel(document, cell, model.cells);
199210
updateCellExecutionTimes(
200211
notebookCellModel,
201-
model,
202212
cell.metadata.runStartTime,
203213
cell.metadata.lastRunDuration
204214
);
@@ -221,6 +231,7 @@ export class NotebookExecutionService implements INotebookExecutionService {
221231
updateCellWithErrorStatus(cell, ex);
222232
this.errorHandler.handleError(ex).ignoreErrors();
223233
} finally {
234+
modelClearedEventHandler?.dispose(); // NOSONAR
224235
subscription?.unsubscribe(); // NOSONAR
225236
// Ensure we remove the cancellation.
226237
const cancellations = this.pendingExecutionCancellations.get(document.uri.fsPath);

src/client/datascience/notebook/helpers/cellUpdateHelpers.ts

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import {
1818
} from '../../../common/application/types';
1919
import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../common/constants';
2020
import { traceError } from '../../../logging';
21-
import { ICell, INotebookModel } from '../../types';
21+
import { INotebookModel } from '../../types';
2222
import { findMappedNotebookCellModel } from './cellMappers';
23-
import { createCellFromVSCNotebookCell, createVSCCellOutputsFromOutputs } from './helpers';
23+
import {
24+
createCellFromVSCNotebookCell,
25+
createVSCCellOutputsFromOutputs,
26+
updateVSCNotebookCellMetadata
27+
} from './helpers';
2428

2529
/**
2630
* If a VS Code cell changes, then ensure we update the corresponding cell in our INotebookModel.
@@ -32,9 +36,7 @@ export function updateCellModelWithChangesToVSCCell(
3236
) {
3337
switch (change.type) {
3438
case 'changeCellOutputs':
35-
// We're not interested in changes to cell output as this happens as a result of us pushing changes to the notebook.
36-
// I.e. cell output is already in our INotebookModel.
37-
return;
39+
return clearCellOutput(change, model);
3840
case 'changeCellLanguage':
3941
return changeCellLanguage(change, model);
4042
case 'changeCells':
@@ -45,6 +47,31 @@ export function updateCellModelWithChangesToVSCCell(
4547
}
4648
}
4749

50+
/**
51+
* We're not interested in changes to cell output as this happens as a result of us pushing changes to the notebook.
52+
* I.e. cell output is already in our INotebookModel.
53+
* However we are interested in cell output being cleared (when user clears output).
54+
*/
55+
function clearCellOutput(change: NotebookCellOutputsChangeEvent, model: INotebookModel) {
56+
if (!change.cells.every((cell) => cell.outputs.length === 0)) {
57+
return;
58+
}
59+
60+
// If a cell has been cleared, then clear the corresponding ICell (cell in INotebookModel).
61+
change.cells.forEach((vscCell) => {
62+
const cell = findMappedNotebookCellModel(change.document, vscCell, model.cells);
63+
cell.data.outputs = [];
64+
updateVSCNotebookCellMetadata(vscCell.metadata, cell);
65+
model.update({
66+
source: 'user',
67+
kind: 'clear',
68+
oldDirty: model.isDirty,
69+
newDirty: true,
70+
oldCells: [cell]
71+
});
72+
});
73+
}
74+
4875
function changeCellLanguage(change: NotebookCellLanguageChangeEvent, model: INotebookModel) {
4976
const cellModel = findMappedNotebookCellModel(change.document, change.cell, model.cells);
5077

@@ -57,26 +84,16 @@ function changeCellLanguage(change: NotebookCellLanguageChangeEvent, model: INot
5784
return;
5885
}
5986

60-
const newCellData = createCellFrom(cellModel.data, change.language === MARKDOWN_LANGUAGE ? 'markdown' : 'code');
61-
const newCell: ICell = {
62-
...cellModel,
63-
data: newCellData
64-
};
65-
66-
model.update({
67-
source: 'user',
68-
kind: 'modify',
69-
newCells: [newCell],
70-
oldCells: [cellModel],
71-
oldDirty: model.isDirty,
72-
newDirty: true
73-
});
74-
87+
const cellData = createCellFrom(cellModel.data, change.language === MARKDOWN_LANGUAGE ? 'markdown' : 'code');
7588
// tslint:disable-next-line: no-any
76-
change.cell.outputs = createVSCCellOutputsFromOutputs(newCellData.outputs as any);
89+
change.cell.outputs = createVSCCellOutputsFromOutputs(cellData.outputs as any);
7790
change.cell.metadata.executionOrder = undefined;
7891
change.cell.metadata.hasExecutionOrder = change.language !== MARKDOWN_LANGUAGE; // Do not check for Python, to support other languages
7992
change.cell.metadata.runnable = change.language !== MARKDOWN_LANGUAGE; // Do not check for Python, to support other languages
93+
94+
// Create a new cell & replace old one.
95+
const oldCellIndex = model.cells.indexOf(cellModel);
96+
model.cells[oldCellIndex] = createCellFromVSCNotebookCell(change.document, change.cell, model);
8097
}
8198

8299
function handleChangesToCells(change: NotebookCellsChangeEvent, model: INotebookModel) {
@@ -135,42 +152,22 @@ function handleCellMove(change: NotebookCellsChangeEvent, model: INotebookModel)
135152
const cellToSwap = findMappedNotebookCellModel(change.document, insertChange.items[0]!, model.cells);
136153
const cellToSwapWith = model.cells[insertChange.start];
137154
assert.notEqual(cellToSwap, cellToSwapWith, 'Cannot swap cell with the same cell');
138-
model.update({
139-
source: 'user',
140-
kind: 'swap',
141-
oldDirty: model.isDirty,
142-
newDirty: true,
143-
firstCellId: cellToSwap.id,
144-
secondCellId: cellToSwapWith.id
145-
});
155+
156+
const indexOfCellToSwap = model.cells.indexOf(cellToSwap);
157+
model.cells[insertChange.start] = cellToSwap;
158+
model.cells[indexOfCellToSwap] = cellToSwapWith;
146159
}
147160
function handleCellInsertion(change: NotebookCellsChangeEvent, model: INotebookModel) {
148161
assert.equal(change.changes.length, 1, 'When inserting cells we must have only 1 change');
149162
assert.equal(change.changes[0].items.length, 1, 'Insertion of more than 1 cell is not supported');
150163
const insertChange = change.changes[0];
151164
const cell = change.changes[0].items[0];
152165
const newCell = createCellFromVSCNotebookCell(change.document, cell, model);
153-
154-
model.update({
155-
source: 'user',
156-
kind: 'insert',
157-
newDirty: true,
158-
oldDirty: model.isDirty,
159-
index: insertChange.start,
160-
cell: newCell
161-
});
166+
model.cells.splice(insertChange.start, 0, newCell);
162167
}
163168
function handleCellDelete(change: NotebookCellsChangeEvent, model: INotebookModel) {
164169
assert.equal(change.changes.length, 1, 'When deleting cells we must have only 1 change');
165170
const deletionChange = change.changes[0];
166171
assert.equal(deletionChange.deletedCount, 1, 'Deleting more than one cell is not supported');
167-
const cellToDelete = model.cells[deletionChange.start];
168-
model.update({
169-
source: 'user',
170-
kind: 'remove',
171-
oldDirty: model.isDirty,
172-
newDirty: true,
173-
cell: cellToDelete,
174-
index: deletionChange.start
175-
});
172+
model.cells.splice(deletionChange.start, 1);
176173
}

src/client/datascience/notebook/helpers/executionHelpers.ts

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { KernelMessage } from '@jupyterlab/services';
88
import { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode';
99
import { IBaseCellVSCodeMetadata } from '../../../../../types/@jupyterlab_coreutils_nbformat';
1010
import { createErrorOutput } from '../../../../datascience-ui/common/cellFactory';
11-
import { INotebookModelModifyChange } from '../../interactive-common/interactiveWindowTypes';
1211
import { ICell, INotebookModel } from '../../types';
1312
import { findMappedNotebookCell } from './cellMappers';
1413
import { createVSCCellOutputsFromOutputs, translateErrorOutput, updateVSCNotebookCellMetadata } from './helpers';
@@ -118,51 +117,23 @@ export function updateCellOutput(vscCell: NotebookCell, cell: ICell, outputs: nb
118117
/**
119118
* Store execution start and end times in ISO format for portability.
120119
*/
121-
export function updateCellExecutionTimes(
122-
notebookCellModel: ICell,
123-
model: INotebookModel,
124-
startTime?: number,
125-
duration?: number
126-
) {
120+
export function updateCellExecutionTimes(notebookCellModel: ICell, startTime?: number, duration?: number) {
127121
const startTimeISO = startTime ? new Date(startTime).toISOString() : undefined;
128122
const endTimeISO = duration && startTime ? new Date(startTime + duration).toISOString() : undefined;
129-
updateCellMetadata(
130-
notebookCellModel,
131-
{
132-
end_execution_time: endTimeISO,
133-
start_execution_time: startTimeISO
134-
},
135-
model
136-
);
123+
updateCellMetadata(notebookCellModel, {
124+
end_execution_time: endTimeISO,
125+
start_execution_time: startTimeISO
126+
});
137127
}
138128

139-
export function updateCellMetadata(
140-
notebookCellModel: ICell,
141-
metadata: Partial<IBaseCellVSCodeMetadata>,
142-
model: INotebookModel
143-
) {
129+
export function updateCellMetadata(notebookCellModel: ICell, metadata: Partial<IBaseCellVSCodeMetadata>) {
144130
const originalVscodeMetadata: IBaseCellVSCodeMetadata = notebookCellModel.data.metadata.vscode || {};
145131
// Update our model with the new metadata stored in jupyter.
146-
const newCell: ICell = {
147-
...notebookCellModel,
148-
data: {
149-
...notebookCellModel.data,
150-
metadata: {
151-
...notebookCellModel.data.metadata,
152-
vscode: {
153-
...originalVscodeMetadata,
154-
...metadata
155-
}
156-
}
132+
notebookCellModel.data.metadata = {
133+
...notebookCellModel.data.metadata,
134+
vscode: {
135+
...originalVscodeMetadata,
136+
...metadata
157137
}
158138
};
159-
const updateCell: INotebookModelModifyChange = {
160-
kind: 'modify',
161-
newCells: [newCell],
162-
oldCells: [notebookCellModel],
163-
newDirty: true,
164-
oldDirty: model.isDirty === true,
165-
source: 'user'
166-
};
167-
model.update(updateCell);
168139
}

src/client/datascience/serviceRegistry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33
'use strict';
44
import { IExtensionSingleActivationService } from '../activation/types';
5-
import { UseCustomEditorApi } from '../common/constants';
5+
import { UseCustomEditorApi, UseNativeEditorApi } from '../common/constants';
66
import { NotebookEditorSupport } from '../common/experiments/groups';
77
import { StartPage } from '../common/startPage/startPage';
88
import { IStartPage } from '../common/startPage/types';
@@ -164,6 +164,7 @@ export function registerTypes(serviceManager: IServiceManager) {
164164
const inCustomEditorApiExperiment = experiments.inExperiment(NotebookEditorSupport.customEditorExperiment);
165165
const usingCustomEditor = inCustomEditorApiExperiment;
166166
serviceManager.addSingletonInstance<boolean>(UseCustomEditorApi, usingCustomEditor);
167+
serviceManager.addSingletonInstance<boolean>(UseNativeEditorApi, useVSCodeNotebookAPI);
167168

168169
// This condition is temporary.
169170
const notebookEditorProvider = useVSCodeNotebookAPI ? NotebookEditorProvider : usingCustomEditor ? NativeEditorProvider : NativeEditorProviderOld;

0 commit comments

Comments
 (0)