Skip to content

Commit 9ed71a5

Browse files
authored
Implement undo support for the custom editor (#9946)
* Partially working undo/redo * Concept created using update message/command * Put the storage object in the global map * Look for more data during undo/redo * New idea with reverse edit and disable undo/redo in monaco * Potentially working with tracking edits * Fix completions for interactive * Fix unit tests * First working functional test of undo * More testing of undo * More tests * Close suggestions on undo * Fix other functional tests * Fix undo misses * Fix focus changes on undo * Fixup after merge * Add news entry * Put back launch.json * Get rid of asyncness of sendCellsToWebView * Add a comment about why we remove \r * Prevent swapCells from doing anything if same cell * Fix changed event for clear * Actually remove all state from editor class. Try to fix undo on save
1 parent ba90733 commit 9ed71a5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1700
-981
lines changed

news/1 Enhancements/9821.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add undo/redo support to notebooks.

src/client/common/application/commands.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
import { CancellationToken, Position, TextDocument, Uri } from 'vscode';
77
import { Commands as LSCommands } from '../../activation/languageServer/constants';
88
import { Commands as DSCommands } from '../../datascience/constants';
9-
import { IEditCell, IInsertCell, ISwapCells } from '../../datascience/interactive-common/interactiveWindowTypes';
10-
import { LiveKernelModel } from '../../datascience/jupyter/kernels/types';
11-
import { ICell, IJupyterKernelSpec, INotebook } from '../../datascience/types';
12-
import { PythonInterpreter } from '../../interpreter/contracts';
9+
import { INotebook } from '../../datascience/types';
1310
import { CommandSource } from '../../testing/common/constants';
1411
import { TestFunction, TestsToRun } from '../../testing/common/types';
1512
import { TestDataItem, TestWorkspaceFolder } from '../../testing/types';
@@ -148,12 +145,4 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
148145
[DSCommands.ScrollToCell]: [string, string];
149146
[DSCommands.ViewJupyterOutput]: [];
150147
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined];
151-
[DSCommands.NotebookStorage_DeleteAllCells]: [Uri];
152-
[DSCommands.NotebookStorage_ModifyCells]: [Uri, ICell[]];
153-
[DSCommands.NotebookStorage_EditCell]: [Uri, IEditCell];
154-
[DSCommands.NotebookStorage_InsertCell]: [Uri, IInsertCell];
155-
[DSCommands.NotebookStorage_RemoveCell]: [Uri, string];
156-
[DSCommands.NotebookStorage_SwapCells]: [Uri, ISwapCells];
157-
[DSCommands.NotebookStorage_ClearCellOutputs]: [Uri];
158-
[DSCommands.NotebookStorage_UpdateVersion]: [Uri, PythonInterpreter | undefined, IJupyterKernelSpec | LiveKernelModel | undefined];
159148
}

src/client/datascience/constants.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ export namespace Commands {
6262
export const ScrollToCell = 'python.datascience.scrolltocell';
6363
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
6464
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';
65-
66-
// Make sure to put these into the package .json
67-
export const NotebookStorage_DeleteAllCells = 'python.datascience.notebook.deleteall';
68-
export const NotebookStorage_ModifyCells = 'python.datascience.notebook.modifycells';
69-
export const NotebookStorage_EditCell = 'python.datascience.notebook.editcell';
70-
export const NotebookStorage_InsertCell = 'python.datascience.notebook.insertcell';
71-
export const NotebookStorage_RemoveCell = 'python.datascience.notebook.removecell';
72-
export const NotebookStorage_SwapCells = 'python.datascience.notebook.swapcells';
73-
export const NotebookStorage_ClearCellOutputs = 'python.datascience.notebook.clearoutputs';
74-
export const NotebookStorage_UpdateVersion = 'python.datascience.notebook.updateversion';
7565
}
7666

7767
export namespace CodeLensCommands {

src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
'use strict';
44
import '../../../common/extensions';
55

6-
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
76
import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode';
87
import * as vscodeLanguageClient from 'vscode-languageclient';
98

109
import { PYTHON_LANGUAGE } from '../../../common/constants';
1110
import { Identifiers } from '../../constants';
11+
import { IEditorContentChange } from '../interactiveWindowTypes';
1212
import { DefaultWordPattern, ensureValidWordDefinition, getWordAtText, regExpLeadsToEndlessLoop } from './wordHelper';
1313

1414
class IntellisenseLine implements TextLine {
@@ -201,54 +201,57 @@ export class IntellisenseDocument implements TextDocument {
201201
}
202202

203203
public loadAllCells(cells: { code: string; id: string }[]): TextDocumentContentChangeEvent[] {
204-
let changes: TextDocumentContentChangeEvent[] = [];
205204
if (!this.inEditMode) {
206205
this.inEditMode = true;
207-
this._version += 1;
206+
return this.reloadCells(cells);
207+
}
208+
return [];
209+
}
208210

209-
// Normalize all of the cells, removing \r and separating each
210-
// with a newline
211-
const normalized = cells.map(c => {
212-
return {
213-
id: c.id,
214-
code: `${c.code.replace(/\r/g, '')}\n`
215-
};
216-
});
211+
public reloadCells(cells: { code: string; id: string }[]): TextDocumentContentChangeEvent[] {
212+
this._version += 1;
213+
214+
// Normalize all of the cells, removing \r and separating each
215+
// with a newline
216+
const normalized = cells.map(c => {
217+
return {
218+
id: c.id,
219+
code: `${c.code.replace(/\r/g, '')}\n`
220+
};
221+
});
217222

218-
// Contents are easy, just load all of the code in a row
219-
this._contents = normalized
220-
.map(c => c.code)
221-
.reduce((p, c) => {
222-
return `${p}${c}`;
223-
});
224-
225-
// Cell ranges are slightly more complicated
226-
let prev: number = 0;
227-
this._cellRanges = normalized.map(c => {
228-
const result = {
229-
id: c.id,
230-
start: prev,
231-
fullEnd: prev + c.code.length,
232-
currentEnd: prev + c.code.length
233-
};
234-
prev += c.code.length;
235-
return result;
223+
// Contents are easy, just load all of the code in a row
224+
this._contents = normalized
225+
.map(c => c.code)
226+
.reduce((p, c) => {
227+
return `${p}${c}`;
236228
});
237229

238-
// Then create the lines.
239-
this._lines = this.createLines();
230+
// Cell ranges are slightly more complicated
231+
let prev: number = 0;
232+
this._cellRanges = normalized.map(c => {
233+
const result = {
234+
id: c.id,
235+
start: prev,
236+
fullEnd: prev + c.code.length,
237+
currentEnd: prev + c.code.length
238+
};
239+
prev += c.code.length;
240+
return result;
241+
});
240242

241-
// Return our changes
242-
changes = [
243-
{
244-
range: this.createSerializableRange(new Position(0, 0), new Position(0, 0)),
245-
rangeOffset: 0,
246-
rangeLength: 0, // Adds are always zero
247-
text: this._contents
248-
}
249-
];
250-
}
251-
return changes;
243+
// Then create the lines.
244+
this._lines = this.createLines();
245+
246+
// Return our changes
247+
return [
248+
{
249+
range: this.createSerializableRange(new Position(0, 0), new Position(0, 0)),
250+
rangeOffset: 0,
251+
rangeLength: 0, // Adds are always zero
252+
text: this._contents
253+
}
254+
];
252255
}
253256

254257
public addCell(fullCode: string, currentCode: string, id: string): TextDocumentContentChangeEvent[] {
@@ -293,16 +296,33 @@ export class IntellisenseDocument implements TextDocument {
293296
];
294297
}
295298

296-
public insertCell(id: string, code: string, codeCellAbove: string | undefined): TextDocumentContentChangeEvent[] {
299+
public reloadCell(id: string, code: string): TextDocumentContentChangeEvent[] {
300+
this._version += 1;
301+
302+
// Make sure to put a newline between this code and the next code
303+
const newCode = `${code.replace(/\r/g, '')}\n`;
304+
305+
// Figure where this goes
306+
const index = this._cellRanges.findIndex(r => r.id === id);
307+
if (index >= 0) {
308+
const start = this.positionAt(this._cellRanges[index].start);
309+
const end = this.positionAt(this._cellRanges[index].currentEnd);
310+
return this.removeRange(newCode, start, end, index);
311+
}
312+
313+
return [];
314+
}
315+
316+
public insertCell(id: string, code: string, codeCellAboveOrIndex: string | undefined | number): TextDocumentContentChangeEvent[] {
297317
// This should only happen once for each cell.
298318
this._version += 1;
299319

300320
// Make sure to put a newline between this code and the next code
301321
const newCode = `${code.replace(/\r/g, '')}\n`;
302322

303323
// Figure where this goes
304-
const aboveIndex = this._cellRanges.findIndex(r => r.id === codeCellAbove);
305-
const insertIndex = aboveIndex + 1;
324+
const aboveIndex = this._cellRanges.findIndex(r => r.id === codeCellAboveOrIndex);
325+
const insertIndex = typeof codeCellAboveOrIndex === 'number' ? codeCellAboveOrIndex : aboveIndex + 1;
306326

307327
// Compute where we start from.
308328
const fromOffset = insertIndex < this._cellRanges.length ? this._cellRanges[insertIndex].start : this._contents.length;
@@ -356,7 +376,7 @@ export class IntellisenseDocument implements TextDocument {
356376
return [];
357377
}
358378

359-
public edit(editorChanges: monacoEditor.editor.IModelContentChange[], id: string): TextDocumentContentChangeEvent[] {
379+
public editCell(editorChanges: IEditorContentChange[], id: string): TextDocumentContentChangeEvent[] {
360380
this._version += 1;
361381

362382
// Convert the range to local (and remove 1 based)

0 commit comments

Comments
 (0)