Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/editStack.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../nls.js';
7
import { onUnexpectedError } from '../../../base/common/errors.js';
8
import { Selection } from '../core/selection.js';
9
import { EndOfLineSequence, ICursorStateComputer, IValidEditOperation, ITextModel } from '../model.js';
10
import { TextModel } from './textModel.js';
11
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
12
import { URI } from '../../../base/common/uri.js';
13
import { TextChange, compressConsecutiveTextChanges } from '../core/textChange.js';
14
import * as buffer from '../../../base/common/buffer.js';
15
import { IDisposable } from '../../../base/common/lifecycle.js';
16
import { basename } from '../../../base/common/resources.js';
17
import { ISingleEditOperation } from '../core/editOperation.js';
18
import { EditSources, TextModelEditSource } from '../textModelEditSource.js';
19
20
function uriGetComparisonKey(resource: URI): string {
21
return resource.toString();
22
}
23
24
export class SingleModelEditStackData {
25
26
public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {
27
const alternativeVersionId = model.getAlternativeVersionId();
28
const eol = getModelEOL(model);
29
return new SingleModelEditStackData(
30
alternativeVersionId,
31
alternativeVersionId,
32
eol,
33
eol,
34
beforeCursorState,
35
beforeCursorState,
36
[]
37
);
38
}
39
40
constructor(
41
public readonly beforeVersionId: number,
42
public afterVersionId: number,
43
public readonly beforeEOL: EndOfLineSequence,
44
public afterEOL: EndOfLineSequence,
45
public readonly beforeCursorState: Selection[] | null,
46
public afterCursorState: Selection[] | null,
47
public changes: TextChange[]
48
) { }
49
50
public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
51
if (textChanges.length > 0) {
52
this.changes = compressConsecutiveTextChanges(this.changes, textChanges);
53
}
54
this.afterEOL = afterEOL;
55
this.afterVersionId = afterVersionId;
56
this.afterCursorState = afterCursorState;
57
}
58
59
private static _writeSelectionsSize(selections: Selection[] | null): number {
60
return 4 + 4 * 4 * (selections ? selections.length : 0);
61
}
62
63
private static _writeSelections(b: Uint8Array, selections: Selection[] | null, offset: number): number {
64
buffer.writeUInt32BE(b, (selections ? selections.length : 0), offset); offset += 4;
65
if (selections) {
66
for (const selection of selections) {
67
buffer.writeUInt32BE(b, selection.selectionStartLineNumber, offset); offset += 4;
68
buffer.writeUInt32BE(b, selection.selectionStartColumn, offset); offset += 4;
69
buffer.writeUInt32BE(b, selection.positionLineNumber, offset); offset += 4;
70
buffer.writeUInt32BE(b, selection.positionColumn, offset); offset += 4;
71
}
72
}
73
return offset;
74
}
75
76
private static _readSelections(b: Uint8Array, offset: number, dest: Selection[]): number {
77
const count = buffer.readUInt32BE(b, offset); offset += 4;
78
for (let i = 0; i < count; i++) {
79
const selectionStartLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
80
const selectionStartColumn = buffer.readUInt32BE(b, offset); offset += 4;
81
const positionLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
82
const positionColumn = buffer.readUInt32BE(b, offset); offset += 4;
83
dest.push(new Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn));
84
}
85
return offset;
86
}
87
88
public serialize(): ArrayBuffer {
89
let necessarySize = (
90
+ 4 // beforeVersionId
91
+ 4 // afterVersionId
92
+ 1 // beforeEOL
93
+ 1 // afterEOL
94
+ SingleModelEditStackData._writeSelectionsSize(this.beforeCursorState)
95
+ SingleModelEditStackData._writeSelectionsSize(this.afterCursorState)
96
+ 4 // change count
97
);
98
for (const change of this.changes) {
99
necessarySize += change.writeSize();
100
}
101
102
const b = new Uint8Array(necessarySize);
103
let offset = 0;
104
buffer.writeUInt32BE(b, this.beforeVersionId, offset); offset += 4;
105
buffer.writeUInt32BE(b, this.afterVersionId, offset); offset += 4;
106
buffer.writeUInt8(b, this.beforeEOL, offset); offset += 1;
107
buffer.writeUInt8(b, this.afterEOL, offset); offset += 1;
108
offset = SingleModelEditStackData._writeSelections(b, this.beforeCursorState, offset);
109
offset = SingleModelEditStackData._writeSelections(b, this.afterCursorState, offset);
110
buffer.writeUInt32BE(b, this.changes.length, offset); offset += 4;
111
for (const change of this.changes) {
112
offset = change.write(b, offset);
113
}
114
return b.buffer;
115
}
116
117
public static deserialize(source: ArrayBuffer): SingleModelEditStackData {
118
const b = new Uint8Array(source);
119
let offset = 0;
120
const beforeVersionId = buffer.readUInt32BE(b, offset); offset += 4;
121
const afterVersionId = buffer.readUInt32BE(b, offset); offset += 4;
122
const beforeEOL = buffer.readUInt8(b, offset); offset += 1;
123
const afterEOL = buffer.readUInt8(b, offset); offset += 1;
124
const beforeCursorState: Selection[] = [];
125
offset = SingleModelEditStackData._readSelections(b, offset, beforeCursorState);
126
const afterCursorState: Selection[] = [];
127
offset = SingleModelEditStackData._readSelections(b, offset, afterCursorState);
128
const changeCount = buffer.readUInt32BE(b, offset); offset += 4;
129
const changes: TextChange[] = [];
130
for (let i = 0; i < changeCount; i++) {
131
offset = TextChange.read(b, offset, changes);
132
}
133
return new SingleModelEditStackData(
134
beforeVersionId,
135
afterVersionId,
136
beforeEOL,
137
afterEOL,
138
beforeCursorState,
139
afterCursorState,
140
changes
141
);
142
}
143
}
144
145
export interface IUndoRedoDelegate {
146
prepareUndoRedo(element: MultiModelEditStackElement): Promise<IDisposable> | IDisposable | void;
147
}
148
149
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
150
151
public model: ITextModel | URI;
152
private _data: SingleModelEditStackData | ArrayBuffer;
153
154
public get type(): UndoRedoElementType.Resource {
155
return UndoRedoElementType.Resource;
156
}
157
158
public get resource(): URI {
159
if (URI.isUri(this.model)) {
160
return this.model;
161
}
162
return this.model.uri;
163
}
164
165
constructor(
166
public readonly label: string,
167
public readonly code: string,
168
model: ITextModel,
169
beforeCursorState: Selection[] | null
170
) {
171
this.model = model;
172
this._data = SingleModelEditStackData.create(model, beforeCursorState);
173
}
174
175
public toString(): string {
176
const data = (this._data instanceof SingleModelEditStackData ? this._data : SingleModelEditStackData.deserialize(this._data));
177
return data.changes.map(change => change.toString()).join(', ');
178
}
179
180
public matchesResource(resource: URI): boolean {
181
const uri = (URI.isUri(this.model) ? this.model : this.model.uri);
182
return (uri.toString() === resource.toString());
183
}
184
185
public setModel(model: ITextModel | URI): void {
186
this.model = model;
187
}
188
189
public canAppend(model: ITextModel): boolean {
190
return (this.model === model && this._data instanceof SingleModelEditStackData);
191
}
192
193
public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
194
if (this._data instanceof SingleModelEditStackData) {
195
this._data.append(model, textChanges, afterEOL, afterVersionId, afterCursorState);
196
}
197
}
198
199
public close(): void {
200
if (this._data instanceof SingleModelEditStackData) {
201
this._data = this._data.serialize();
202
}
203
}
204
205
public open(): void {
206
if (!(this._data instanceof SingleModelEditStackData)) {
207
this._data = SingleModelEditStackData.deserialize(this._data);
208
}
209
}
210
211
public undo(): void {
212
if (URI.isUri(this.model)) {
213
// don't have a model
214
throw new Error(`Invalid SingleModelEditStackElement`);
215
}
216
if (this._data instanceof SingleModelEditStackData) {
217
this._data = this._data.serialize();
218
}
219
const data = SingleModelEditStackData.deserialize(this._data);
220
this.model._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState);
221
}
222
223
public redo(): void {
224
if (URI.isUri(this.model)) {
225
// don't have a model
226
throw new Error(`Invalid SingleModelEditStackElement`);
227
}
228
if (this._data instanceof SingleModelEditStackData) {
229
this._data = this._data.serialize();
230
}
231
const data = SingleModelEditStackData.deserialize(this._data);
232
this.model._applyRedo(data.changes, data.afterEOL, data.afterVersionId, data.afterCursorState);
233
}
234
235
public heapSize(): number {
236
if (this._data instanceof SingleModelEditStackData) {
237
this._data = this._data.serialize();
238
}
239
return this._data.byteLength + 168/*heap overhead*/;
240
}
241
}
242
243
export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
244
245
public readonly type = UndoRedoElementType.Workspace;
246
private _isOpen: boolean;
247
248
private readonly _editStackElementsArr: SingleModelEditStackElement[];
249
private readonly _editStackElementsMap: Map<string, SingleModelEditStackElement>;
250
251
private _delegate: IUndoRedoDelegate | null;
252
253
public get resources(): readonly URI[] {
254
return this._editStackElementsArr.map(editStackElement => editStackElement.resource);
255
}
256
257
constructor(
258
public readonly label: string,
259
public readonly code: string,
260
editStackElements: SingleModelEditStackElement[]
261
) {
262
this._isOpen = true;
263
this._editStackElementsArr = editStackElements.slice(0);
264
this._editStackElementsMap = new Map<string, SingleModelEditStackElement>();
265
for (const editStackElement of this._editStackElementsArr) {
266
const key = uriGetComparisonKey(editStackElement.resource);
267
this._editStackElementsMap.set(key, editStackElement);
268
}
269
this._delegate = null;
270
}
271
272
public setDelegate(delegate: IUndoRedoDelegate): void {
273
this._delegate = delegate;
274
}
275
276
public prepareUndoRedo(): Promise<IDisposable> | IDisposable | void {
277
if (this._delegate) {
278
return this._delegate.prepareUndoRedo(this);
279
}
280
}
281
282
public getMissingModels(): URI[] {
283
const result: URI[] = [];
284
for (const editStackElement of this._editStackElementsArr) {
285
if (URI.isUri(editStackElement.model)) {
286
result.push(editStackElement.model);
287
}
288
}
289
return result;
290
}
291
292
public matchesResource(resource: URI): boolean {
293
const key = uriGetComparisonKey(resource);
294
return (this._editStackElementsMap.has(key));
295
}
296
297
public setModel(model: ITextModel | URI): void {
298
const key = uriGetComparisonKey(URI.isUri(model) ? model : model.uri);
299
if (this._editStackElementsMap.has(key)) {
300
this._editStackElementsMap.get(key)!.setModel(model);
301
}
302
}
303
304
public canAppend(model: ITextModel): boolean {
305
if (!this._isOpen) {
306
return false;
307
}
308
const key = uriGetComparisonKey(model.uri);
309
if (this._editStackElementsMap.has(key)) {
310
const editStackElement = this._editStackElementsMap.get(key)!;
311
return editStackElement.canAppend(model);
312
}
313
return false;
314
}
315
316
public append(model: ITextModel, textChanges: TextChange[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
317
const key = uriGetComparisonKey(model.uri);
318
const editStackElement = this._editStackElementsMap.get(key)!;
319
editStackElement.append(model, textChanges, afterEOL, afterVersionId, afterCursorState);
320
}
321
322
public close(): void {
323
this._isOpen = false;
324
}
325
326
public open(): void {
327
// cannot reopen
328
}
329
330
public undo(): void {
331
this._isOpen = false;
332
333
for (const editStackElement of this._editStackElementsArr) {
334
editStackElement.undo();
335
}
336
}
337
338
public redo(): void {
339
for (const editStackElement of this._editStackElementsArr) {
340
editStackElement.redo();
341
}
342
}
343
344
public heapSize(resource: URI): number {
345
const key = uriGetComparisonKey(resource);
346
if (this._editStackElementsMap.has(key)) {
347
const editStackElement = this._editStackElementsMap.get(key)!;
348
return editStackElement.heapSize();
349
}
350
return 0;
351
}
352
353
public split(): IResourceUndoRedoElement[] {
354
return this._editStackElementsArr;
355
}
356
357
public toString(): string {
358
const result: string[] = [];
359
for (const editStackElement of this._editStackElementsArr) {
360
result.push(`${basename(editStackElement.resource)}: ${editStackElement}`);
361
}
362
return `{${result.join(', ')}}`;
363
}
364
}
365
366
export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement;
367
368
function getModelEOL(model: ITextModel): EndOfLineSequence {
369
const eol = model.getEOL();
370
if (eol === '\n') {
371
return EndOfLineSequence.LF;
372
} else {
373
return EndOfLineSequence.CRLF;
374
}
375
}
376
377
export function isEditStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement {
378
if (!element) {
379
return false;
380
}
381
return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement));
382
}
383
384
export class EditStack {
385
386
private readonly _model: TextModel;
387
private readonly _undoRedoService: IUndoRedoService;
388
389
constructor(model: TextModel, undoRedoService: IUndoRedoService) {
390
this._model = model;
391
this._undoRedoService = undoRedoService;
392
}
393
394
public pushStackElement(): void {
395
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
396
if (isEditStackElement(lastElement)) {
397
lastElement.close();
398
}
399
}
400
401
public popStackElement(): void {
402
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
403
if (isEditStackElement(lastElement)) {
404
lastElement.open();
405
}
406
}
407
408
public clear(): void {
409
this._undoRedoService.removeElements(this._model.uri);
410
}
411
412
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null, group: UndoRedoGroup | undefined): EditStackElement {
413
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
414
if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) {
415
return lastElement;
416
}
417
const newElement = new SingleModelEditStackElement(nls.localize('edit', "Typing"), 'undoredo.textBufferEdit', this._model, beforeCursorState);
418
this._undoRedoService.pushElement(newElement, group);
419
return newElement;
420
}
421
422
public pushEOL(eol: EndOfLineSequence): void {
423
const editStackElement = this._getOrCreateEditStackElement(null, undefined);
424
this._model.setEOL(eol);
425
editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null);
426
}
427
428
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup, reason: TextModelEditSource = EditSources.unknown({ name: 'pushEditOperation' })): Selection[] | null {
429
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group);
430
const inverseEditOperations = this._model.applyEdits(editOperations, true, reason);
431
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);
432
const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange }));
433
textChanges.sort((a, b) => {
434
if (a.textChange.oldPosition === b.textChange.oldPosition) {
435
return a.index - b.index;
436
}
437
return a.textChange.oldPosition - b.textChange.oldPosition;
438
});
439
editStackElement.append(this._model, textChanges.map(op => op.textChange), getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState);
440
return afterCursorState;
441
}
442
443
private static _computeCursorState(cursorStateComputer: ICursorStateComputer | null, inverseEditOperations: IValidEditOperation[]): Selection[] | null {
444
try {
445
return cursorStateComputer ? cursorStateComputer(inverseEditOperations) : null;
446
} catch (e) {
447
onUnexpectedError(e);
448
return null;
449
}
450
}
451
}
452
453