Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/cursor/cursor.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 { onUnexpectedError } from '../../../base/common/errors.js';
7
import * as strings from '../../../base/common/strings.js';
8
import { CursorCollection } from './cursorCollection.js';
9
import { CursorConfiguration, CursorState, EditOperationResult, EditOperationType, IColumnSelectData, PartialCursorState, ICursorSimpleModel } from '../cursorCommon.js';
10
import { CursorContext } from './cursorContext.js';
11
import { DeleteOperations } from './cursorDeleteOperations.js';
12
import { CursorChangeReason } from '../cursorEvents.js';
13
import { CompositionOutcome, TypeOperations } from './cursorTypeOperations.js';
14
import { BaseTypeWithAutoClosingCommand } from './cursorTypeEditOperations.js';
15
import { Position } from '../core/position.js';
16
import { Range, IRange } from '../core/range.js';
17
import { ISelection, Selection, SelectionDirection } from '../core/selection.js';
18
import * as editorCommon from '../editorCommon.js';
19
import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from '../model.js';
20
import { RawContentChangedType, ModelInjectedTextChangedEvent, InternalModelContentChangeEvent } from '../textModelEvents.js';
21
import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from '../viewEvents.js';
22
import { dispose, Disposable } from '../../../base/common/lifecycle.js';
23
import { CursorStateChangedEvent, ViewModelEventsCollector } from '../viewModelEventDispatcher.js';
24
import { TextModelEditSource, EditSources } from '../textModelEditSource.js';
25
import { ICoordinatesConverter } from '../coordinatesConverter.js';
26
27
export class CursorsController extends Disposable {
28
29
private readonly _model: ITextModel;
30
private _knownModelVersionId: number;
31
private readonly _viewModel: ICursorSimpleModel;
32
private readonly _coordinatesConverter: ICoordinatesConverter;
33
public context: CursorContext;
34
private _cursors: CursorCollection;
35
36
private _hasFocus: boolean;
37
private _isHandling: boolean;
38
private _compositionState: CompositionState | null;
39
private _columnSelectData: IColumnSelectData | null;
40
private _autoClosedActions: AutoClosedAction[];
41
private _prevEditOperationType: EditOperationType;
42
43
constructor(model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) {
44
super();
45
this._model = model;
46
this._knownModelVersionId = this._model.getVersionId();
47
this._viewModel = viewModel;
48
this._coordinatesConverter = coordinatesConverter;
49
this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig);
50
this._cursors = new CursorCollection(this.context);
51
52
this._hasFocus = false;
53
this._isHandling = false;
54
this._compositionState = null;
55
this._columnSelectData = null;
56
this._autoClosedActions = [];
57
this._prevEditOperationType = EditOperationType.Other;
58
}
59
60
public override dispose(): void {
61
this._cursors.dispose();
62
this._autoClosedActions = dispose(this._autoClosedActions);
63
super.dispose();
64
}
65
66
public updateConfiguration(cursorConfig: CursorConfiguration): void {
67
this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig);
68
this._cursors.updateContext(this.context);
69
}
70
71
public onLineMappingChanged(eventsCollector: ViewModelEventsCollector): void {
72
if (this._knownModelVersionId !== this._model.getVersionId()) {
73
// There are model change events that I didn't yet receive.
74
//
75
// This can happen when editing the model, and the view model receives the change events first,
76
// and the view model emits line mapping changed events, all before the cursor gets a chance to
77
// recover from markers.
78
//
79
// The model change listener above will be called soon and we'll ensure a valid cursor state there.
80
return;
81
}
82
// Ensure valid state
83
this.setStates(eventsCollector, 'viewModel', CursorChangeReason.NotSet, this.getCursorStates());
84
}
85
86
public setHasFocus(hasFocus: boolean): void {
87
this._hasFocus = hasFocus;
88
}
89
90
private _validateAutoClosedActions(): void {
91
if (this._autoClosedActions.length > 0) {
92
const selections: Range[] = this._cursors.getSelections();
93
for (let i = 0; i < this._autoClosedActions.length; i++) {
94
const autoClosedAction = this._autoClosedActions[i];
95
if (!autoClosedAction.isValid(selections)) {
96
autoClosedAction.dispose();
97
this._autoClosedActions.splice(i, 1);
98
i--;
99
}
100
}
101
}
102
}
103
104
// ------ some getters/setters
105
106
public getPrimaryCursorState(): CursorState {
107
return this._cursors.getPrimaryCursor();
108
}
109
110
public getLastAddedCursorIndex(): number {
111
return this._cursors.getLastAddedCursorIndex();
112
}
113
114
public getCursorStates(): CursorState[] {
115
return this._cursors.getAll();
116
}
117
118
public setStates(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean {
119
let reachedMaxCursorCount = false;
120
const multiCursorLimit = this.context.cursorConfig.multiCursorLimit;
121
if (states !== null && states.length > multiCursorLimit) {
122
states = states.slice(0, multiCursorLimit);
123
reachedMaxCursorCount = true;
124
}
125
126
const oldState = CursorModelState.from(this._model, this);
127
128
this._cursors.setStates(states);
129
this._cursors.normalize();
130
this._columnSelectData = null;
131
132
this._validateAutoClosedActions();
133
134
return this._emitStateChangedIfNecessary(eventsCollector, source, reason, oldState, reachedMaxCursorCount);
135
}
136
137
public setCursorColumnSelectData(columnSelectData: IColumnSelectData): void {
138
this._columnSelectData = columnSelectData;
139
}
140
141
public revealAll(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, minimalReveal: boolean, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void {
142
const viewPositions = this._cursors.getViewPositions();
143
144
let revealViewRange: Range | null = null;
145
let revealViewSelections: Selection[] | null = null;
146
if (viewPositions.length > 1) {
147
revealViewSelections = this._cursors.getViewSelections();
148
} else {
149
revealViewRange = Range.fromPositions(viewPositions[0], viewPositions[0]);
150
}
151
152
eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, minimalReveal, revealViewRange, revealViewSelections, verticalType, revealHorizontal, scrollType));
153
}
154
155
public revealPrimary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, minimalReveal: boolean, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void {
156
const primaryCursor = this._cursors.getPrimaryCursor();
157
const revealViewSelections = [primaryCursor.viewState.selection];
158
eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, minimalReveal, null, revealViewSelections, verticalType, revealHorizontal, scrollType));
159
}
160
161
public saveState(): editorCommon.ICursorState[] {
162
163
const result: editorCommon.ICursorState[] = [];
164
165
const selections = this._cursors.getSelections();
166
for (let i = 0, len = selections.length; i < len; i++) {
167
const selection = selections[i];
168
169
result.push({
170
inSelectionMode: !selection.isEmpty(),
171
selectionStart: {
172
lineNumber: selection.selectionStartLineNumber,
173
column: selection.selectionStartColumn,
174
},
175
position: {
176
lineNumber: selection.positionLineNumber,
177
column: selection.positionColumn,
178
}
179
});
180
}
181
182
return result;
183
}
184
185
public restoreState(eventsCollector: ViewModelEventsCollector, states: editorCommon.ICursorState[]): void {
186
187
const desiredSelections: ISelection[] = [];
188
189
for (let i = 0, len = states.length; i < len; i++) {
190
const state = states[i];
191
192
let positionLineNumber = 1;
193
let positionColumn = 1;
194
195
// Avoid missing properties on the literal
196
if (state.position && state.position.lineNumber) {
197
positionLineNumber = state.position.lineNumber;
198
}
199
if (state.position && state.position.column) {
200
positionColumn = state.position.column;
201
}
202
203
let selectionStartLineNumber = positionLineNumber;
204
let selectionStartColumn = positionColumn;
205
206
// Avoid missing properties on the literal
207
if (state.selectionStart && state.selectionStart.lineNumber) {
208
selectionStartLineNumber = state.selectionStart.lineNumber;
209
}
210
if (state.selectionStart && state.selectionStart.column) {
211
selectionStartColumn = state.selectionStart.column;
212
}
213
214
desiredSelections.push({
215
selectionStartLineNumber: selectionStartLineNumber,
216
selectionStartColumn: selectionStartColumn,
217
positionLineNumber: positionLineNumber,
218
positionColumn: positionColumn
219
});
220
}
221
222
this.setStates(eventsCollector, 'restoreState', CursorChangeReason.NotSet, CursorState.fromModelSelections(desiredSelections));
223
this.revealAll(eventsCollector, 'restoreState', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Immediate);
224
}
225
226
public onModelContentChanged(eventsCollector: ViewModelEventsCollector, event: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void {
227
if (event instanceof ModelInjectedTextChangedEvent) {
228
// If injected texts change, the view positions of all cursors need to be updated.
229
if (this._isHandling) {
230
// The view positions will be updated when handling finishes
231
return;
232
}
233
// setStates might remove markers, which could trigger a decoration change.
234
// If there are injected text decorations for that line, `onModelContentChanged` is emitted again
235
// and an endless recursion happens.
236
// _isHandling prevents that.
237
this._isHandling = true;
238
try {
239
this.setStates(eventsCollector, 'modelChange', CursorChangeReason.NotSet, this.getCursorStates());
240
} finally {
241
this._isHandling = false;
242
}
243
} else {
244
const e = event.rawContentChangedEvent;
245
this._knownModelVersionId = e.versionId;
246
if (this._isHandling) {
247
return;
248
}
249
250
const hadFlushEvent = e.containsEvent(RawContentChangedType.Flush);
251
this._prevEditOperationType = EditOperationType.Other;
252
253
if (hadFlushEvent) {
254
// a model.setValue() was called
255
this._cursors.dispose();
256
this._cursors = new CursorCollection(this.context);
257
this._validateAutoClosedActions();
258
this._emitStateChangedIfNecessary(eventsCollector, 'model', CursorChangeReason.ContentFlush, null, false);
259
} else {
260
if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) {
261
const cursorState = CursorState.fromModelSelections(e.resultingSelection);
262
if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) {
263
this.revealAll(eventsCollector, 'modelChange', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
264
}
265
} else {
266
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
267
this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers));
268
}
269
}
270
}
271
}
272
273
public getSelection(): Selection {
274
return this._cursors.getPrimaryCursor().modelState.selection;
275
}
276
277
public getTopMostViewPosition(): Position {
278
return this._cursors.getTopMostViewPosition();
279
}
280
281
public getBottomMostViewPosition(): Position {
282
return this._cursors.getBottomMostViewPosition();
283
}
284
285
public getCursorColumnSelectData(): IColumnSelectData {
286
if (this._columnSelectData) {
287
return this._columnSelectData;
288
}
289
const primaryCursor = this._cursors.getPrimaryCursor();
290
const viewSelectionStart = primaryCursor.viewState.selectionStart.getStartPosition();
291
const viewPosition = primaryCursor.viewState.position;
292
return {
293
isReal: false,
294
fromViewLineNumber: viewSelectionStart.lineNumber,
295
fromViewVisualColumn: this.context.cursorConfig.visibleColumnFromColumn(this._viewModel, viewSelectionStart),
296
toViewLineNumber: viewPosition.lineNumber,
297
toViewVisualColumn: this.context.cursorConfig.visibleColumnFromColumn(this._viewModel, viewPosition),
298
};
299
}
300
301
public getSelections(): Selection[] {
302
return this._cursors.getSelections();
303
}
304
305
public getPosition(): Position {
306
return this._cursors.getPrimaryCursor().modelState.position;
307
}
308
309
public setSelections(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, selections: readonly ISelection[], reason: CursorChangeReason): void {
310
this.setStates(eventsCollector, source, reason, CursorState.fromModelSelections(selections));
311
}
312
313
public getPrevEditOperationType(): EditOperationType {
314
return this._prevEditOperationType;
315
}
316
317
public setPrevEditOperationType(type: EditOperationType): void {
318
this._prevEditOperationType = type;
319
}
320
321
// ------ auxiliary handling logic
322
323
private _pushAutoClosedAction(autoClosedCharactersRanges: Range[], autoClosedEnclosingRanges: Range[]): void {
324
const autoClosedCharactersDeltaDecorations: IModelDeltaDecoration[] = [];
325
const autoClosedEnclosingDeltaDecorations: IModelDeltaDecoration[] = [];
326
327
for (let i = 0, len = autoClosedCharactersRanges.length; i < len; i++) {
328
autoClosedCharactersDeltaDecorations.push({
329
range: autoClosedCharactersRanges[i],
330
options: {
331
description: 'auto-closed-character',
332
inlineClassName: 'auto-closed-character',
333
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
334
}
335
});
336
autoClosedEnclosingDeltaDecorations.push({
337
range: autoClosedEnclosingRanges[i],
338
options: {
339
description: 'auto-closed-enclosing',
340
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
341
}
342
});
343
}
344
345
const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersDeltaDecorations);
346
const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingDeltaDecorations);
347
this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations));
348
}
349
350
private _executeEditOperation(opResult: EditOperationResult | null, editReason: TextModelEditSource): void {
351
352
if (!opResult) {
353
// Nothing to execute
354
return;
355
}
356
357
if (opResult.shouldPushStackElementBefore) {
358
this._model.pushStackElement();
359
}
360
361
const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands, editReason);
362
if (result) {
363
// The commands were applied correctly
364
this._interpretCommandResult(result);
365
366
// Check for auto-closing closed characters
367
const autoClosedCharactersRanges: Range[] = [];
368
const autoClosedEnclosingRanges: Range[] = [];
369
370
for (let i = 0; i < opResult.commands.length; i++) {
371
const command = opResult.commands[i];
372
if (command instanceof BaseTypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) {
373
autoClosedCharactersRanges.push(command.closeCharacterRange);
374
autoClosedEnclosingRanges.push(command.enclosingRange);
375
}
376
}
377
378
if (autoClosedCharactersRanges.length > 0) {
379
this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges);
380
}
381
382
this._prevEditOperationType = opResult.type;
383
}
384
385
if (opResult.shouldPushStackElementAfter) {
386
this._model.pushStackElement();
387
}
388
}
389
390
private _interpretCommandResult(cursorState: Selection[] | null): void {
391
if (!cursorState || cursorState.length === 0) {
392
cursorState = this._cursors.readSelectionFromMarkers();
393
}
394
395
this._columnSelectData = null;
396
this._cursors.setSelections(cursorState);
397
this._cursors.normalize();
398
}
399
400
// -----------------------------------------------------------------------------------------------------------
401
// ----- emitting events
402
403
private _emitStateChangedIfNecessary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, oldState: CursorModelState | null, reachedMaxCursorCount: boolean): boolean {
404
const newState = CursorModelState.from(this._model, this);
405
if (newState.equals(oldState)) {
406
return false;
407
}
408
409
const selections = this._cursors.getSelections();
410
const viewSelections = this._cursors.getViewSelections();
411
412
// Let the view get the event first.
413
eventsCollector.emitViewEvent(new ViewCursorStateChangedEvent(viewSelections, selections, reason));
414
415
// Only after the view has been notified, let the rest of the world know...
416
if (!oldState
417
|| oldState.cursorState.length !== newState.cursorState.length
418
|| newState.cursorState.some((newCursorState, i) => !newCursorState.modelState.equals(oldState.cursorState[i].modelState))
419
) {
420
const oldSelections = oldState ? oldState.cursorState.map(s => s.modelState.selection) : null;
421
const oldModelVersionId = oldState ? oldState.modelVersionId : 0;
422
eventsCollector.emitOutgoingEvent(new CursorStateChangedEvent(oldSelections, selections, oldModelVersionId, newState.modelVersionId, source || 'keyboard', reason, reachedMaxCursorCount));
423
}
424
425
return true;
426
}
427
428
// -----------------------------------------------------------------------------------------------------------
429
// ----- handlers beyond this point
430
431
private _findAutoClosingPairs(edits: IIdentifiedSingleEditOperation[]): [number, number][] | null {
432
if (!edits.length) {
433
return null;
434
}
435
436
const indices: [number, number][] = [];
437
for (let i = 0, len = edits.length; i < len; i++) {
438
const edit = edits[i];
439
if (!edit.text || edit.text.indexOf('\n') >= 0) {
440
return null;
441
}
442
443
const m = edit.text.match(/([)\]}>'"`])([^)\]}>'"`]*)$/);
444
if (!m) {
445
return null;
446
}
447
const closeChar = m[1];
448
449
const autoClosingPairsCandidates = this.context.cursorConfig.autoClosingPairs.autoClosingPairsCloseSingleChar.get(closeChar);
450
if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) {
451
return null;
452
}
453
454
const openChar = autoClosingPairsCandidates[0].open;
455
const closeCharIndex = edit.text.length - m[2].length - 1;
456
const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1);
457
if (openCharIndex === -1) {
458
return null;
459
}
460
461
indices.push([openCharIndex, closeCharIndex]);
462
}
463
464
return indices;
465
}
466
467
public executeEdits(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, reason: TextModelEditSource): void {
468
let autoClosingIndices: [number, number][] | null = null;
469
if (source === 'snippet') {
470
autoClosingIndices = this._findAutoClosingPairs(edits);
471
}
472
473
if (autoClosingIndices) {
474
edits[0]._isTracked = true;
475
}
476
const autoClosedCharactersRanges: Range[] = [];
477
const autoClosedEnclosingRanges: Range[] = [];
478
const selections = this._model.pushEditOperations(this.getSelections(), edits, (undoEdits) => {
479
if (autoClosingIndices) {
480
for (let i = 0, len = autoClosingIndices.length; i < len; i++) {
481
const [openCharInnerIndex, closeCharInnerIndex] = autoClosingIndices[i];
482
const undoEdit = undoEdits[i];
483
const lineNumber = undoEdit.range.startLineNumber;
484
const openCharIndex = undoEdit.range.startColumn - 1 + openCharInnerIndex;
485
const closeCharIndex = undoEdit.range.startColumn - 1 + closeCharInnerIndex;
486
487
autoClosedCharactersRanges.push(new Range(lineNumber, closeCharIndex + 1, lineNumber, closeCharIndex + 2));
488
autoClosedEnclosingRanges.push(new Range(lineNumber, openCharIndex + 1, lineNumber, closeCharIndex + 2));
489
}
490
}
491
const selections = cursorStateComputer(undoEdits);
492
if (selections) {
493
// Don't recover the selection from markers because
494
// we know what it should be.
495
this._isHandling = true;
496
}
497
498
return selections;
499
}, undefined, reason);
500
if (selections) {
501
this._isHandling = false;
502
this.setSelections(eventsCollector, source, selections, CursorChangeReason.NotSet);
503
}
504
if (autoClosedCharactersRanges.length > 0) {
505
this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges);
506
}
507
}
508
509
private _executeEdit(callback: () => void, eventsCollector: ViewModelEventsCollector, source: string | null | undefined, cursorChangeReason: CursorChangeReason = CursorChangeReason.NotSet): void {
510
if (this.context.cursorConfig.readOnly) {
511
// we cannot edit when read only...
512
return;
513
}
514
515
const oldState = CursorModelState.from(this._model, this);
516
this._cursors.stopTrackingSelections();
517
this._isHandling = true;
518
519
try {
520
this._cursors.ensureValidState();
521
callback();
522
} catch (err) {
523
onUnexpectedError(err);
524
}
525
526
this._isHandling = false;
527
this._cursors.startTrackingSelections();
528
this._validateAutoClosedActions();
529
if (this._emitStateChangedIfNecessary(eventsCollector, source, cursorChangeReason, oldState, false)) {
530
this.revealAll(eventsCollector, source, false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
531
}
532
}
533
534
public getAutoClosedCharacters(): Range[] {
535
return AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
536
}
537
538
public startComposition(eventsCollector: ViewModelEventsCollector): void {
539
this._compositionState = new CompositionState(this._model, this.getSelections());
540
}
541
542
public endComposition(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
543
const reason = EditSources.cursor({ kind: 'compositionEnd', detailedSource: source });
544
545
const compositionOutcome = this._compositionState ? this._compositionState.deduceOutcome(this._model, this.getSelections()) : null;
546
this._compositionState = null;
547
548
this._executeEdit(() => {
549
if (source === 'keyboard') {
550
// composition finishes, let's check if we need to auto complete if necessary.
551
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, compositionOutcome, this.getSelections(), this.getAutoClosedCharacters()), reason);
552
}
553
}, eventsCollector, source);
554
}
555
556
public type(eventsCollector: ViewModelEventsCollector, text: string, source?: string | null | undefined): void {
557
const reason = EditSources.cursor({ kind: 'type', detailedSource: source });
558
559
this._executeEdit(() => {
560
if (source === 'keyboard') {
561
// If this event is coming straight from the keyboard, look for electric characters and enter
562
563
const len = text.length;
564
let offset = 0;
565
while (offset < len) {
566
const charLength = strings.nextCharLength(text, offset);
567
const chr = text.substr(offset, charLength);
568
569
// Here we must interpret each typed character individually
570
this._executeEditOperation(TypeOperations.typeWithInterceptors(!!this._compositionState, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr), reason);
571
572
offset += charLength;
573
}
574
575
} else {
576
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text), reason);
577
}
578
}, eventsCollector, source);
579
}
580
581
public compositionType(eventsCollector: ViewModelEventsCollector, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number, source?: string | null | undefined): void {
582
const reason = EditSources.cursor({ kind: 'compositionType', detailedSource: source });
583
584
if (text.length === 0 && replacePrevCharCnt === 0 && replaceNextCharCnt === 0) {
585
// this edit is a no-op
586
if (positionDelta !== 0) {
587
// but it still wants to move the cursor
588
const newSelections = this.getSelections().map(selection => {
589
const position = selection.getPosition();
590
return new Selection(position.lineNumber, position.column + positionDelta, position.lineNumber, position.column + positionDelta);
591
});
592
this.setSelections(eventsCollector, source, newSelections, CursorChangeReason.NotSet);
593
}
594
return;
595
}
596
this._executeEdit(() => {
597
this._executeEditOperation(TypeOperations.compositionType(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replacePrevCharCnt, replaceNextCharCnt, positionDelta), reason);
598
}, eventsCollector, source);
599
}
600
601
public paste(eventsCollector: ViewModelEventsCollector, text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void {
602
const reason = EditSources.cursor({ kind: 'paste', detailedSource: source });
603
604
this._executeEdit(() => {
605
this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || []), reason);
606
}, eventsCollector, source, CursorChangeReason.Paste);
607
}
608
609
public cut(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
610
const reason = EditSources.cursor({ kind: 'cut', detailedSource: source });
611
this._executeEdit(() => {
612
this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections()), reason);
613
}, eventsCollector, source);
614
}
615
616
public executeCommand(eventsCollector: ViewModelEventsCollector, command: editorCommon.ICommand, source?: string | null | undefined): void {
617
const reason = EditSources.cursor({ kind: 'executeCommand', detailedSource: source });
618
619
this._executeEdit(() => {
620
this._cursors.killSecondaryCursors();
621
622
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, [command], {
623
shouldPushStackElementBefore: false,
624
shouldPushStackElementAfter: false
625
}), reason);
626
}, eventsCollector, source);
627
}
628
629
public executeCommands(eventsCollector: ViewModelEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void {
630
const reason = EditSources.cursor({ kind: 'executeCommands', detailedSource: source });
631
632
this._executeEdit(() => {
633
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, {
634
shouldPushStackElementBefore: false,
635
shouldPushStackElementAfter: false
636
}), reason);
637
}, eventsCollector, source);
638
}
639
}
640
641
/**
642
* A snapshot of the cursor and the model state
643
*/
644
class CursorModelState {
645
public static from(model: ITextModel, cursor: CursorsController): CursorModelState {
646
return new CursorModelState(model.getVersionId(), cursor.getCursorStates());
647
}
648
649
constructor(
650
public readonly modelVersionId: number,
651
public readonly cursorState: CursorState[],
652
) {
653
}
654
655
public equals(other: CursorModelState | null): boolean {
656
if (!other) {
657
return false;
658
}
659
if (this.modelVersionId !== other.modelVersionId) {
660
return false;
661
}
662
if (this.cursorState.length !== other.cursorState.length) {
663
return false;
664
}
665
for (let i = 0, len = this.cursorState.length; i < len; i++) {
666
if (!this.cursorState[i].equals(other.cursorState[i])) {
667
return false;
668
}
669
}
670
return true;
671
}
672
}
673
674
class AutoClosedAction {
675
676
public static getAllAutoClosedCharacters(autoClosedActions: AutoClosedAction[]): Range[] {
677
let autoClosedCharacters: Range[] = [];
678
for (const autoClosedAction of autoClosedActions) {
679
autoClosedCharacters = autoClosedCharacters.concat(autoClosedAction.getAutoClosedCharactersRanges());
680
}
681
return autoClosedCharacters;
682
}
683
684
private readonly _model: ITextModel;
685
686
private _autoClosedCharactersDecorations: string[];
687
private _autoClosedEnclosingDecorations: string[];
688
689
constructor(model: ITextModel, autoClosedCharactersDecorations: string[], autoClosedEnclosingDecorations: string[]) {
690
this._model = model;
691
this._autoClosedCharactersDecorations = autoClosedCharactersDecorations;
692
this._autoClosedEnclosingDecorations = autoClosedEnclosingDecorations;
693
}
694
695
public dispose(): void {
696
this._autoClosedCharactersDecorations = this._model.deltaDecorations(this._autoClosedCharactersDecorations, []);
697
this._autoClosedEnclosingDecorations = this._model.deltaDecorations(this._autoClosedEnclosingDecorations, []);
698
}
699
700
public getAutoClosedCharactersRanges(): Range[] {
701
const result: Range[] = [];
702
for (let i = 0; i < this._autoClosedCharactersDecorations.length; i++) {
703
const decorationRange = this._model.getDecorationRange(this._autoClosedCharactersDecorations[i]);
704
if (decorationRange) {
705
result.push(decorationRange);
706
}
707
}
708
return result;
709
}
710
711
public isValid(selections: Range[]): boolean {
712
const enclosingRanges: Range[] = [];
713
for (let i = 0; i < this._autoClosedEnclosingDecorations.length; i++) {
714
const decorationRange = this._model.getDecorationRange(this._autoClosedEnclosingDecorations[i]);
715
if (decorationRange) {
716
enclosingRanges.push(decorationRange);
717
if (decorationRange.startLineNumber !== decorationRange.endLineNumber) {
718
// Stop tracking if the range becomes multiline...
719
return false;
720
}
721
}
722
}
723
enclosingRanges.sort(Range.compareRangesUsingStarts);
724
725
selections.sort(Range.compareRangesUsingStarts);
726
727
for (let i = 0; i < selections.length; i++) {
728
if (i >= enclosingRanges.length) {
729
return false;
730
}
731
if (!enclosingRanges[i].strictContainsRange(selections[i])) {
732
return false;
733
}
734
}
735
736
return true;
737
}
738
}
739
740
interface IExecContext {
741
readonly model: ITextModel;
742
readonly selectionsBefore: Selection[];
743
readonly trackedRanges: string[];
744
readonly trackedRangesDirection: SelectionDirection[];
745
}
746
747
interface ICommandData {
748
operations: IIdentifiedSingleEditOperation[];
749
hadTrackedEditOperation: boolean;
750
}
751
752
interface ICommandsData {
753
operations: IIdentifiedSingleEditOperation[];
754
hadTrackedEditOperation: boolean;
755
}
756
757
export class CommandExecutor {
758
759
public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[], editReason: TextModelEditSource = EditSources.unknown({ name: 'executeCommands' })): Selection[] | null {
760
761
const ctx: IExecContext = {
762
model: model,
763
selectionsBefore: selectionsBefore,
764
trackedRanges: [],
765
trackedRangesDirection: []
766
};
767
768
const result = this._innerExecuteCommands(ctx, commands, editReason);
769
770
for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) {
771
ctx.model._setTrackedRange(ctx.trackedRanges[i], null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges);
772
}
773
774
return result;
775
}
776
777
private static _innerExecuteCommands(ctx: IExecContext, commands: (editorCommon.ICommand | null)[], editReason: TextModelEditSource): Selection[] | null {
778
779
if (this._arrayIsEmpty(commands)) {
780
return null;
781
}
782
783
const commandsData = this._getEditOperations(ctx, commands);
784
if (commandsData.operations.length === 0) {
785
return null;
786
}
787
788
const rawOperations = commandsData.operations;
789
790
const loserCursorsMap = this._getLoserCursorMap(rawOperations);
791
if (loserCursorsMap.hasOwnProperty('0')) {
792
// These commands are very messed up
793
console.warn('Ignoring commands');
794
return null;
795
}
796
797
// Remove operations belonging to losing cursors
798
const filteredOperations: IIdentifiedSingleEditOperation[] = [];
799
for (let i = 0, len = rawOperations.length; i < len; i++) {
800
if (!loserCursorsMap.hasOwnProperty(rawOperations[i].identifier!.major.toString())) {
801
filteredOperations.push(rawOperations[i]);
802
}
803
}
804
805
// TODO@Alex: find a better way to do this.
806
// give the hint that edit operations are tracked to the model
807
if (commandsData.hadTrackedEditOperation && filteredOperations.length > 0) {
808
filteredOperations[0]._isTracked = true;
809
}
810
let selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations: IValidEditOperation[]): Selection[] => {
811
const groupedInverseEditOperations: IValidEditOperation[][] = [];
812
for (let i = 0; i < ctx.selectionsBefore.length; i++) {
813
groupedInverseEditOperations[i] = [];
814
}
815
for (const op of inverseEditOperations) {
816
if (!op.identifier) {
817
// perhaps auto whitespace trim edits
818
continue;
819
}
820
groupedInverseEditOperations[op.identifier.major].push(op);
821
}
822
const minorBasedSorter = (a: IValidEditOperation, b: IValidEditOperation) => {
823
return a.identifier!.minor - b.identifier!.minor;
824
};
825
const cursorSelections: Selection[] = [];
826
for (let i = 0; i < ctx.selectionsBefore.length; i++) {
827
if (groupedInverseEditOperations[i].length > 0) {
828
groupedInverseEditOperations[i].sort(minorBasedSorter);
829
cursorSelections[i] = commands[i]!.computeCursorState(ctx.model, {
830
getInverseEditOperations: () => {
831
return groupedInverseEditOperations[i];
832
},
833
834
getTrackedSelection: (id: string) => {
835
const idx = parseInt(id, 10);
836
const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx])!;
837
if (ctx.trackedRangesDirection[idx] === SelectionDirection.LTR) {
838
return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
839
}
840
return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn);
841
}
842
});
843
} else {
844
cursorSelections[i] = ctx.selectionsBefore[i];
845
}
846
}
847
return cursorSelections;
848
}, undefined, editReason);
849
if (!selectionsAfter) {
850
selectionsAfter = ctx.selectionsBefore;
851
}
852
853
// Extract losing cursors
854
const losingCursors: number[] = [];
855
for (const losingCursorIndex in loserCursorsMap) {
856
if (loserCursorsMap.hasOwnProperty(losingCursorIndex)) {
857
losingCursors.push(parseInt(losingCursorIndex, 10));
858
}
859
}
860
861
// Sort losing cursors descending
862
losingCursors.sort((a: number, b: number): number => {
863
return b - a;
864
});
865
866
// Remove losing cursors
867
for (const losingCursor of losingCursors) {
868
selectionsAfter.splice(losingCursor, 1);
869
}
870
871
return selectionsAfter;
872
}
873
874
private static _arrayIsEmpty(commands: (editorCommon.ICommand | null)[]): boolean {
875
for (let i = 0, len = commands.length; i < len; i++) {
876
if (commands[i]) {
877
return false;
878
}
879
}
880
return true;
881
}
882
883
private static _getEditOperations(ctx: IExecContext, commands: (editorCommon.ICommand | null)[]): ICommandsData {
884
let operations: IIdentifiedSingleEditOperation[] = [];
885
let hadTrackedEditOperation: boolean = false;
886
887
for (let i = 0, len = commands.length; i < len; i++) {
888
const command = commands[i];
889
if (command) {
890
const r = this._getEditOperationsFromCommand(ctx, i, command);
891
operations = operations.concat(r.operations);
892
hadTrackedEditOperation = hadTrackedEditOperation || r.hadTrackedEditOperation;
893
}
894
}
895
return {
896
operations: operations,
897
hadTrackedEditOperation: hadTrackedEditOperation
898
};
899
}
900
901
private static _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand): ICommandData {
902
// This method acts as a transaction, if the command fails
903
// everything it has done is ignored
904
const operations: IIdentifiedSingleEditOperation[] = [];
905
let operationMinor = 0;
906
907
const addEditOperation = (range: IRange, text: string | null, forceMoveMarkers: boolean = false) => {
908
if (Range.isEmpty(range) && text === '') {
909
// This command wants to add a no-op => no thank you
910
return;
911
}
912
operations.push({
913
identifier: {
914
major: majorIdentifier,
915
minor: operationMinor++
916
},
917
range: range,
918
text: text,
919
forceMoveMarkers: forceMoveMarkers,
920
isAutoWhitespaceEdit: command.insertsAutoWhitespace
921
});
922
};
923
924
let hadTrackedEditOperation = false;
925
const addTrackedEditOperation = (selection: IRange, text: string | null, forceMoveMarkers?: boolean) => {
926
hadTrackedEditOperation = true;
927
addEditOperation(selection, text, forceMoveMarkers);
928
};
929
930
const trackSelection = (_selection: ISelection, trackPreviousOnEmpty?: boolean) => {
931
const selection = Selection.liftSelection(_selection);
932
let stickiness: TrackedRangeStickiness;
933
if (selection.isEmpty()) {
934
if (typeof trackPreviousOnEmpty === 'boolean') {
935
if (trackPreviousOnEmpty) {
936
stickiness = TrackedRangeStickiness.GrowsOnlyWhenTypingBefore;
937
} else {
938
stickiness = TrackedRangeStickiness.GrowsOnlyWhenTypingAfter;
939
}
940
} else {
941
// Try to lock it with surrounding text
942
const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber);
943
if (selection.startColumn === maxLineColumn) {
944
stickiness = TrackedRangeStickiness.GrowsOnlyWhenTypingBefore;
945
} else {
946
stickiness = TrackedRangeStickiness.GrowsOnlyWhenTypingAfter;
947
}
948
}
949
} else {
950
stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges;
951
}
952
953
const l = ctx.trackedRanges.length;
954
const id = ctx.model._setTrackedRange(null, selection, stickiness);
955
ctx.trackedRanges[l] = id;
956
ctx.trackedRangesDirection[l] = selection.getDirection();
957
return l.toString();
958
};
959
960
const editOperationBuilder: editorCommon.IEditOperationBuilder = {
961
addEditOperation: addEditOperation,
962
addTrackedEditOperation: addTrackedEditOperation,
963
trackSelection: trackSelection
964
};
965
966
try {
967
command.getEditOperations(ctx.model, editOperationBuilder);
968
} catch (e) {
969
// TODO@Alex use notification service if this should be user facing
970
// e.friendlyMessage = nls.localize('corrupt.commands', "Unexpected exception while executing command.");
971
onUnexpectedError(e);
972
return {
973
operations: [],
974
hadTrackedEditOperation: false
975
};
976
}
977
978
return {
979
operations: operations,
980
hadTrackedEditOperation: hadTrackedEditOperation
981
};
982
}
983
984
private static _getLoserCursorMap(operations: IIdentifiedSingleEditOperation[]): { [index: string]: boolean } {
985
// This is destructive on the array
986
operations = operations.slice(0);
987
988
// Sort operations with last one first
989
operations.sort((a: IIdentifiedSingleEditOperation, b: IIdentifiedSingleEditOperation): number => {
990
// Note the minus!
991
return -(Range.compareRangesUsingEnds(a.range, b.range));
992
});
993
994
// Operations can not overlap!
995
const loserCursorsMap: { [index: string]: boolean } = {};
996
997
for (let i = 1; i < operations.length; i++) {
998
const previousOp = operations[i - 1];
999
const currentOp = operations[i];
1000
1001
if (Range.getStartPosition(previousOp.range).isBefore(Range.getEndPosition(currentOp.range))) {
1002
1003
let loserMajor: number;
1004
1005
if (previousOp.identifier!.major > currentOp.identifier!.major) {
1006
// previousOp loses the battle
1007
loserMajor = previousOp.identifier!.major;
1008
} else {
1009
loserMajor = currentOp.identifier!.major;
1010
}
1011
1012
loserCursorsMap[loserMajor.toString()] = true;
1013
1014
for (let j = 0; j < operations.length; j++) {
1015
if (operations[j].identifier!.major === loserMajor) {
1016
operations.splice(j, 1);
1017
if (j < i) {
1018
i--;
1019
}
1020
j--;
1021
}
1022
}
1023
1024
if (i > 0) {
1025
i--;
1026
}
1027
}
1028
}
1029
1030
return loserCursorsMap;
1031
}
1032
}
1033
1034
class CompositionLineState {
1035
constructor(
1036
public readonly text: string,
1037
public readonly lineNumber: number,
1038
public readonly startSelectionOffset: number,
1039
public readonly endSelectionOffset: number
1040
) { }
1041
}
1042
1043
class CompositionState {
1044
1045
private readonly _original: CompositionLineState[] | null;
1046
1047
private static _capture(textModel: ITextModel, selections: Selection[]): CompositionLineState[] | null {
1048
const result: CompositionLineState[] = [];
1049
for (const selection of selections) {
1050
if (selection.startLineNumber !== selection.endLineNumber) {
1051
return null;
1052
}
1053
const lineNumber = selection.startLineNumber;
1054
result.push(new CompositionLineState(
1055
textModel.getLineContent(lineNumber),
1056
lineNumber,
1057
selection.startColumn - 1,
1058
selection.endColumn - 1
1059
));
1060
}
1061
return result;
1062
}
1063
1064
constructor(textModel: ITextModel, selections: Selection[]) {
1065
this._original = CompositionState._capture(textModel, selections);
1066
}
1067
1068
/**
1069
* Returns the inserted text during this composition.
1070
* If the composition resulted in existing text being changed (i.e. not a pure insertion) it returns null.
1071
*/
1072
deduceOutcome(textModel: ITextModel, selections: Selection[]): CompositionOutcome[] | null {
1073
if (!this._original) {
1074
return null;
1075
}
1076
const current = CompositionState._capture(textModel, selections);
1077
if (!current) {
1078
return null;
1079
}
1080
if (this._original.length !== current.length) {
1081
return null;
1082
}
1083
const result: CompositionOutcome[] = [];
1084
for (let i = 0, len = this._original.length; i < len; i++) {
1085
result.push(CompositionState._deduceOutcome(this._original[i], current[i]));
1086
}
1087
return result;
1088
}
1089
1090
private static _deduceOutcome(original: CompositionLineState, current: CompositionLineState): CompositionOutcome {
1091
const commonPrefix = Math.min(
1092
original.startSelectionOffset,
1093
current.startSelectionOffset,
1094
strings.commonPrefixLength(original.text, current.text)
1095
);
1096
const commonSuffix = Math.min(
1097
original.text.length - original.endSelectionOffset,
1098
current.text.length - current.endSelectionOffset,
1099
strings.commonSuffixLength(original.text, current.text)
1100
);
1101
const deletedText = original.text.substring(commonPrefix, original.text.length - commonSuffix);
1102
const insertedTextStartOffset = commonPrefix;
1103
const insertedTextEndOffset = current.text.length - commonSuffix;
1104
const insertedText = current.text.substring(insertedTextStartOffset, insertedTextEndOffset);
1105
const insertedTextRange = new Range(current.lineNumber, insertedTextStartOffset + 1, current.lineNumber, insertedTextEndOffset + 1);
1106
return new CompositionOutcome(
1107
deletedText,
1108
original.startSelectionOffset - commonPrefix,
1109
original.endSelectionOffset - commonPrefix,
1110
insertedText,
1111
current.startSelectionOffset - commonPrefix,
1112
current.endSelectionOffset - commonPrefix,
1113
insertedTextRange
1114
);
1115
}
1116
}
1117
1118