Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/cursor/cursorCollection.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 { compareBy } from '../../../base/common/arrays.js';
7
import { findLastMax, findFirstMin } from '../../../base/common/arraysFind.js';
8
import { CursorState, PartialCursorState } from '../cursorCommon.js';
9
import { CursorContext } from './cursorContext.js';
10
import { Cursor } from './oneCursor.js';
11
import { Position } from '../core/position.js';
12
import { Range } from '../core/range.js';
13
import { ISelection, Selection } from '../core/selection.js';
14
15
export class CursorCollection {
16
17
private context: CursorContext;
18
19
/**
20
* `cursors[0]` is the primary cursor, thus `cursors.length >= 1` is always true.
21
* `cursors.slice(1)` are secondary cursors.
22
*/
23
private cursors: Cursor[];
24
25
// An index which identifies the last cursor that was added / moved (think Ctrl+drag)
26
// This index refers to `cursors.slice(1)`, i.e. after removing the primary cursor.
27
private lastAddedCursorIndex: number;
28
29
constructor(context: CursorContext) {
30
this.context = context;
31
this.cursors = [new Cursor(context)];
32
this.lastAddedCursorIndex = 0;
33
}
34
35
public dispose(): void {
36
for (const cursor of this.cursors) {
37
cursor.dispose(this.context);
38
}
39
}
40
41
public startTrackingSelections(): void {
42
for (const cursor of this.cursors) {
43
cursor.startTrackingSelection(this.context);
44
}
45
}
46
47
public stopTrackingSelections(): void {
48
for (const cursor of this.cursors) {
49
cursor.stopTrackingSelection(this.context);
50
}
51
}
52
53
public updateContext(context: CursorContext): void {
54
this.context = context;
55
}
56
57
public ensureValidState(): void {
58
for (const cursor of this.cursors) {
59
cursor.ensureValidState(this.context);
60
}
61
}
62
63
public readSelectionFromMarkers(): Selection[] {
64
return this.cursors.map(c => c.readSelectionFromMarkers(this.context));
65
}
66
67
public getAll(): CursorState[] {
68
return this.cursors.map(c => c.asCursorState());
69
}
70
71
public getViewPositions(): Position[] {
72
return this.cursors.map(c => c.viewState.position);
73
}
74
75
public getTopMostViewPosition(): Position {
76
return findFirstMin(
77
this.cursors,
78
compareBy(c => c.viewState.position, Position.compare)
79
)!.viewState.position;
80
}
81
82
public getBottomMostViewPosition(): Position {
83
return findLastMax(
84
this.cursors,
85
compareBy(c => c.viewState.position, Position.compare)
86
)!.viewState.position;
87
}
88
89
public getSelections(): Selection[] {
90
return this.cursors.map(c => c.modelState.selection);
91
}
92
93
public getViewSelections(): Selection[] {
94
return this.cursors.map(c => c.viewState.selection);
95
}
96
97
public setSelections(selections: ISelection[]): void {
98
this.setStates(CursorState.fromModelSelections(selections));
99
}
100
101
public getPrimaryCursor(): CursorState {
102
return this.cursors[0].asCursorState();
103
}
104
105
public setStates(states: PartialCursorState[] | null): void {
106
if (states === null) {
107
return;
108
}
109
this.cursors[0].setState(this.context, states[0].modelState, states[0].viewState);
110
this._setSecondaryStates(states.slice(1));
111
}
112
113
/**
114
* Creates or disposes secondary cursors as necessary to match the number of `secondarySelections`.
115
*/
116
private _setSecondaryStates(secondaryStates: PartialCursorState[]): void {
117
const secondaryCursorsLength = this.cursors.length - 1;
118
const secondaryStatesLength = secondaryStates.length;
119
120
if (secondaryCursorsLength < secondaryStatesLength) {
121
const createCnt = secondaryStatesLength - secondaryCursorsLength;
122
for (let i = 0; i < createCnt; i++) {
123
this._addSecondaryCursor();
124
}
125
} else if (secondaryCursorsLength > secondaryStatesLength) {
126
const removeCnt = secondaryCursorsLength - secondaryStatesLength;
127
for (let i = 0; i < removeCnt; i++) {
128
this._removeSecondaryCursor(this.cursors.length - 2);
129
}
130
}
131
132
for (let i = 0; i < secondaryStatesLength; i++) {
133
this.cursors[i + 1].setState(this.context, secondaryStates[i].modelState, secondaryStates[i].viewState);
134
}
135
}
136
137
public killSecondaryCursors(): void {
138
this._setSecondaryStates([]);
139
}
140
141
private _addSecondaryCursor(): void {
142
this.cursors.push(new Cursor(this.context));
143
this.lastAddedCursorIndex = this.cursors.length - 1;
144
}
145
146
public getLastAddedCursorIndex(): number {
147
if (this.cursors.length === 1 || this.lastAddedCursorIndex === 0) {
148
return 0;
149
}
150
return this.lastAddedCursorIndex;
151
}
152
153
private _removeSecondaryCursor(removeIndex: number): void {
154
if (this.lastAddedCursorIndex >= removeIndex + 1) {
155
this.lastAddedCursorIndex--;
156
}
157
this.cursors[removeIndex + 1].dispose(this.context);
158
this.cursors.splice(removeIndex + 1, 1);
159
}
160
161
public normalize(): void {
162
if (this.cursors.length === 1) {
163
return;
164
}
165
const cursors = this.cursors.slice(0);
166
167
interface SortedCursor {
168
index: number;
169
selection: Selection;
170
}
171
const sortedCursors: SortedCursor[] = [];
172
for (let i = 0, len = cursors.length; i < len; i++) {
173
sortedCursors.push({
174
index: i,
175
selection: cursors[i].modelState.selection,
176
});
177
}
178
179
sortedCursors.sort(compareBy(s => s.selection, Range.compareRangesUsingStarts));
180
181
for (let sortedCursorIndex = 0; sortedCursorIndex < sortedCursors.length - 1; sortedCursorIndex++) {
182
const current = sortedCursors[sortedCursorIndex];
183
const next = sortedCursors[sortedCursorIndex + 1];
184
185
const currentSelection = current.selection;
186
const nextSelection = next.selection;
187
188
if (!this.context.cursorConfig.multiCursorMergeOverlapping) {
189
continue;
190
}
191
192
let shouldMergeCursors: boolean;
193
if (nextSelection.isEmpty() || currentSelection.isEmpty()) {
194
// Merge touching cursors if one of them is collapsed
195
shouldMergeCursors = nextSelection.getStartPosition().isBeforeOrEqual(currentSelection.getEndPosition());
196
} else {
197
// Merge only overlapping cursors (i.e. allow touching ranges)
198
shouldMergeCursors = nextSelection.getStartPosition().isBefore(currentSelection.getEndPosition());
199
}
200
201
if (shouldMergeCursors) {
202
const winnerSortedCursorIndex = current.index < next.index ? sortedCursorIndex : sortedCursorIndex + 1;
203
const looserSortedCursorIndex = current.index < next.index ? sortedCursorIndex + 1 : sortedCursorIndex;
204
205
const looserIndex = sortedCursors[looserSortedCursorIndex].index;
206
const winnerIndex = sortedCursors[winnerSortedCursorIndex].index;
207
208
const looserSelection = sortedCursors[looserSortedCursorIndex].selection;
209
const winnerSelection = sortedCursors[winnerSortedCursorIndex].selection;
210
211
if (!looserSelection.equalsSelection(winnerSelection)) {
212
const resultingRange = looserSelection.plusRange(winnerSelection);
213
const looserSelectionIsLTR = (looserSelection.selectionStartLineNumber === looserSelection.startLineNumber && looserSelection.selectionStartColumn === looserSelection.startColumn);
214
const winnerSelectionIsLTR = (winnerSelection.selectionStartLineNumber === winnerSelection.startLineNumber && winnerSelection.selectionStartColumn === winnerSelection.startColumn);
215
216
// Give more importance to the last added cursor (think Ctrl-dragging + hitting another cursor)
217
let resultingSelectionIsLTR: boolean;
218
if (looserIndex === this.lastAddedCursorIndex) {
219
resultingSelectionIsLTR = looserSelectionIsLTR;
220
this.lastAddedCursorIndex = winnerIndex;
221
} else {
222
// Winner takes it all
223
resultingSelectionIsLTR = winnerSelectionIsLTR;
224
}
225
226
let resultingSelection: Selection;
227
if (resultingSelectionIsLTR) {
228
resultingSelection = new Selection(resultingRange.startLineNumber, resultingRange.startColumn, resultingRange.endLineNumber, resultingRange.endColumn);
229
} else {
230
resultingSelection = new Selection(resultingRange.endLineNumber, resultingRange.endColumn, resultingRange.startLineNumber, resultingRange.startColumn);
231
}
232
233
sortedCursors[winnerSortedCursorIndex].selection = resultingSelection;
234
const resultingState = CursorState.fromModelSelection(resultingSelection);
235
cursors[winnerIndex].setState(this.context, resultingState.modelState, resultingState.viewState);
236
}
237
238
for (const sortedCursor of sortedCursors) {
239
if (sortedCursor.index > looserIndex) {
240
sortedCursor.index--;
241
}
242
}
243
244
cursors.splice(looserIndex, 1);
245
sortedCursors.splice(looserSortedCursorIndex, 1);
246
this._removeSecondaryCursor(looserIndex - 1);
247
248
sortedCursorIndex--;
249
}
250
}
251
}
252
}
253
254