Path: blob/main/src/vs/editor/common/cursor/cursorCollection.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { compareBy } from '../../../base/common/arrays.js';6import { findLastMax, findFirstMin } from '../../../base/common/arraysFind.js';7import { CursorState, PartialCursorState } from '../cursorCommon.js';8import { CursorContext } from './cursorContext.js';9import { Cursor } from './oneCursor.js';10import { Position } from '../core/position.js';11import { Range } from '../core/range.js';12import { ISelection, Selection } from '../core/selection.js';1314export class CursorCollection {1516private context: CursorContext;1718/**19* `cursors[0]` is the primary cursor, thus `cursors.length >= 1` is always true.20* `cursors.slice(1)` are secondary cursors.21*/22private cursors: Cursor[];2324// An index which identifies the last cursor that was added / moved (think Ctrl+drag)25// This index refers to `cursors.slice(1)`, i.e. after removing the primary cursor.26private lastAddedCursorIndex: number;2728constructor(context: CursorContext) {29this.context = context;30this.cursors = [new Cursor(context)];31this.lastAddedCursorIndex = 0;32}3334public dispose(): void {35for (const cursor of this.cursors) {36cursor.dispose(this.context);37}38}3940public startTrackingSelections(): void {41for (const cursor of this.cursors) {42cursor.startTrackingSelection(this.context);43}44}4546public stopTrackingSelections(): void {47for (const cursor of this.cursors) {48cursor.stopTrackingSelection(this.context);49}50}5152public updateContext(context: CursorContext): void {53this.context = context;54}5556public ensureValidState(): void {57for (const cursor of this.cursors) {58cursor.ensureValidState(this.context);59}60}6162public readSelectionFromMarkers(): Selection[] {63return this.cursors.map(c => c.readSelectionFromMarkers(this.context));64}6566public getAll(): CursorState[] {67return this.cursors.map(c => c.asCursorState());68}6970public getViewPositions(): Position[] {71return this.cursors.map(c => c.viewState.position);72}7374public getTopMostViewPosition(): Position {75return findFirstMin(76this.cursors,77compareBy(c => c.viewState.position, Position.compare)78)!.viewState.position;79}8081public getBottomMostViewPosition(): Position {82return findLastMax(83this.cursors,84compareBy(c => c.viewState.position, Position.compare)85)!.viewState.position;86}8788public getSelections(): Selection[] {89return this.cursors.map(c => c.modelState.selection);90}9192public getViewSelections(): Selection[] {93return this.cursors.map(c => c.viewState.selection);94}9596public setSelections(selections: ISelection[]): void {97this.setStates(CursorState.fromModelSelections(selections));98}99100public getPrimaryCursor(): CursorState {101return this.cursors[0].asCursorState();102}103104public setStates(states: PartialCursorState[] | null): void {105if (states === null) {106return;107}108this.cursors[0].setState(this.context, states[0].modelState, states[0].viewState);109this._setSecondaryStates(states.slice(1));110}111112/**113* Creates or disposes secondary cursors as necessary to match the number of `secondarySelections`.114*/115private _setSecondaryStates(secondaryStates: PartialCursorState[]): void {116const secondaryCursorsLength = this.cursors.length - 1;117const secondaryStatesLength = secondaryStates.length;118119if (secondaryCursorsLength < secondaryStatesLength) {120const createCnt = secondaryStatesLength - secondaryCursorsLength;121for (let i = 0; i < createCnt; i++) {122this._addSecondaryCursor();123}124} else if (secondaryCursorsLength > secondaryStatesLength) {125const removeCnt = secondaryCursorsLength - secondaryStatesLength;126for (let i = 0; i < removeCnt; i++) {127this._removeSecondaryCursor(this.cursors.length - 2);128}129}130131for (let i = 0; i < secondaryStatesLength; i++) {132this.cursors[i + 1].setState(this.context, secondaryStates[i].modelState, secondaryStates[i].viewState);133}134}135136public killSecondaryCursors(): void {137this._setSecondaryStates([]);138}139140private _addSecondaryCursor(): void {141this.cursors.push(new Cursor(this.context));142this.lastAddedCursorIndex = this.cursors.length - 1;143}144145public getLastAddedCursorIndex(): number {146if (this.cursors.length === 1 || this.lastAddedCursorIndex === 0) {147return 0;148}149return this.lastAddedCursorIndex;150}151152private _removeSecondaryCursor(removeIndex: number): void {153if (this.lastAddedCursorIndex >= removeIndex + 1) {154this.lastAddedCursorIndex--;155}156this.cursors[removeIndex + 1].dispose(this.context);157this.cursors.splice(removeIndex + 1, 1);158}159160public normalize(): void {161if (this.cursors.length === 1) {162return;163}164const cursors = this.cursors.slice(0);165166interface SortedCursor {167index: number;168selection: Selection;169}170const sortedCursors: SortedCursor[] = [];171for (let i = 0, len = cursors.length; i < len; i++) {172sortedCursors.push({173index: i,174selection: cursors[i].modelState.selection,175});176}177178sortedCursors.sort(compareBy(s => s.selection, Range.compareRangesUsingStarts));179180for (let sortedCursorIndex = 0; sortedCursorIndex < sortedCursors.length - 1; sortedCursorIndex++) {181const current = sortedCursors[sortedCursorIndex];182const next = sortedCursors[sortedCursorIndex + 1];183184const currentSelection = current.selection;185const nextSelection = next.selection;186187if (!this.context.cursorConfig.multiCursorMergeOverlapping) {188continue;189}190191let shouldMergeCursors: boolean;192if (nextSelection.isEmpty() || currentSelection.isEmpty()) {193// Merge touching cursors if one of them is collapsed194shouldMergeCursors = nextSelection.getStartPosition().isBeforeOrEqual(currentSelection.getEndPosition());195} else {196// Merge only overlapping cursors (i.e. allow touching ranges)197shouldMergeCursors = nextSelection.getStartPosition().isBefore(currentSelection.getEndPosition());198}199200if (shouldMergeCursors) {201const winnerSortedCursorIndex = current.index < next.index ? sortedCursorIndex : sortedCursorIndex + 1;202const looserSortedCursorIndex = current.index < next.index ? sortedCursorIndex + 1 : sortedCursorIndex;203204const looserIndex = sortedCursors[looserSortedCursorIndex].index;205const winnerIndex = sortedCursors[winnerSortedCursorIndex].index;206207const looserSelection = sortedCursors[looserSortedCursorIndex].selection;208const winnerSelection = sortedCursors[winnerSortedCursorIndex].selection;209210if (!looserSelection.equalsSelection(winnerSelection)) {211const resultingRange = looserSelection.plusRange(winnerSelection);212const looserSelectionIsLTR = (looserSelection.selectionStartLineNumber === looserSelection.startLineNumber && looserSelection.selectionStartColumn === looserSelection.startColumn);213const winnerSelectionIsLTR = (winnerSelection.selectionStartLineNumber === winnerSelection.startLineNumber && winnerSelection.selectionStartColumn === winnerSelection.startColumn);214215// Give more importance to the last added cursor (think Ctrl-dragging + hitting another cursor)216let resultingSelectionIsLTR: boolean;217if (looserIndex === this.lastAddedCursorIndex) {218resultingSelectionIsLTR = looserSelectionIsLTR;219this.lastAddedCursorIndex = winnerIndex;220} else {221// Winner takes it all222resultingSelectionIsLTR = winnerSelectionIsLTR;223}224225let resultingSelection: Selection;226if (resultingSelectionIsLTR) {227resultingSelection = new Selection(resultingRange.startLineNumber, resultingRange.startColumn, resultingRange.endLineNumber, resultingRange.endColumn);228} else {229resultingSelection = new Selection(resultingRange.endLineNumber, resultingRange.endColumn, resultingRange.startLineNumber, resultingRange.startColumn);230}231232sortedCursors[winnerSortedCursorIndex].selection = resultingSelection;233const resultingState = CursorState.fromModelSelection(resultingSelection);234cursors[winnerIndex].setState(this.context, resultingState.modelState, resultingState.viewState);235}236237for (const sortedCursor of sortedCursors) {238if (sortedCursor.index > looserIndex) {239sortedCursor.index--;240}241}242243cursors.splice(looserIndex, 1);244sortedCursors.splice(looserSortedCursorIndex, 1);245this._removeSecondaryCursor(looserIndex - 1);246247sortedCursorIndex--;248}249}250}251}252253254