Skip to content

Commit aac0b16

Browse files
committed
1 parent 9684962 commit aac0b16

10 files changed

Lines changed: 185 additions & 51 deletions

File tree

src/vs/editor/common/model.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,6 @@ export interface ITextModel {
596596
*/
597597
getEOL(): string;
598598

599-
/**
600-
* Change the end of line sequence used in the text buffer.
601-
*/
602-
setEOL(eol: EndOfLineSequence): void;
603-
604599
/**
605600
* Get the minimum legal column for line at `lineNumber`
606601
*/
@@ -1008,6 +1003,12 @@ export interface ITextModel {
10081003
*/
10091004
pushEditOperations(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[];
10101005

1006+
/**
1007+
* Change the end of line sequence. This is the preferred way of
1008+
* changing the eol sequence. This will land on the undo stack.
1009+
*/
1010+
pushEOL(eol: EndOfLineSequence): void;
1011+
10111012
/**
10121013
* Edit the model without adding the edits to the undo stack.
10131014
* This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way.
@@ -1016,6 +1017,12 @@ export interface ITextModel {
10161017
*/
10171018
applyEdits(operations: IIdentifiedSingleEditOperation[]): IIdentifiedSingleEditOperation[];
10181019

1020+
/**
1021+
* Change the end of line sequence without recording in the undo stack.
1022+
* This can have dire consequences on the undo stack! See @pushEOL for the preferred way.
1023+
*/
1024+
setEOL(eol: EndOfLineSequence): void;
1025+
10191026
/**
10201027
* Undo edit operations until the first previous stop point created by `pushStackElement`.
10211028
* The inverse edit operations will be pushed on the redo stack.

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

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict';
66

77
import { onUnexpectedError } from 'vs/base/common/errors';
8-
import { ICursorStateComputer, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
8+
import { ICursorStateComputer, IIdentifiedSingleEditOperation, EndOfLineSequence } from 'vs/editor/common/model';
99
import { Selection } from 'vs/editor/common/core/selection';
1010
import { TextModel } from 'vs/editor/common/model/textModel';
1111

@@ -14,13 +14,86 @@ interface IEditOperation {
1414
}
1515

1616
interface IStackElement {
17-
beforeVersionId: number;
18-
beforeCursorState: Selection[];
17+
readonly beforeVersionId: number;
18+
readonly beforeCursorState: Selection[];
19+
readonly afterCursorState: Selection[];
20+
readonly afterVersionId: number;
1921

20-
editOperations: IEditOperation[];
22+
undo(model: TextModel): void;
23+
redo(model: TextModel): void;
24+
}
25+
26+
class EditStackElement implements IStackElement {
27+
public readonly beforeVersionId: number;
28+
public readonly beforeCursorState: Selection[];
29+
public afterCursorState: Selection[];
30+
public afterVersionId: number;
31+
32+
public editOperations: IEditOperation[];
33+
34+
constructor(beforeVersionId: number, beforeCursorState: Selection[]) {
35+
this.beforeVersionId = beforeVersionId;
36+
this.beforeCursorState = beforeCursorState;
37+
this.afterCursorState = null;
38+
this.afterVersionId = -1;
39+
this.editOperations = [];
40+
}
41+
42+
public undo(model: TextModel): void {
43+
// Apply all operations in reverse order
44+
for (let i = this.editOperations.length - 1; i >= 0; i--) {
45+
this.editOperations[i] = {
46+
operations: model.applyEdits(this.editOperations[i].operations)
47+
};
48+
}
49+
}
50+
51+
public redo(model: TextModel): void {
52+
// Apply all operations
53+
for (let i = 0; i < this.editOperations.length; i++) {
54+
this.editOperations[i] = {
55+
operations: model.applyEdits(this.editOperations[i].operations)
56+
};
57+
}
58+
}
59+
}
60+
61+
function getModelEOL(model: TextModel): EndOfLineSequence {
62+
const eol = model.getEOL();
63+
if (eol === '\n') {
64+
return EndOfLineSequence.LF;
65+
} else {
66+
return EndOfLineSequence.CRLF;
67+
}
68+
}
69+
70+
class EOLStackElement implements IStackElement {
71+
public readonly beforeVersionId: number;
72+
public readonly beforeCursorState: Selection[];
73+
public readonly afterCursorState: Selection[];
74+
public afterVersionId: number;
75+
76+
public eol: EndOfLineSequence;
77+
78+
constructor(beforeVersionId: number, setEOL: EndOfLineSequence) {
79+
this.beforeVersionId = beforeVersionId;
80+
this.beforeCursorState = null;
81+
this.afterCursorState = null;
82+
this.afterVersionId = -1;
83+
this.eol = setEOL;
84+
}
85+
86+
public undo(model: TextModel): void {
87+
let redoEOL = getModelEOL(model);
88+
model.setEOL(this.eol);
89+
this.eol = redoEOL;
90+
}
2191

22-
afterCursorState: Selection[];
23-
afterVersionId: number;
92+
public redo(model: TextModel): void {
93+
let undoEOL = getModelEOL(model);
94+
model.setEOL(this.eol);
95+
this.eol = undoEOL;
96+
}
2497
}
2598

2699
export interface IUndoRedoResult {
@@ -55,34 +128,60 @@ export class EditStack {
55128
this.future = [];
56129
}
57130

131+
public pushEOL(eol: EndOfLineSequence): void {
132+
// No support for parallel universes :(
133+
this.future = [];
134+
135+
if (this.currentOpenStackElement) {
136+
this.pushStackElement();
137+
}
138+
139+
const prevEOL = getModelEOL(this.model);
140+
let stackElement = new EOLStackElement(this.model.getAlternativeVersionId(), prevEOL);
141+
142+
this.model.setEOL(eol);
143+
144+
stackElement.afterVersionId = this.model.getVersionId();
145+
this.currentOpenStackElement = stackElement;
146+
this.pushStackElement();
147+
}
148+
58149
public pushEditOperation(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] {
59150
// No support for parallel universes :(
60151
this.future = [];
61152

153+
let stackElement: EditStackElement = null;
154+
155+
if (this.currentOpenStackElement) {
156+
if (this.currentOpenStackElement instanceof EditStackElement) {
157+
stackElement = this.currentOpenStackElement;
158+
} else {
159+
this.pushStackElement();
160+
}
161+
}
162+
62163
if (!this.currentOpenStackElement) {
63-
this.currentOpenStackElement = {
64-
beforeVersionId: this.model.getAlternativeVersionId(),
65-
beforeCursorState: beforeCursorState,
66-
editOperations: [],
67-
afterCursorState: null,
68-
afterVersionId: -1
69-
};
164+
stackElement = new EditStackElement(this.model.getAlternativeVersionId(), beforeCursorState);
165+
this.currentOpenStackElement = stackElement;
70166
}
71167

72168
const inverseEditOperation: IEditOperation = {
73169
operations: this.model.applyEdits(editOperations)
74170
};
75171

76-
this.currentOpenStackElement.editOperations.push(inverseEditOperation);
172+
stackElement.editOperations.push(inverseEditOperation);
173+
stackElement.afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperation.operations);
174+
stackElement.afterVersionId = this.model.getVersionId();
175+
return stackElement.afterCursorState;
176+
}
177+
178+
private static _computeCursorState(cursorStateComputer: ICursorStateComputer, inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] {
77179
try {
78-
this.currentOpenStackElement.afterCursorState = cursorStateComputer ? cursorStateComputer(inverseEditOperation.operations) : null;
180+
return cursorStateComputer ? cursorStateComputer(inverseEditOperations) : null;
79181
} catch (e) {
80182
onUnexpectedError(e);
81-
this.currentOpenStackElement.afterCursorState = null;
183+
return null;
82184
}
83-
84-
this.currentOpenStackElement.afterVersionId = this.model.getVersionId();
85-
return this.currentOpenStackElement.afterCursorState;
86185
}
87186

88187
public undo(): IUndoRedoResult {
@@ -93,13 +192,9 @@ export class EditStack {
93192
const pastStackElement = this.past.pop();
94193

95194
try {
96-
// Apply all operations in reverse order
97-
for (let i = pastStackElement.editOperations.length - 1; i >= 0; i--) {
98-
pastStackElement.editOperations[i] = {
99-
operations: this.model.applyEdits(pastStackElement.editOperations[i].operations)
100-
};
101-
}
195+
pastStackElement.undo(this.model);
102196
} catch (e) {
197+
onUnexpectedError(e);
103198
this.clear();
104199
return null;
105200
}
@@ -118,20 +213,12 @@ export class EditStack {
118213
public redo(): IUndoRedoResult {
119214

120215
if (this.future.length > 0) {
121-
if (this.currentOpenStackElement) {
122-
throw new Error('How is this possible?');
123-
}
124-
125216
const futureStackElement = this.future.pop();
126217

127218
try {
128-
// Apply all operations
129-
for (let i = 0; i < futureStackElement.editOperations.length; i++) {
130-
futureStackElement.editOperations[i] = {
131-
operations: this.model.applyEdits(futureStackElement.editOperations[i].operations)
132-
};
133-
}
219+
futureStackElement.redo(this.model);
134220
} catch (e) {
221+
onUnexpectedError(e);
135222
this.clear();
136223
return null;
137224
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,21 @@ export class TextModel extends Disposable implements model.ITextModel {
11431143
this._commandManager.pushStackElement();
11441144
}
11451145

1146+
public pushEOL(eol: model.EndOfLineSequence): void {
1147+
const currentEOL = (this.getEOL() === '\n' ? model.EndOfLineSequence.LF : model.EndOfLineSequence.CRLF);
1148+
if (currentEOL === eol) {
1149+
return;
1150+
}
1151+
try {
1152+
this._onDidChangeDecorations.beginDeferredEmit();
1153+
this._eventEmitter.beginDeferredEmit();
1154+
this._commandManager.pushEOL(eol);
1155+
} finally {
1156+
this._eventEmitter.endDeferredEmit();
1157+
this._onDidChangeDecorations.endDeferredEmit();
1158+
}
1159+
}
1160+
11461161
public pushEditOperations(beforeCursorState: Selection[], editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer): Selection[] {
11471162
try {
11481163
this._onDidChangeDecorations.beginDeferredEmit();

src/vs/editor/common/services/modelServiceImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ export class ModelServiceImpl implements IModelService {
418418

419419
// Otherwise find a diff between the values and update model
420420
model.pushStackElement();
421-
model.setEOL(textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF : EndOfLineSequence.LF);
421+
model.pushEOL(textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF : EndOfLineSequence.LF);
422422
model.pushEditOperations(
423423
[],
424424
ModelServiceImpl._computeEdits(model, textBuffer),

src/vs/editor/contrib/format/formattingEdit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class FormattingEdit {
2626
}
2727

2828
if (typeof newEol === 'number') {
29-
editor.getModel().setEOL(newEol);
29+
editor.getModel().pushEOL(newEol);
3030
}
3131

3232
return singleEdits;
@@ -40,8 +40,8 @@ export class FormattingEdit {
4040
}
4141

4242
static execute(editor: ICodeEditor, _edits: TextEdit[]) {
43-
let edits = FormattingEdit._handleEolEdits(editor, _edits);
4443
editor.pushUndoStop();
44+
let edits = FormattingEdit._handleEolEdits(editor, _edits);
4545
if (edits.length === 1 && FormattingEdit._isFullModelReplaceEdit(editor, edits[0])) {
4646
// We use replace semantics and hope that markers stay put...
4747
editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));

src/vs/editor/test/browser/controller/cursor.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,25 @@ suite('Editor Controller - Regression tests', () => {
12141214
model.dispose();
12151215
});
12161216

1217+
test('issue #23539: Setting model EOL isn\'t undoable', () => {
1218+
usingCursor({
1219+
text: [
1220+
'Hello',
1221+
'world'
1222+
]
1223+
}, (model, cursor) => {
1224+
assertCursor(cursor, new Position(1, 1));
1225+
model.setEOL(EndOfLineSequence.LF);
1226+
assert.equal(model.getValue(), 'Hello\nworld');
1227+
1228+
model.pushEOL(EndOfLineSequence.CRLF);
1229+
assert.equal(model.getValue(), 'Hello\r\nworld');
1230+
1231+
cursorCommand(cursor, H.Undo);
1232+
assert.equal(model.getValue(), 'Hello\nworld');
1233+
});
1234+
});
1235+
12171236
test('issue #47733: Undo mangles unicode characters', () => {
12181237
const languageId = new LanguageIdentifier('myMode', 3);
12191238
class MyMode extends MockMode {

src/vs/monaco.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,10 +1505,6 @@ declare namespace monaco.editor {
15051505
* @return EOL char sequence (e.g.: '\n' or '\r\n').
15061506
*/
15071507
getEOL(): string;
1508-
/**
1509-
* Change the end of line sequence used in the text buffer.
1510-
*/
1511-
setEOL(eol: EndOfLineSequence): void;
15121508
/**
15131509
* Get the minimum legal column for line at `lineNumber`
15141510
*/
@@ -1744,13 +1740,23 @@ declare namespace monaco.editor {
17441740
* @return The cursor state returned by the `cursorStateComputer`.
17451741
*/
17461742
pushEditOperations(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[];
1743+
/**
1744+
* Change the end of line sequence. This is the preferred way of
1745+
* changing the eol sequence. This will land on the undo stack.
1746+
*/
1747+
pushEOL(eol: EndOfLineSequence): void;
17471748
/**
17481749
* Edit the model without adding the edits to the undo stack.
17491750
* This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way.
17501751
* @param operations The edit operations.
17511752
* @return The inverse edit operations, that, when applied, will bring the model back to the previous state.
17521753
*/
17531754
applyEdits(operations: IIdentifiedSingleEditOperation[]): IIdentifiedSingleEditOperation[];
1755+
/**
1756+
* Change the end of line sequence without recording in the undo stack.
1757+
* This can have dire consequences on the undo stack! See @pushEOL for the preferred way.
1758+
*/
1759+
setEOL(eol: EndOfLineSequence): void;
17541760
/**
17551761
* An event emitted when the contents of the model have changed.
17561762
* @event

src/vs/workbench/api/electron-browser/mainThreadEditor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,9 @@ export class MainThreadTextEditor {
448448
}
449449

450450
if (opts.setEndOfLine === EndOfLine.CRLF) {
451-
this._model.setEOL(EndOfLineSequence.CRLF);
451+
this._model.pushEOL(EndOfLineSequence.CRLF);
452452
} else if (opts.setEndOfLine === EndOfLine.LF) {
453-
this._model.setEOL(EndOfLineSequence.LF);
453+
this._model.pushEOL(EndOfLineSequence.LF);
454454
}
455455

456456
let transformedEdits = edits.map((edit): IIdentifiedSingleEditOperation => {

src/vs/workbench/browser/parts/editor/editorStatus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ export class ChangeEOLAction extends Action {
11221122
const editorWidget = getEditorWidget(activeEditor);
11231123
if (editorWidget && isWritableCodeEditor(editorWidget)) {
11241124
const textModel = editorWidget.getModel();
1125-
textModel.setEOL(eol.eol);
1125+
textModel.pushEOL(eol.eol);
11261126
}
11271127
}
11281128
});

src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class EditTask implements IDisposable {
109109
}
110110
if (this._newEol !== undefined) {
111111
this._model.pushStackElement();
112-
this._model.setEOL(this._newEol);
112+
this._model.pushEOL(this._newEol);
113113
this._model.pushStackElement();
114114
}
115115
}

0 commit comments

Comments
 (0)