Skip to content

Commit 6c68514

Browse files
committed
Fix microsoft#100329. Content providers can contribute to undo/redo stack of a notebook document.
1 parent ba6892d commit 6c68514

9 files changed

Lines changed: 300 additions & 39 deletions

File tree

extensions/vscode-notebook-tests/src/notebook.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,70 @@ suite('notebook undo redo', () => {
568568
await vscode.commands.executeCommand('workbench.action.files.save');
569569
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
570570
});
571+
572+
test('execute and then undo redo', async function () {
573+
const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb'));
574+
await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest');
575+
576+
const cellsChangeEvent = getEventOncePromise<vscode.NotebookCellsChangeEvent>(vscode.notebook.onDidChangeNotebookCells);
577+
await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow');
578+
const cellChangeEventRet = await cellsChangeEvent;
579+
assert.equal(cellChangeEventRet.document, vscode.notebook.activeNotebookEditor?.document);
580+
assert.equal(cellChangeEventRet.changes.length, 1);
581+
assert.deepEqual(cellChangeEventRet.changes[0], {
582+
start: 1,
583+
deletedCount: 0,
584+
deletedItems: [],
585+
items: [
586+
vscode.notebook.activeNotebookEditor!.document.cells[1]
587+
]
588+
});
589+
590+
const secondCell = vscode.notebook.activeNotebookEditor!.document.cells[1];
591+
592+
const moveCellEvent = getEventOncePromise<vscode.NotebookCellsChangeEvent>(vscode.notebook.onDidChangeNotebookCells);
593+
await vscode.commands.executeCommand('notebook.cell.moveUp');
594+
const moveCellEventRet = await moveCellEvent;
595+
assert.deepEqual(moveCellEventRet, {
596+
document: vscode.notebook.activeNotebookEditor!.document,
597+
changes: [
598+
{
599+
start: 1,
600+
deletedCount: 1,
601+
deletedItems: [secondCell],
602+
items: []
603+
},
604+
{
605+
start: 0,
606+
deletedCount: 0,
607+
deletedItems: [],
608+
items: [vscode.notebook.activeNotebookEditor?.document.cells[0]]
609+
}
610+
]
611+
});
612+
613+
const cellOutputChange = getEventOncePromise<vscode.NotebookCellOutputsChangeEvent>(vscode.notebook.onDidChangeCellOutputs);
614+
await vscode.commands.executeCommand('notebook.cell.execute');
615+
const cellOutputsAddedRet = await cellOutputChange;
616+
assert.deepEqual(cellOutputsAddedRet, {
617+
document: vscode.notebook.activeNotebookEditor!.document,
618+
cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]]
619+
});
620+
assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 1);
621+
622+
const cellOutputClear = getEventOncePromise<vscode.NotebookCellOutputsChangeEvent>(vscode.notebook.onDidChangeCellOutputs);
623+
await vscode.commands.executeCommand('notebook.undo');
624+
const cellOutputsCleardRet = await cellOutputClear;
625+
assert.deepEqual(cellOutputsCleardRet, {
626+
document: vscode.notebook.activeNotebookEditor!.document,
627+
cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]]
628+
});
629+
assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 0);
630+
631+
await vscode.commands.executeCommand('workbench.action.files.save');
632+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
633+
});
634+
571635
});
572636

573637
suite('notebook working copy', () => {

extensions/vscode-notebook-tests/src/notebookTestMain.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import { smokeTestActivate } from './notebookSmokeTestMain';
1010
export function activate(context: vscode.ExtensionContext): any {
1111
smokeTestActivate(context);
1212

13+
const _onDidChangeNotebook = new vscode.EventEmitter<vscode.NotebookDocumentEditEvent | vscode.NotebookDocumentContentChangeEvent>();
14+
context.subscriptions.push(_onDidChangeNotebook);
1315
context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', {
14-
onDidChangeNotebook: new vscode.EventEmitter<vscode.NotebookDocumentEditEvent>().event,
16+
onDidChangeNotebook: _onDidChangeNotebook.event,
1517
openNotebook: async (_resource: vscode.Uri) => {
1618
if (_resource.path.endsWith('empty.vsctestnb')) {
1719
return {
@@ -71,13 +73,13 @@ export function activate(context: vscode.ExtensionContext): any {
7173
}];
7274
return;
7375
},
74-
executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => {
75-
if (!_cell) {
76-
_cell = _document.cells[0];
76+
executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => {
77+
if (!cell) {
78+
cell = document.cells[0];
7779
}
7880

79-
if (_document.uri.path.endsWith('customRenderer.vsctestnb')) {
80-
_cell.outputs = [{
81+
if (document.uri.path.endsWith('customRenderer.vsctestnb')) {
82+
cell.outputs = [{
8183
outputKind: vscode.CellOutputKind.Rich,
8284
data: {
8385
'text/custom': 'test'
@@ -87,13 +89,29 @@ export function activate(context: vscode.ExtensionContext): any {
8789
return;
8890
}
8991

90-
_cell.outputs = [{
92+
const previousOutputs = cell.outputs;
93+
const newOutputs: vscode.CellOutput[] = [{
9194
outputKind: vscode.CellOutputKind.Rich,
9295
data: {
9396
'text/plain': ['my output']
9497
}
9598
}];
9699

100+
cell.outputs = newOutputs;
101+
102+
_onDidChangeNotebook.fire({
103+
document: document,
104+
undo: () => {
105+
if (cell) {
106+
cell.outputs = previousOutputs;
107+
}
108+
},
109+
redo: () => {
110+
if (cell) {
111+
cell.outputs = newOutputs;
112+
}
113+
}
114+
});
97115
return;
98116
}
99117
}));

src/vs/vscode.proposed.d.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1604,12 +1604,45 @@ declare module 'vscode' {
16041604
readonly metadata: NotebookDocumentMetadata;
16051605
}
16061606

1607+
interface NotebookDocumentContentChangeEvent {
1608+
1609+
/**
1610+
* The document that the edit is for.
1611+
*/
1612+
readonly document: NotebookDocument;
1613+
}
1614+
16071615
interface NotebookDocumentEditEvent {
16081616

16091617
/**
16101618
* The document that the edit is for.
16111619
*/
16121620
readonly document: NotebookDocument;
1621+
1622+
/**
1623+
* Undo the edit operation.
1624+
*
1625+
* This is invoked by VS Code when the user undoes this edit. To implement `undo`, your
1626+
* extension should restore the document and editor to the state they were in just before this
1627+
* edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`.
1628+
*/
1629+
undo(): Thenable<void> | void;
1630+
1631+
/**
1632+
* Redo the edit operation.
1633+
*
1634+
* This is invoked by VS Code when the user redoes this edit. To implement `redo`, your
1635+
* extension should restore the document and editor to the state they were in just after this
1636+
* edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`.
1637+
*/
1638+
redo(): Thenable<void> | void;
1639+
1640+
/**
1641+
* Display name describing the edit.
1642+
*
1643+
* This will be shown to users in the UI for undo/redo operations.
1644+
*/
1645+
readonly label?: string;
16131646
}
16141647

16151648
interface NotebookDocumentBackup {
@@ -1660,7 +1693,7 @@ declare module 'vscode' {
16601693
}): Promise<void>;
16611694
saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
16621695
saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
1663-
readonly onDidChangeNotebook: Event<NotebookDocumentEditEvent>;
1696+
readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent>;
16641697
backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise<NotebookDocumentBackup>;
16651698

16661699
kernel?: NotebookKernel;

src/vs/workbench/api/browser/mainThreadNotebook.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as nls from 'vs/nls';
67
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
78
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol';
89
import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
@@ -17,6 +18,8 @@ import { CancellationToken } from 'vs/base/common/cancellation';
1718
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
1819
import { IRelativePattern } from 'vs/base/common/glob';
1920
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
21+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
22+
import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
2023

2124
export class MainThreadNotebookDocument extends Disposable {
2225
private _textModel: NotebookTextModel;
@@ -31,9 +34,12 @@ export class MainThreadNotebookDocument extends Disposable {
3134
public viewType: string,
3235
public supportBackup: boolean,
3336
public uri: URI,
34-
readonly notebookService: INotebookService
37+
@INotebookService readonly notebookService: INotebookService,
38+
@IUndoRedoService readonly undoRedoService: IUndoRedoService
39+
3540
) {
3641
super();
42+
3743
this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri);
3844
this._register(this._textModel.onDidModelChangeProxy(e => {
3945
this._proxy.$acceptModelChanged(this.uri, e);
@@ -54,6 +60,22 @@ export class MainThreadNotebookDocument extends Disposable {
5460
await this.notebookService.transformSpliceOutputs(this.textModel, splices);
5561
this._textModel.$spliceNotebookCellOutputs(cellHandle, splices);
5662
}
63+
64+
handleEdit(editId: number, label: string | undefined): void {
65+
this.undoRedoService.pushElement({
66+
type: UndoRedoElementType.Resource,
67+
resource: this._textModel.uri,
68+
label: label ?? nls.localize('defaultEditLabel', "Edit"),
69+
undo: async () => {
70+
await this._proxy.$undoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty);
71+
},
72+
redo: async () => {
73+
await this._proxy.$redoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty);
74+
},
75+
});
76+
this._textModel.setDirty(true);
77+
}
78+
5779
dispose() {
5880
this._textModel.dispose();
5981
super.dispose();
@@ -179,7 +201,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
179201
@INotebookService private _notebookService: INotebookService,
180202
@IConfigurationService private readonly configurationService: IConfigurationService,
181203
@IEditorService private readonly editorService: IEditorService,
182-
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
204+
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
205+
@IInstantiationService private readonly _instantiationService: IInstantiationService
183206

184207
) {
185208
super();
@@ -388,7 +411,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
388411
}
389412

390413
async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernel: INotebookKernelInfoDto | undefined): Promise<void> {
391-
let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService);
414+
let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService, this._instantiationService);
392415
this._notebookProviders.set(viewType, controller);
393416
this._notebookService.registerNotebookController(viewType, extension, controller);
394417
return;
@@ -467,6 +490,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
467490

468491
return false;
469492
}
493+
494+
$onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void {
495+
let controller = this._notebookProviders.get(viewType);
496+
controller?.handleEdit(resource, editId, label);
497+
}
498+
499+
$onContentChange(resource: UriComponents, viewType: string): void {
500+
let controller = this._notebookProviders.get(viewType);
501+
controller?.handleNotebookChange(resource);
502+
}
470503
}
471504

472505
export class MainThreadNotebookController implements IMainNotebookController {
@@ -480,6 +513,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
480513
private _supportBackup: boolean,
481514
readonly kernel: INotebookKernelInfoDto | undefined,
482515
readonly notebookService: INotebookService,
516+
readonly _instantiationService: IInstantiationService
483517

484518
) {
485519
}
@@ -504,7 +538,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
504538
return mainthreadNotebook.textModel;
505539
}
506540

507-
let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri, this.notebookService);
541+
let document = this._instantiationService.createInstance(MainThreadNotebookDocument, this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri);
508542
this._mapping.set(document.uri.toString(), document);
509543

510544
if (backup) {
@@ -635,6 +669,11 @@ export class MainThreadNotebookController implements IMainNotebookController {
635669
document?.textModel.handleUnknownChange();
636670
}
637671

672+
handleEdit(resource: UriComponents, editId: number, label: string | undefined): void {
673+
let document = this._mapping.get(URI.from(resource).toString());
674+
document?.handleEdit(editId, label);
675+
}
676+
638677
updateLanguages(resource: UriComponents, languages: string[]) {
639678
let document = this._mapping.get(URI.from(resource).toString());
640679
document?.textModel.updateLanguages(languages);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,9 @@ export interface MainThreadNotebookShape extends IDisposable {
710710
$updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise<void>;
711711
$spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>;
712712
$postMessage(handle: number, value: any): Promise<boolean>;
713+
714+
$onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void;
715+
$onContentChange(resource: UriComponents, viewType: string): void;
713716
}
714717

715718
export interface MainThreadUrlsShape extends IDisposable {
@@ -1596,6 +1599,9 @@ export interface ExtHostNotebookShape {
15961599
$acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void;
15971600
$acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void;
15981601
$acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise<void>;
1602+
$undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise<void>;
1603+
$redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise<void>;
1604+
15991605
}
16001606

16011607
export interface ExtHostStorageShape {

0 commit comments

Comments
 (0)