/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError } from '../../../base/common/errors.js'; import * as strings from '../../../base/common/strings.js'; import { CursorCollection } from './cursorCollection.js'; import { CursorState, EditOperationResult } from '../cursorCommon.js'; import { CursorContext } from './cursorContext.js'; import { DeleteOperations } from './cursorDeleteOperations.js'; import { CompositionOutcome, TypeOperations, TypeWithAutoClosingCommand } from './cursorTypeOperations.js'; import { Range } from '../core/range.js'; import { Selection } from '../core/selection.js'; import { ModelInjectedTextChangedEvent } from '../textModelEvents.js'; import { ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from '../viewEvents.js'; import { dispose, Disposable } from '../../../base/common/lifecycle.js'; import { CursorStateChangedEvent } from '../viewModelEventDispatcher.js'; export class CursorsController extends Disposable { constructor(model, viewModel, coordinatesConverter, cursorConfig) { super(); this._model = model; this._knownModelVersionId = this._model.getVersionId(); this._viewModel = viewModel; this._coordinatesConverter = coordinatesConverter; this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig); this._cursors = new CursorCollection(this.context); this._hasFocus = false; this._isHandling = false; this._compositionState = null; this._columnSelectData = null; this._autoClosedActions = []; this._prevEditOperationType = 0 /* EditOperationType.Other */; } dispose() { this._cursors.dispose(); this._autoClosedActions = dispose(this._autoClosedActions); super.dispose(); } updateConfiguration(cursorConfig) { this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig); this._cursors.updateContext(this.context); } onLineMappingChanged(eventsCollector) { if (this._knownModelVersionId !== this._model.getVersionId()) { // There are model change events that I didn't yet receive. // // This can happen when editing the model, and the view model receives the change events first, // and the view model emits line mapping changed events, all before the cursor gets a chance to // recover from markers. // // The model change listener above will be called soon and we'll ensure a valid cursor state there. return; } // Ensure valid state this.setStates(eventsCollector, 'viewModel', 0 /* CursorChangeReason.NotSet */, this.getCursorStates()); } setHasFocus(hasFocus) { this._hasFocus = hasFocus; } _validateAutoClosedActions() { if (this._autoClosedActions.length > 0) { const selections = this._cursors.getSelections(); for (let i = 0; i < this._autoClosedActions.length; i++) { const autoClosedAction = this._autoClosedActions[i]; if (!autoClosedAction.isValid(selections)) { autoClosedAction.dispose(); this._autoClosedActions.splice(i, 1); i--; } } } } // ------ some getters/setters getPrimaryCursorState() { return this._cursors.getPrimaryCursor(); } getLastAddedCursorIndex() { return this._cursors.getLastAddedCursorIndex(); } getCursorStates() { return this._cursors.getAll(); } setStates(eventsCollector, source, reason, states) { let reachedMaxCursorCount = false; if (states !== null && states.length > CursorsController.MAX_CURSOR_COUNT) { states = states.slice(0, CursorsController.MAX_CURSOR_COUNT); reachedMaxCursorCount = true; } const oldState = CursorModelState.from(this._model, this); this._cursors.setStates(states); this._cursors.normalize(); this._columnSelectData = null; this._validateAutoClosedActions(); return this._emitStateChangedIfNecessary(eventsCollector, source, reason, oldState, reachedMaxCursorCount); } setCursorColumnSelectData(columnSelectData) { this._columnSelectData = columnSelectData; } revealPrimary(eventsCollector, source, minimalReveal, verticalType, revealHorizontal, scrollType) { const viewPositions = this._cursors.getViewPositions(); let revealViewRange = null; let revealViewSelections = null; if (viewPositions.length > 1) { revealViewSelections = this._cursors.getViewSelections(); } else { revealViewRange = Range.fromPositions(viewPositions[0], viewPositions[0]); } eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, minimalReveal, revealViewRange, revealViewSelections, verticalType, revealHorizontal, scrollType)); } saveState() { const result = []; const selections = this._cursors.getSelections(); for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; result.push({ inSelectionMode: !selection.isEmpty(), selectionStart: { lineNumber: selection.selectionStartLineNumber, column: selection.selectionStartColumn, }, position: { lineNumber: selection.positionLineNumber, column: selection.positionColumn, } }); } return result; } restoreState(eventsCollector, states) { const desiredSelections = []; for (let i = 0, len = states.length; i < len; i++) { const state = states[i]; let positionLineNumber = 1; let positionColumn = 1; // Avoid missing properties on the literal if (state.position && state.position.lineNumber) { positionLineNumber = state.position.lineNumber; } if (state.position && state.position.column) { positionColumn = state.position.column; } let selectionStartLineNumber = positionLineNumber; let selectionStartColumn = positionColumn; // Avoid missing properties on the literal if (state.selectionStart && state.selectionStart.lineNumber) { selectionStartLineNumber = state.selectionStart.lineNumber; } if (state.selectionStart && state.selectionStart.column) { selectionStartColumn = state.selectionStart.column; } desiredSelections.push({ selectionStartLineNumber: selectionStartLineNumber, selectionStartColumn: selectionStartColumn, positionLineNumber: positionLineNumber, positionColumn: positionColumn }); } this.setStates(eventsCollector, 'restoreState', 0 /* CursorChangeReason.NotSet */, CursorState.fromModelSelections(desiredSelections)); this.revealPrimary(eventsCollector, 'restoreState', false, 0 /* VerticalRevealType.Simple */, true, 1 /* editorCommon.ScrollType.Immediate */); } onModelContentChanged(eventsCollector, event) { if (event instanceof ModelInjectedTextChangedEvent) { // If injected texts change, the view positions of all cursors need to be updated. if (this._isHandling) { // The view positions will be updated when handling finishes return; } // setStates might remove markers, which could trigger a decoration change. // If there are injected text decorations for that line, `onModelContentChanged` is emitted again // and an endless recursion happens. // _isHandling prevents that. this._isHandling = true; try { this.setStates(eventsCollector, 'modelChange', 0 /* CursorChangeReason.NotSet */, this.getCursorStates()); } finally { this._isHandling = false; } } else { const e = event.rawContentChangedEvent; this._knownModelVersionId = e.versionId; if (this._isHandling) { return; } const hadFlushEvent = e.containsEvent(1 /* RawContentChangedType.Flush */); this._prevEditOperationType = 0 /* EditOperationType.Other */; if (hadFlushEvent) { // a model.setValue() was called this._cursors.dispose(); this._cursors = new CursorCollection(this.context); this._validateAutoClosedActions(); this._emitStateChangedIfNecessary(eventsCollector, 'model', 1 /* CursorChangeReason.ContentFlush */, null, false); } else { if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) { const cursorState = CursorState.fromModelSelections(e.resultingSelection); if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? 5 /* CursorChangeReason.Undo */ : e.isRedoing ? 6 /* CursorChangeReason.Redo */ : 2 /* CursorChangeReason.RecoverFromMarkers */, cursorState)) { this.revealPrimary(eventsCollector, 'modelChange', false, 0 /* VerticalRevealType.Simple */, true, 0 /* editorCommon.ScrollType.Smooth */); } } else { const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); this.setStates(eventsCollector, 'modelChange', 2 /* CursorChangeReason.RecoverFromMarkers */, CursorState.fromModelSelections(selectionsFromMarkers)); } } } } getSelection() { return this._cursors.getPrimaryCursor().modelState.selection; } getTopMostViewPosition() { return this._cursors.getTopMostViewPosition(); } getBottomMostViewPosition() { return this._cursors.getBottomMostViewPosition(); } getCursorColumnSelectData() { if (this._columnSelectData) { return this._columnSelectData; } const primaryCursor = this._cursors.getPrimaryCursor(); const viewSelectionStart = primaryCursor.viewState.selectionStart.getStartPosition(); const viewPosition = primaryCursor.viewState.position; return { isReal: false, fromViewLineNumber: viewSelectionStart.lineNumber, fromViewVisualColumn: this.context.cursorConfig.visibleColumnFromColumn(this._viewModel, viewSelectionStart), toViewLineNumber: viewPosition.lineNumber, toViewVisualColumn: this.context.cursorConfig.visibleColumnFromColumn(this._viewModel, viewPosition), }; } getSelections() { return this._cursors.getSelections(); } setSelections(eventsCollector, source, selections, reason) { this.setStates(eventsCollector, source, reason, CursorState.fromModelSelections(selections)); } getPrevEditOperationType() { return this._prevEditOperationType; } setPrevEditOperationType(type) { this._prevEditOperationType = type; } // ------ auxiliary handling logic _pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges) { const autoClosedCharactersDeltaDecorations = []; const autoClosedEnclosingDeltaDecorations = []; for (let i = 0, len = autoClosedCharactersRanges.length; i < len; i++) { autoClosedCharactersDeltaDecorations.push({ range: autoClosedCharactersRanges[i], options: { description: 'auto-closed-character', inlineClassName: 'auto-closed-character', stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */ } }); autoClosedEnclosingDeltaDecorations.push({ range: autoClosedEnclosingRanges[i], options: { description: 'auto-closed-enclosing', stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */ } }); } const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersDeltaDecorations); const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingDeltaDecorations); this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations)); } _executeEditOperation(opResult) { if (!opResult) { // Nothing to execute return; } if (opResult.shouldPushStackElementBefore) { this._model.pushStackElement(); } const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands); if (result) { // The commands were applied correctly this._interpretCommandResult(result); // Check for auto-closing closed characters const autoClosedCharactersRanges = []; const autoClosedEnclosingRanges = []; for (let i = 0; i < opResult.commands.length; i++) { const command = opResult.commands[i]; if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { autoClosedCharactersRanges.push(command.closeCharacterRange); autoClosedEnclosingRanges.push(command.enclosingRange); } } if (autoClosedCharactersRanges.length > 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); } this._prevEditOperationType = opResult.type; } if (opResult.shouldPushStackElementAfter) { this._model.pushStackElement(); } } _interpretCommandResult(cursorState) { if (!cursorState || cursorState.length === 0) { cursorState = this._cursors.readSelectionFromMarkers(); } this._columnSelectData = null; this._cursors.setSelections(cursorState); this._cursors.normalize(); } // ----------------------------------------------------------------------------------------------------------- // ----- emitting events _emitStateChangedIfNecessary(eventsCollector, source, reason, oldState, reachedMaxCursorCount) { const newState = CursorModelState.from(this._model, this); if (newState.equals(oldState)) { return false; } const selections = this._cursors.getSelections(); const viewSelections = this._cursors.getViewSelections(); // Let the view get the event first. eventsCollector.emitViewEvent(new ViewCursorStateChangedEvent(viewSelections, selections)); // Only after the view has been notified, let the rest of the world know... if (!oldState || oldState.cursorState.length !== newState.cursorState.length || newState.cursorState.some((newCursorState, i) => !newCursorState.modelState.equals(oldState.cursorState[i].modelState))) { const oldSelections = oldState ? oldState.cursorState.map(s => s.modelState.selection) : null; const oldModelVersionId = oldState ? oldState.modelVersionId : 0; eventsCollector.emitOutgoingEvent(new CursorStateChangedEvent(oldSelections, selections, oldModelVersionId, newState.modelVersionId, source || 'keyboard', reason, reachedMaxCursorCount)); } return true; } // ----------------------------------------------------------------------------------------------------------- // ----- handlers beyond this point _findAutoClosingPairs(edits) { if (!edits.length) { return null; } const indices = []; for (let i = 0, len = edits.length; i < len; i++) { const edit = edits[i]; if (!edit.text || edit.text.indexOf('\n') >= 0) { return null; } const m = edit.text.match(/([)\]}>'"`])([^)\]}>'"`]*)$/); if (!m) { return null; } const closeChar = m[1]; const autoClosingPairsCandidates = this.context.cursorConfig.autoClosingPairs.autoClosingPairsCloseSingleChar.get(closeChar); if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) { return null; } const openChar = autoClosingPairsCandidates[0].open; const closeCharIndex = edit.text.length - m[2].length - 1; const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1); if (openCharIndex === -1) { return null; } indices.push([openCharIndex, closeCharIndex]); } return indices; } executeEdits(eventsCollector, source, edits, cursorStateComputer) { let autoClosingIndices = null; if (source === 'snippet') { autoClosingIndices = this._findAutoClosingPairs(edits); } if (autoClosingIndices) { edits[0]._isTracked = true; } const autoClosedCharactersRanges = []; const autoClosedEnclosingRanges = []; const selections = this._model.pushEditOperations(this.getSelections(), edits, (undoEdits) => { if (autoClosingIndices) { for (let i = 0, len = autoClosingIndices.length; i < len; i++) { const [openCharInnerIndex, closeCharInnerIndex] = autoClosingIndices[i]; const undoEdit = undoEdits[i]; const lineNumber = undoEdit.range.startLineNumber; const openCharIndex = undoEdit.range.startColumn - 1 + openCharInnerIndex; const closeCharIndex = undoEdit.range.startColumn - 1 + closeCharInnerIndex; autoClosedCharactersRanges.push(new Range(lineNumber, closeCharIndex + 1, lineNumber, closeCharIndex + 2)); autoClosedEnclosingRanges.push(new Range(lineNumber, openCharIndex + 1, lineNumber, closeCharIndex + 2)); } } const selections = cursorStateComputer(undoEdits); if (selections) { // Don't recover the selection from markers because // we know what it should be. this._isHandling = true; } return selections; }); if (selections) { this._isHandling = false; this.setSelections(eventsCollector, source, selections, 0 /* CursorChangeReason.NotSet */); } if (autoClosedCharactersRanges.length > 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); } } _executeEdit(callback, eventsCollector, source, cursorChangeReason = 0 /* CursorChangeReason.NotSet */) { if (this.context.cursorConfig.readOnly) { // we cannot edit when read only... return; } const oldState = CursorModelState.from(this._model, this); this._cursors.stopTrackingSelections(); this._isHandling = true; try { this._cursors.ensureValidState(); callback(); } catch (err) { onUnexpectedError(err); } this._isHandling = false; this._cursors.startTrackingSelections(); this._validateAutoClosedActions(); if (this._emitStateChangedIfNecessary(eventsCollector, source, cursorChangeReason, oldState, false)) { this.revealPrimary(eventsCollector, source, false, 0 /* VerticalRevealType.Simple */, true, 0 /* editorCommon.ScrollType.Smooth */); } } getAutoClosedCharacters() { return AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); } startComposition(eventsCollector) { this._compositionState = new CompositionState(this._model, this.getSelections()); } endComposition(eventsCollector, source) { const compositionOutcome = this._compositionState ? this._compositionState.deduceOutcome(this._model, this.getSelections()) : null; this._compositionState = null; this._executeEdit(() => { if (source === 'keyboard') { // composition finishes, let's check if we need to auto complete if necessary. this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, compositionOutcome, this.getSelections(), this.getAutoClosedCharacters())); } }, eventsCollector, source); } type(eventsCollector, text, source) { this._executeEdit(() => { if (source === 'keyboard') { // If this event is coming straight from the keyboard, look for electric characters and enter const len = text.length; let offset = 0; while (offset < len) { const charLength = strings.nextCharLength(text, offset); const chr = text.substr(offset, charLength); // Here we must interpret each typed character individually this._executeEditOperation(TypeOperations.typeWithInterceptors(!!this._compositionState, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr)); offset += charLength; } } else { this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text)); } }, eventsCollector, source); } compositionType(eventsCollector, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta, source) { if (text.length === 0 && replacePrevCharCnt === 0 && replaceNextCharCnt === 0) { // this edit is a no-op if (positionDelta !== 0) { // but it still wants to move the cursor const newSelections = this.getSelections().map(selection => { const position = selection.getPosition(); return new Selection(position.lineNumber, position.column + positionDelta, position.lineNumber, position.column + positionDelta); }); this.setSelections(eventsCollector, source, newSelections, 0 /* CursorChangeReason.NotSet */); } return; } this._executeEdit(() => { this._executeEditOperation(TypeOperations.compositionType(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); }, eventsCollector, source); } paste(eventsCollector, text, pasteOnNewLine, multicursorText, source) { this._executeEdit(() => { this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || [])); }, eventsCollector, source, 4 /* CursorChangeReason.Paste */); } cut(eventsCollector, source) { this._executeEdit(() => { this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections())); }, eventsCollector, source); } executeCommand(eventsCollector, command, source) { this._executeEdit(() => { this._cursors.killSecondaryCursors(); this._executeEditOperation(new EditOperationResult(0 /* EditOperationType.Other */, [command], { shouldPushStackElementBefore: false, shouldPushStackElementAfter: false })); }, eventsCollector, source); } executeCommands(eventsCollector, commands, source) { this._executeEdit(() => { this._executeEditOperation(new EditOperationResult(0 /* EditOperationType.Other */, commands, { shouldPushStackElementBefore: false, shouldPushStackElementAfter: false })); }, eventsCollector, source); } } CursorsController.MAX_CURSOR_COUNT = 10000; /** * A snapshot of the cursor and the model state */ class CursorModelState { constructor(modelVersionId, cursorState) { this.modelVersionId = modelVersionId; this.cursorState = cursorState; } static from(model, cursor) { return new CursorModelState(model.getVersionId(), cursor.getCursorStates()); } equals(other) { if (!other) { return false; } if (this.modelVersionId !== other.modelVersionId) { return false; } if (this.cursorState.length !== other.cursorState.length) { return false; } for (let i = 0, len = this.cursorState.length; i < len; i++) { if (!this.cursorState[i].equals(other.cursorState[i])) { return false; } } return true; } } class AutoClosedAction { constructor(model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations) { this._model = model; this._autoClosedCharactersDecorations = autoClosedCharactersDecorations; this._autoClosedEnclosingDecorations = autoClosedEnclosingDecorations; } static getAllAutoClosedCharacters(autoClosedActions) { let autoClosedCharacters = []; for (const autoClosedAction of autoClosedActions) { autoClosedCharacters = autoClosedCharacters.concat(autoClosedAction.getAutoClosedCharactersRanges()); } return autoClosedCharacters; } dispose() { this._autoClosedCharactersDecorations = this._model.deltaDecorations(this._autoClosedCharactersDecorations, []); this._autoClosedEnclosingDecorations = this._model.deltaDecorations(this._autoClosedEnclosingDecorations, []); } getAutoClosedCharactersRanges() { const result = []; for (let i = 0; i < this._autoClosedCharactersDecorations.length; i++) { const decorationRange = this._model.getDecorationRange(this._autoClosedCharactersDecorations[i]); if (decorationRange) { result.push(decorationRange); } } return result; } isValid(selections) { const enclosingRanges = []; for (let i = 0; i < this._autoClosedEnclosingDecorations.length; i++) { const decorationRange = this._model.getDecorationRange(this._autoClosedEnclosingDecorations[i]); if (decorationRange) { enclosingRanges.push(decorationRange); if (decorationRange.startLineNumber !== decorationRange.endLineNumber) { // Stop tracking if the range becomes multiline... return false; } } } enclosingRanges.sort(Range.compareRangesUsingStarts); selections.sort(Range.compareRangesUsingStarts); for (let i = 0; i < selections.length; i++) { if (i >= enclosingRanges.length) { return false; } if (!enclosingRanges[i].strictContainsRange(selections[i])) { return false; } } return true; } } class CommandExecutor { static executeCommands(model, selectionsBefore, commands) { const ctx = { model: model, selectionsBefore: selectionsBefore, trackedRanges: [], trackedRangesDirection: [] }; const result = this._innerExecuteCommands(ctx, commands); for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { ctx.model._setTrackedRange(ctx.trackedRanges[i], null, 0 /* TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges */); } return result; } static _innerExecuteCommands(ctx, commands) { if (this._arrayIsEmpty(commands)) { return null; } const commandsData = this._getEditOperations(ctx, commands); if (commandsData.operations.length === 0) { return null; } const rawOperations = commandsData.operations; const loserCursorsMap = this._getLoserCursorMap(rawOperations); if (loserCursorsMap.hasOwnProperty('0')) { // These commands are very messed up console.warn('Ignoring commands'); return null; } // Remove operations belonging to losing cursors const filteredOperations = []; for (let i = 0, len = rawOperations.length; i < len; i++) { if (!loserCursorsMap.hasOwnProperty(rawOperations[i].identifier.major.toString())) { filteredOperations.push(rawOperations[i]); } } // TODO@Alex: find a better way to do this. // give the hint that edit operations are tracked to the model if (commandsData.hadTrackedEditOperation && filteredOperations.length > 0) { filteredOperations[0]._isTracked = true; } let selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations) => { const groupedInverseEditOperations = []; for (let i = 0; i < ctx.selectionsBefore.length; i++) { groupedInverseEditOperations[i] = []; } for (const op of inverseEditOperations) { if (!op.identifier) { // perhaps auto whitespace trim edits continue; } groupedInverseEditOperations[op.identifier.major].push(op); } const minorBasedSorter = (a, b) => { return a.identifier.minor - b.identifier.minor; }; const cursorSelections = []; for (let i = 0; i < ctx.selectionsBefore.length; i++) { if (groupedInverseEditOperations[i].length > 0) { groupedInverseEditOperations[i].sort(minorBasedSorter); cursorSelections[i] = commands[i].computeCursorState(ctx.model, { getInverseEditOperations: () => { return groupedInverseEditOperations[i]; }, getTrackedSelection: (id) => { const idx = parseInt(id, 10); const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx]); if (ctx.trackedRangesDirection[idx] === 0 /* SelectionDirection.LTR */) { return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); } }); } else { cursorSelections[i] = ctx.selectionsBefore[i]; } } return cursorSelections; }); if (!selectionsAfter) { selectionsAfter = ctx.selectionsBefore; } // Extract losing cursors const losingCursors = []; for (const losingCursorIndex in loserCursorsMap) { if (loserCursorsMap.hasOwnProperty(losingCursorIndex)) { losingCursors.push(parseInt(losingCursorIndex, 10)); } } // Sort losing cursors descending losingCursors.sort((a, b) => { return b - a; }); // Remove losing cursors for (const losingCursor of losingCursors) { selectionsAfter.splice(losingCursor, 1); } return selectionsAfter; } static _arrayIsEmpty(commands) { for (let i = 0, len = commands.length; i < len; i++) { if (commands[i]) { return false; } } return true; } static _getEditOperations(ctx, commands) { let operations = []; let hadTrackedEditOperation = false; for (let i = 0, len = commands.length; i < len; i++) { const command = commands[i]; if (command) { const r = this._getEditOperationsFromCommand(ctx, i, command); operations = operations.concat(r.operations); hadTrackedEditOperation = hadTrackedEditOperation || r.hadTrackedEditOperation; } } return { operations: operations, hadTrackedEditOperation: hadTrackedEditOperation }; } static _getEditOperationsFromCommand(ctx, majorIdentifier, command) { // This method acts as a transaction, if the command fails // everything it has done is ignored const operations = []; let operationMinor = 0; const addEditOperation = (range, text, forceMoveMarkers = false) => { if (Range.isEmpty(range) && text === '') { // This command wants to add a no-op => no thank you return; } operations.push({ identifier: { major: majorIdentifier, minor: operationMinor++ }, range: range, text: text, forceMoveMarkers: forceMoveMarkers, isAutoWhitespaceEdit: command.insertsAutoWhitespace }); }; let hadTrackedEditOperation = false; const addTrackedEditOperation = (selection, text, forceMoveMarkers) => { hadTrackedEditOperation = true; addEditOperation(selection, text, forceMoveMarkers); }; const trackSelection = (_selection, trackPreviousOnEmpty) => { const selection = Selection.liftSelection(_selection); let stickiness; if (selection.isEmpty()) { if (typeof trackPreviousOnEmpty === 'boolean') { if (trackPreviousOnEmpty) { stickiness = 2 /* TrackedRangeStickiness.GrowsOnlyWhenTypingBefore */; } else { stickiness = 3 /* TrackedRangeStickiness.GrowsOnlyWhenTypingAfter */; } } else { // Try to lock it with surrounding text const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); if (selection.startColumn === maxLineColumn) { stickiness = 2 /* TrackedRangeStickiness.GrowsOnlyWhenTypingBefore */; } else { stickiness = 3 /* TrackedRangeStickiness.GrowsOnlyWhenTypingAfter */; } } } else { stickiness = 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */; } const l = ctx.trackedRanges.length; const id = ctx.model._setTrackedRange(null, selection, stickiness); ctx.trackedRanges[l] = id; ctx.trackedRangesDirection[l] = selection.getDirection(); return l.toString(); }; const editOperationBuilder = { addEditOperation: addEditOperation, addTrackedEditOperation: addTrackedEditOperation, trackSelection: trackSelection }; try { command.getEditOperations(ctx.model, editOperationBuilder); } catch (e) { // TODO@Alex use notification service if this should be user facing // e.friendlyMessage = nls.localize('corrupt.commands', "Unexpected exception while executing command."); onUnexpectedError(e); return { operations: [], hadTrackedEditOperation: false }; } return { operations: operations, hadTrackedEditOperation: hadTrackedEditOperation }; } static _getLoserCursorMap(operations) { // This is destructive on the array operations = operations.slice(0); // Sort operations with last one first operations.sort((a, b) => { // Note the minus! return -(Range.compareRangesUsingEnds(a.range, b.range)); }); // Operations can not overlap! const loserCursorsMap = {}; for (let i = 1; i < operations.length; i++) { const previousOp = operations[i - 1]; const currentOp = operations[i]; if (Range.getStartPosition(previousOp.range).isBefore(Range.getEndPosition(currentOp.range))) { let loserMajor; if (previousOp.identifier.major > currentOp.identifier.major) { // previousOp loses the battle loserMajor = previousOp.identifier.major; } else { loserMajor = currentOp.identifier.major; } loserCursorsMap[loserMajor.toString()] = true; for (let j = 0; j < operations.length; j++) { if (operations[j].identifier.major === loserMajor) { operations.splice(j, 1); if (j < i) { i--; } j--; } } if (i > 0) { i--; } } } return loserCursorsMap; } } class CompositionLineState { constructor(text, startSelection, endSelection) { this.text = text; this.startSelection = startSelection; this.endSelection = endSelection; } } class CompositionState { constructor(textModel, selections) { this._original = CompositionState._capture(textModel, selections); } static _capture(textModel, selections) { const result = []; for (const selection of selections) { if (selection.startLineNumber !== selection.endLineNumber) { return null; } result.push(new CompositionLineState(textModel.getLineContent(selection.startLineNumber), selection.startColumn - 1, selection.endColumn - 1)); } return result; } /** * Returns the inserted text during this composition. * If the composition resulted in existing text being changed (i.e. not a pure insertion) it returns null. */ deduceOutcome(textModel, selections) { if (!this._original) { return null; } const current = CompositionState._capture(textModel, selections); if (!current) { return null; } if (this._original.length !== current.length) { return null; } const result = []; for (let i = 0, len = this._original.length; i < len; i++) { result.push(CompositionState._deduceOutcome(this._original[i], current[i])); } return result; } static _deduceOutcome(original, current) { const commonPrefix = Math.min(original.startSelection, current.startSelection, strings.commonPrefixLength(original.text, current.text)); const commonSuffix = Math.min(original.text.length - original.endSelection, current.text.length - current.endSelection, strings.commonSuffixLength(original.text, current.text)); const deletedText = original.text.substring(commonPrefix, original.text.length - commonSuffix); const insertedText = current.text.substring(commonPrefix, current.text.length - commonSuffix); return new CompositionOutcome(deletedText, original.startSelection - commonPrefix, original.endSelection - commonPrefix, insertedText, current.startSelection - commonPrefix, current.endSelection - commonPrefix); } }