Skip to content

Commit cc937a3

Browse files
committed
Undo/Redo for split/join cells. Fix microsoft#99158.
1 parent 9007dfc commit cc937a3

5 files changed

Lines changed: 460 additions & 183 deletions

File tree

src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,32 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable {
510510
public getPieceTree(): PieceTreeBase {
511511
return this._pieceTree;
512512
}
513+
514+
public static _getInverseEditRange(range: Range, text: string) {
515+
let startLineNumber = range.startLineNumber;
516+
let startColumn = range.startColumn;
517+
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
518+
let resultRange: Range;
519+
520+
if (text.length > 0) {
521+
// the operation inserts something
522+
const lineCount = eolCount + 1;
523+
524+
if (lineCount === 1) {
525+
// single line insert
526+
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLineLength);
527+
} else {
528+
// multi line insert
529+
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLineLength + 1);
530+
}
531+
} else {
532+
// There is nothing to insert
533+
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);
534+
}
535+
536+
return resultRange;
537+
}
538+
513539
/**
514540
* Assumes `operations` are validated and sorted ascending
515541
*/

src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts

Lines changed: 12 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ import 'vs/css!./media/notebook';
1515
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1616
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
1717
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
18-
import { IPosition, Position } from 'vs/editor/common/core/position';
1918
import { Range } from 'vs/editor/common/core/range';
2019
import { IEditor } from 'vs/editor/common/editorCommon';
21-
import { IReadonlyTextBuffer } from 'vs/editor/common/model';
2220
import * as nls from 'vs/nls';
2321
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2422
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@@ -30,7 +28,7 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic
3028
import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor';
3129
import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor';
3230
import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
33-
import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
31+
import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
3432
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
3533
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
3634
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
@@ -867,175 +865,26 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
867865
return newCell;
868866
}
869867

870-
private pushIfAbsent(positions: IPosition[], p: IPosition) {
871-
const last = positions.length > 0 ? positions[positions.length - 1] : undefined;
872-
if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) {
873-
positions.push(p);
874-
}
875-
}
876-
877-
/**
878-
* Add split point at the beginning and the end;
879-
* Move end of line split points to the beginning of the next line;
880-
* Avoid duplicate split points
881-
*/
882-
private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null {
883-
const boundaries: IPosition[] = [];
884-
const lineCnt = textBuffer.getLineCount();
885-
const getLineLen = (lineNumber: number) => {
886-
return textBuffer.getLineLength(lineNumber);
887-
};
888-
889-
// split points need to be sorted
890-
splitPoints = splitPoints.sort((l, r) => {
891-
const lineDiff = l.lineNumber - r.lineNumber;
892-
const columnDiff = l.column - r.column;
893-
return lineDiff !== 0 ? lineDiff : columnDiff;
894-
});
895-
896-
// eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning
897-
this.pushIfAbsent(boundaries, new Position(1, 1));
898-
899-
for (let sp of splitPoints) {
900-
if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) {
901-
sp = new Position(sp.lineNumber + 1, 1);
902-
}
903-
this.pushIfAbsent(boundaries, sp);
904-
}
905-
906-
// eat-up any split point at the beginning, i.e. we ignore the split point at the very end
907-
this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1));
908-
909-
// if we only have two then they describe the whole range and nothing needs to be split
910-
return boundaries.length > 2 ? boundaries : null;
911-
}
912-
913-
private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null {
914-
const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer);
915-
if (!rangeBoundaries) {
916-
return null;
917-
}
918-
const newLineModels: string[] = [];
919-
for (let i = 1; i < rangeBoundaries.length; i++) {
920-
const start = rangeBoundaries[i - 1];
921-
const end = rangeBoundaries[i];
922-
923-
newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column)));
924-
}
925-
926-
return newLineModels;
927-
}
928-
929868
async splitNotebookCell(cell: ICellViewModel): Promise<CellViewModel[] | null> {
930-
if (!this.notebookViewModel!.metadata.editable) {
931-
return null;
932-
}
933-
934-
if (!cell.metadata?.editable) {
935-
return null;
936-
}
937-
938-
let splitPoints = cell.getSelectionsStartPosition();
939-
if (splitPoints && splitPoints.length > 0) {
940-
await cell.resolveTextModel();
941-
942-
if (!cell.hasModel()) {
943-
return null;
944-
}
945-
946-
let newLinesContents = this.computeCellLinesContents(cell, splitPoints);
947-
if (newLinesContents) {
948-
949-
// update the contents of the first cell
950-
cell.textModel.applyEdits([
951-
{ range: cell.textModel.getFullModelRange(), text: newLinesContents[0] }
952-
], true);
953-
954-
// create new cells based on the new text models
955-
const language = cell.model.language;
956-
const kind = cell.cellKind;
957-
let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1;
958-
const newCells = [];
959-
for (let j = 1; j < newLinesContents.length; j++, insertIndex++) {
960-
newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true));
961-
}
962-
return newCells;
963-
}
964-
}
869+
const index = this.notebookViewModel!.getCellIndex(cell);
965870

966-
return null;
871+
return this.notebookViewModel!.splitNotebookCell(index);
967872
}
968873

969874
async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<ICellViewModel | null> {
970-
if (!this.notebookViewModel!.metadata.editable) {
971-
return null;
972-
}
973-
974-
if (!cell.getEvaluatedMetadata(this.viewModel!.notebookDocument.metadata).editable) {
975-
return null;
976-
}
977-
978-
if (constraint && cell.cellKind !== constraint) {
979-
return null;
980-
}
981-
982875
const index = this.notebookViewModel!.getCellIndex(cell);
983-
if (index === 0 && direction === 'above') {
984-
return null;
985-
}
986-
987-
if (index === this.notebookViewModel!.length - 1 && direction === 'below') {
988-
return null;
989-
}
990-
991-
if (direction === 'above') {
992-
const above = this.notebookViewModel!.viewCells[index - 1];
993-
if (constraint && above.cellKind !== constraint) {
994-
return null;
995-
}
876+
const ret = await this.notebookViewModel!.joinNotebookCells(index, direction, constraint);
996877

997-
if (!above.getEvaluatedMetadata(this.viewModel!.notebookDocument.metadata).editable) {
998-
return null;
999-
}
878+
if (ret) {
879+
ret.deletedCells.forEach(cell => {
880+
if (this.pendingLayouts.has(cell)) {
881+
this.pendingLayouts.get(cell)!.dispose();
882+
}
883+
});
1000884

1001-
await above.resolveTextModel();
1002-
if (!above.hasModel()) {
1003-
return null;
1004-
}
1005-
const insertContent = (cell.textModel?.getEOL() ?? '') + cell.getText();
1006-
const aboveCellLineCount = above.textModel.getLineCount();
1007-
const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount);
1008-
above.textModel.applyEdits([
1009-
{ range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent }
1010-
]);
1011-
1012-
await this.deleteNotebookCell(cell);
1013-
return above;
885+
return ret.cell;
1014886
} else {
1015-
const below = this.notebookViewModel!.viewCells[index + 1];
1016-
if (constraint && below.cellKind !== constraint) {
1017-
return null;
1018-
}
1019-
1020-
if (!below.getEvaluatedMetadata(this.viewModel!.notebookDocument.metadata).editable) {
1021-
return null;
1022-
}
1023-
1024-
await cell.resolveTextModel();
1025-
if (!cell.hasModel()) {
1026-
return null;
1027-
}
1028-
1029-
const insertContent = (cell.textModel?.getEOL() ?? '') + below.getText();
1030-
1031-
const cellLineCount = cell.textModel.getLineCount();
1032-
const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount);
1033-
cell.textModel.applyEdits([
1034-
{ range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent }
1035-
]);
1036-
1037-
await this.deleteNotebookCell(below);
1038-
return cell;
887+
return null;
1039888
}
1040889
}
1041890

src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
88
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
99
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1010
import { Range } from 'vs/editor/common/core/range';
11+
import { Selection } from 'vs/editor/common/core/selection';
1112
import { IPosition } from 'vs/editor/common/core/position';
1213
import * as editorCommon from 'vs/editor/common/editorCommon';
1314
import * as model from 'vs/editor/common/model';
@@ -185,6 +186,7 @@ export abstract class BaseCellViewModel extends Disposable {
185186
});
186187

187188
this._textEditor = undefined;
189+
this._textModel = undefined;
188190
this._cursorChangeListener?.dispose();
189191
this._cursorChangeListener = null;
190192
this._onDidChangeEditorAttachState.fire();
@@ -264,6 +266,14 @@ export abstract class BaseCellViewModel extends Disposable {
264266
this._textEditor?.setSelection(range);
265267
}
266268

269+
setSelections(selections: Selection[]) {
270+
this._textEditor?.setSelections(selections);
271+
}
272+
273+
getSelections() {
274+
return this._textEditor?.getSelections() || [];
275+
}
276+
267277
getSelectionsStartPosition(): IPosition[] | undefined {
268278
if (this._textEditor) {
269279
const selections = this._textEditor.getSelections();
@@ -326,6 +336,8 @@ export abstract class BaseCellViewModel extends Disposable {
326336
return this.model.textBuffer;
327337
}
328338

339+
abstract resolveTextModel(): Promise<model.ITextModel>;
340+
329341
protected cellStartFind(value: string): model.FindMatch[] | null {
330342
let cellMatches: model.FindMatch[] = [];
331343

@@ -367,6 +379,10 @@ export abstract class BaseCellViewModel extends Disposable {
367379
};
368380
}
369381

382+
dispose() {
383+
super.dispose();
384+
}
385+
370386
toJSON(): any {
371387
return {
372388
handle: this.handle

0 commit comments

Comments
 (0)