diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index f080795bf6d845a22a4dcb6c730dd34a4e904b17..3c35af418f36800341bcfb35fbb8c3e1b03f5699 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -33,11 +33,6 @@ class CursorOperationArgs { } } -interface IExecContext { - selectionStartMarkers: string[]; - positionMarkers: string[]; -} - interface ICommandData { operations: editorCommon.IIdentifiedSingleEditOperation[]; hadTrackedEditOperation: boolean; @@ -343,17 +338,11 @@ export class Cursor extends Disposable implements ICursors { this._columnSelectData = null; - if (opResult) { - const execCtx: IExecContext = { - selectionStartMarkers: [], - positionMarkers: [] - }; - - this._innerExecuteCommands(execCtx, opResult.commands); - - for (let i = 0; i < execCtx.selectionStartMarkers.length; i++) { - this._model._removeMarker(execCtx.selectionStartMarkers[i]); - this._model._removeMarker(execCtx.positionMarkers[i]); + if (opResult && !this._configuration.editor.readOnly) { + const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands); + if (result) { + // The commands were applied correctly + this._interpretCommandResult(result); } } @@ -423,411 +412,138 @@ export class Cursor extends Disposable implements ICursors { this._cursors.setSelections(cursorState); } - private _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand): ICommandData { - // This method acts as a transaction, if the command fails - // everything it has done is ignored - let operations: editorCommon.IIdentifiedSingleEditOperation[] = []; - let operationMinor = 0; - - const addEditOperation = (selection: Range, text: string) => { - if (selection.isEmpty() && text === '') { - // This command wants to add a no-op => no thank you - return; - } - operations.push({ - identifier: { - major: majorIdentifier, - minor: operationMinor++ - }, - range: selection, - text: text, - forceMoveMarkers: false, - isAutoWhitespaceEdit: command.insertsAutoWhitespace - }); - }; + // ----------------------------------------------------------------------------------------------------------- + // ----- emitting events - let hadTrackedEditOperation = false; - const addTrackedEditOperation = (selection: Range, text: string) => { - hadTrackedEditOperation = true; - addEditOperation(selection, text); - }; + private emitCursorPositionChanged(source: string, reason: CursorChangeReason): void { + const positions = this._cursors.getPositions(); + const primaryPosition = positions[0]; + const secondaryPositions = positions.slice(1); - const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { - let selectionMarkerStickToPreviousCharacter: boolean; - let positionMarkerStickToPreviousCharacter: boolean; + const viewPositions = this._cursors.getViewPositions(); + const primaryViewPosition = viewPositions[0]; + const secondaryViewPositions = viewPositions.slice(1); - if (selection.isEmpty()) { - // Try to lock it with surrounding text - if (typeof trackPreviousOnEmpty === 'boolean') { - selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - } else { - const maxLineColumn = this._model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn === maxLineColumn) { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = true; - } else { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = false; - } - } - } else { - if (selection.getDirection() === SelectionDirection.LTR) { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = true; - } else { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = false; - } + let isInEditableRange: boolean = true; + if (this._model.hasEditableRange()) { + const editableRange = this._model.getEditableRange(); + if (!editableRange.containsPosition(primaryPosition)) { + isInEditableRange = false; } - - const l = ctx.selectionStartMarkers.length; - ctx.selectionStartMarkers[l] = this._model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter); - ctx.positionMarkers[l] = this._model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter); - return l.toString(); - }; - - const editOperationBuilder: editorCommon.IEditOperationBuilder = { - addEditOperation: addEditOperation, - addTrackedEditOperation: addTrackedEditOperation, - trackSelection: trackSelection - }; - - try { - command.getEditOperations(this._model, editOperationBuilder); - } catch (e) { - e.friendlyMessage = nls.localize('corrupt.commands', "Unexpected exception while executing command."); - onUnexpectedError(e); - return { - operations: [], - hadTrackedEditOperation: false - }; } - - return { - operations: operations, - hadTrackedEditOperation: hadTrackedEditOperation + const e: ICursorPositionChangedEvent = { + position: primaryPosition, + viewPosition: primaryViewPosition, + secondaryPositions: secondaryPositions, + secondaryViewPositions: secondaryViewPositions, + reason: reason, + source: source, + isInEditableRange: isInEditableRange }; + this._eventEmitter.emit(CursorEventType.CursorPositionChanged, e); } - private _getEditOperations(ctx: IExecContext, commands: editorCommon.ICommand[]): ICommandsData { - let operations: editorCommon.IIdentifiedSingleEditOperation[] = []; - let hadTrackedEditOperation: boolean = false; + private emitCursorSelectionChanged(source: string, reason: CursorChangeReason): void { + const selections = this._cursors.getSelections(); + const primarySelection = selections[0]; + const secondarySelections = selections.slice(1); - for (let i = 0, len = commands.length; i < len; i++) { - if (commands[i]) { - const r = this._getEditOperationsFromCommand(ctx, i, commands[i]); - operations = operations.concat(r.operations); - hadTrackedEditOperation = hadTrackedEditOperation || r.hadTrackedEditOperation; - } - } - return { - operations: operations, - hadTrackedEditOperation: hadTrackedEditOperation + const viewSelections = this._cursors.getViewSelections(); + const primaryViewSelection = viewSelections[0]; + const secondaryViewSelections = viewSelections.slice(1); + + const e: ICursorSelectionChangedEvent = { + selection: primarySelection, + viewSelection: primaryViewSelection, + secondarySelections: secondarySelections, + secondaryViewSelections: secondaryViewSelections, + source: source || 'keyboard', + reason: reason }; + this._eventEmitter.emit(CursorEventType.CursorSelectionChanged, e); } - private _getLoserCursorMap(operations: editorCommon.IIdentifiedSingleEditOperation[]): { [index: string]: boolean; } { - // This is destructive on the array - operations = operations.slice(0); - - // Sort operations with last one first - operations.sort((a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation): number => { - // Note the minus! - return -(Range.compareRangesUsingEnds(a.range, b.range)); - }); - - // Operations can not overlap! - let loserCursorsMap: { [index: string]: boolean; } = {}; - - for (let i = 1; i < operations.length; i++) { - const previousOp = operations[i - 1]; - const currentOp = operations[i]; - - if (previousOp.range.getStartPosition().isBefore(currentOp.range.getEndPosition())) { - - let loserMajor: number; - - if (previousOp.identifier.major > currentOp.identifier.major) { - // previousOp loses the battle - loserMajor = previousOp.identifier.major; - } else { - loserMajor = currentOp.identifier.major; - } + private _revealRange(revealTarget: RevealTarget, verticalType: VerticalRevealType, revealHorizontal: boolean): void { + const positions = this._cursors.getPositions(); + const viewPositions = this._cursors.getViewPositions(); - loserCursorsMap[loserMajor.toString()] = true; + let position = positions[0]; + let viewPosition = viewPositions[0]; - for (let j = 0; j < operations.length; j++) { - if (operations[j].identifier.major === loserMajor) { - operations.splice(j, 1); - if (j < i) { - i--; - } - j--; - } + if (revealTarget === RevealTarget.TopMost) { + for (let i = 1; i < positions.length; i++) { + if (positions[i].isBefore(position)) { + position = positions[i]; + viewPosition = viewPositions[i]; } - - if (i > 0) { - i--; + } + } else if (revealTarget === RevealTarget.BottomMost) { + for (let i = 1; i < positions.length; i++) { + if (position.isBeforeOrEqual(positions[i])) { + position = positions[i]; + viewPosition = viewPositions[i]; } } + } else { + if (positions.length > 1) { + // no revealing! + return; + } } - return loserCursorsMap; + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); + this.emitCursorRevealRange(range, viewRange, verticalType, revealHorizontal); } - private _arrayIsEmpty(commands: editorCommon.ICommand[]): boolean { - for (let i = 0, len = commands.length; i < len; i++) { - if (commands[i]) { - return false; - } - } - return true; + public emitCursorRevealRange(range: Range, viewRange: Range, verticalType: VerticalRevealType, revealHorizontal: boolean) { + const e: ICursorRevealRangeEvent = { + range: range, + viewRange: viewRange, + verticalType: verticalType, + revealHorizontal: revealHorizontal + }; + this._eventEmitter.emit(CursorEventType.CursorRevealRange, e); } - private _innerExecuteCommands(ctx: IExecContext, commands: editorCommon.ICommand[]): void { + // ----------------------------------------------------------------------------------------------------------- + // ----- handlers beyond this point - if (this._configuration.editor.readOnly) { - return; - } + public trigger(source: string, handlerId: string, payload: any): void { + if (!this._handlers.hasOwnProperty(handlerId)) { + const command = CommonEditorRegistry.getEditorCommand(handlerId); + if (!command || !(command instanceof CoreEditorCommand)) { + return; + } - if (this._arrayIsEmpty(commands)) { + payload = payload || {}; + payload.source = source; + command.runCoreEditorCommand(this, payload); return; } + const handler = this._handlers[handlerId]; + const args = new CursorOperationArgs(source, payload); + this._onHandler(handlerId, handler, args); + } - const selectionsBefore = this._cursors.getSelections(); + private _registerHandlers(): void { + let H = editorCommon.Handler; - const commandsData = this._getEditOperations(ctx, commands); - if (commandsData.operations.length === 0) { - return; - } + this._handlers[H.LineInsertBefore] = (args) => this._lineInsertBefore(args); + this._handlers[H.LineInsertAfter] = (args) => this._lineInsertAfter(args); + this._handlers[H.LineBreakInsert] = (args) => this._lineBreakInsert(args); - const rawOperations = commandsData.operations; + this._handlers[H.Type] = (args) => this._type(args); + this._handlers[H.ReplacePreviousChar] = (args) => this._replacePreviousChar(args); + this._handlers[H.CompositionStart] = (args) => this._compositionStart(args); + this._handlers[H.CompositionEnd] = (args) => this._compositionEnd(args); + this._handlers[H.Tab] = (args) => this._tab(args); + this._handlers[H.Indent] = (args) => this._indent(args); + this._handlers[H.Outdent] = (args) => this._outdent(args); + this._handlers[H.Paste] = (args) => this._paste(args); - const editableRange = this._model.getEditableRange(); - const editableRangeStart = editableRange.getStartPosition(); - const editableRangeEnd = editableRange.getEndPosition(); - for (let i = 0, len = rawOperations.length; i < len; i++) { - const operationRange = rawOperations[i].range; - if (!editableRangeStart.isBeforeOrEqual(operationRange.getStartPosition()) || !operationRange.getEndPosition().isBeforeOrEqual(editableRangeEnd)) { - // These commands are outside of the editable range - return; - } - } - - const loserCursorsMap = this._getLoserCursorMap(rawOperations); - if (loserCursorsMap.hasOwnProperty('0')) { - // These commands are very messed up - console.warn('Ignoring commands'); - return; - } - - // Remove operations belonging to losing cursors - let filteredOperations: editorCommon.IIdentifiedSingleEditOperation[] = []; - 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; - } - const selectionsAfter = this._model.pushEditOperations(selectionsBefore, filteredOperations, (inverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[]): Selection[] => { - let groupedInverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[][] = []; - for (let i = 0; i < selectionsBefore.length; i++) { - groupedInverseEditOperations[i] = []; - } - for (let i = 0; i < inverseEditOperations.length; i++) { - const op = inverseEditOperations[i]; - if (!op.identifier) { - // perhaps auto whitespace trim edits - continue; - } - groupedInverseEditOperations[op.identifier.major].push(op); - } - const minorBasedSorter = (a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation) => { - return a.identifier.minor - b.identifier.minor; - }; - let cursorSelections: Selection[] = []; - for (let i = 0; i < selectionsBefore.length; i++) { - if (groupedInverseEditOperations[i].length > 0) { - groupedInverseEditOperations[i].sort(minorBasedSorter); - cursorSelections[i] = commands[i].computeCursorState(this._model, { - getInverseEditOperations: () => { - return groupedInverseEditOperations[i]; - }, - - getTrackedSelection: (id: string) => { - const idx = parseInt(id, 10); - const selectionStartMarker = this._model._getMarker(ctx.selectionStartMarkers[idx]); - const positionMarker = this._model._getMarker(ctx.positionMarkers[idx]); - return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column); - } - }); - } else { - cursorSelections[i] = selectionsBefore[i]; - } - } - return cursorSelections; - }); - - // Extract losing cursors - let losingCursors: number[] = []; - for (let losingCursorIndex in loserCursorsMap) { - if (loserCursorsMap.hasOwnProperty(losingCursorIndex)) { - losingCursors.push(parseInt(losingCursorIndex, 10)); - } - } - - // Sort losing cursors descending - losingCursors.sort((a: number, b: number): number => { - return b - a; - }); - - // Remove losing cursors - for (let i = 0; i < losingCursors.length; i++) { - selectionsAfter.splice(losingCursors[i], 1); - } - - this._interpretCommandResult(selectionsAfter); - } - - - // ----------------------------------------------------------------------------------------------------------- - // ----- emitting events - - private emitCursorPositionChanged(source: string, reason: CursorChangeReason): void { - const positions = this._cursors.getPositions(); - const primaryPosition = positions[0]; - const secondaryPositions = positions.slice(1); - - const viewPositions = this._cursors.getViewPositions(); - const primaryViewPosition = viewPositions[0]; - const secondaryViewPositions = viewPositions.slice(1); - - let isInEditableRange: boolean = true; - if (this._model.hasEditableRange()) { - const editableRange = this._model.getEditableRange(); - if (!editableRange.containsPosition(primaryPosition)) { - isInEditableRange = false; - } - } - const e: ICursorPositionChangedEvent = { - position: primaryPosition, - viewPosition: primaryViewPosition, - secondaryPositions: secondaryPositions, - secondaryViewPositions: secondaryViewPositions, - reason: reason, - source: source, - isInEditableRange: isInEditableRange - }; - this._eventEmitter.emit(CursorEventType.CursorPositionChanged, e); - } - - private emitCursorSelectionChanged(source: string, reason: CursorChangeReason): void { - const selections = this._cursors.getSelections(); - const primarySelection = selections[0]; - const secondarySelections = selections.slice(1); - - const viewSelections = this._cursors.getViewSelections(); - const primaryViewSelection = viewSelections[0]; - const secondaryViewSelections = viewSelections.slice(1); - - const e: ICursorSelectionChangedEvent = { - selection: primarySelection, - viewSelection: primaryViewSelection, - secondarySelections: secondarySelections, - secondaryViewSelections: secondaryViewSelections, - source: source || 'keyboard', - reason: reason - }; - this._eventEmitter.emit(CursorEventType.CursorSelectionChanged, e); - } - - private _revealRange(revealTarget: RevealTarget, verticalType: VerticalRevealType, revealHorizontal: boolean): void { - const positions = this._cursors.getPositions(); - const viewPositions = this._cursors.getViewPositions(); - - let position = positions[0]; - let viewPosition = viewPositions[0]; - - if (revealTarget === RevealTarget.TopMost) { - for (let i = 1; i < positions.length; i++) { - if (positions[i].isBefore(position)) { - position = positions[i]; - viewPosition = viewPositions[i]; - } - } - } else if (revealTarget === RevealTarget.BottomMost) { - for (let i = 1; i < positions.length; i++) { - if (position.isBeforeOrEqual(positions[i])) { - position = positions[i]; - viewPosition = viewPositions[i]; - } - } - } else { - if (positions.length > 1) { - // no revealing! - return; - } - } - - const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); - this.emitCursorRevealRange(range, viewRange, verticalType, revealHorizontal); - } - - public emitCursorRevealRange(range: Range, viewRange: Range, verticalType: VerticalRevealType, revealHorizontal: boolean) { - const e: ICursorRevealRangeEvent = { - range: range, - viewRange: viewRange, - verticalType: verticalType, - revealHorizontal: revealHorizontal - }; - this._eventEmitter.emit(CursorEventType.CursorRevealRange, e); - } - - // ----------------------------------------------------------------------------------------------------------- - // ----- handlers beyond this point - - public trigger(source: string, handlerId: string, payload: any): void { - if (!this._handlers.hasOwnProperty(handlerId)) { - const command = CommonEditorRegistry.getEditorCommand(handlerId); - if (!command || !(command instanceof CoreEditorCommand)) { - return; - } - - payload = payload || {}; - payload.source = source; - command.runCoreEditorCommand(this, payload); - return; - } - const handler = this._handlers[handlerId]; - const args = new CursorOperationArgs(source, payload); - this._onHandler(handlerId, handler, args); - } - - private _registerHandlers(): void { - let H = editorCommon.Handler; - - this._handlers[H.LineInsertBefore] = (args) => this._lineInsertBefore(args); - this._handlers[H.LineInsertAfter] = (args) => this._lineInsertAfter(args); - this._handlers[H.LineBreakInsert] = (args) => this._lineBreakInsert(args); - - this._handlers[H.Type] = (args) => this._type(args); - this._handlers[H.ReplacePreviousChar] = (args) => this._replacePreviousChar(args); - this._handlers[H.CompositionStart] = (args) => this._compositionStart(args); - this._handlers[H.CompositionEnd] = (args) => this._compositionEnd(args); - this._handlers[H.Tab] = (args) => this._tab(args); - this._handlers[H.Indent] = (args) => this._indent(args); - this._handlers[H.Outdent] = (args) => this._outdent(args); - this._handlers[H.Paste] = (args) => this._paste(args); - - this._handlers[H.DeleteLeft] = (args) => this._deleteLeft(args); - this._handlers[H.DeleteRight] = (args) => this._deleteRight(args); + this._handlers[H.DeleteLeft] = (args) => this._deleteLeft(args); + this._handlers[H.DeleteRight] = (args) => this._deleteRight(args); this._handlers[H.Cut] = (args) => this._cut(args); @@ -1027,3 +743,298 @@ export class Cursor extends Disposable implements ICursors { }); } } + +interface IExecContext { + readonly model: editorCommon.IModel; + readonly selectionsBefore: Selection[]; + readonly selectionStartMarkers: string[]; + readonly positionMarkers: string[]; +} + +class CommandExecutor { + + public static executeCommands(model: editorCommon.IModel, selectionsBefore: Selection[], commands: editorCommon.ICommand[]): Selection[] { + + const ctx: IExecContext = { + model: model, + selectionsBefore: selectionsBefore, + selectionStartMarkers: [], + positionMarkers: [] + }; + + const result = this._innerExecuteCommands(ctx, commands); + + for (let i = 0; i < ctx.selectionStartMarkers.length; i++) { + ctx.model._removeMarker(ctx.selectionStartMarkers[i]); + ctx.model._removeMarker(ctx.positionMarkers[i]); + } + + return result; + } + + private static _innerExecuteCommands(ctx: IExecContext, commands: editorCommon.ICommand[]): Selection[] { + + if (this._arrayIsEmpty(commands)) { + return null; + } + + const commandsData = this._getEditOperations(ctx, commands); + if (commandsData.operations.length === 0) { + return null; + } + + const rawOperations = commandsData.operations; + + const editableRange = ctx.model.getEditableRange(); + const editableRangeStart = editableRange.getStartPosition(); + const editableRangeEnd = editableRange.getEndPosition(); + for (let i = 0, len = rawOperations.length; i < len; i++) { + const operationRange = rawOperations[i].range; + if (!editableRangeStart.isBeforeOrEqual(operationRange.getStartPosition()) || !operationRange.getEndPosition().isBeforeOrEqual(editableRangeEnd)) { + // These commands are outside of the editable range + return null; + } + } + + 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 + let filteredOperations: editorCommon.IIdentifiedSingleEditOperation[] = []; + 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; + } + const selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[]): Selection[] => { + let groupedInverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[][] = []; + for (let i = 0; i < ctx.selectionsBefore.length; i++) { + groupedInverseEditOperations[i] = []; + } + for (let i = 0; i < inverseEditOperations.length; i++) { + const op = inverseEditOperations[i]; + if (!op.identifier) { + // perhaps auto whitespace trim edits + continue; + } + groupedInverseEditOperations[op.identifier.major].push(op); + } + const minorBasedSorter = (a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation) => { + return a.identifier.minor - b.identifier.minor; + }; + let cursorSelections: Selection[] = []; + 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: string) => { + const idx = parseInt(id, 10); + const selectionStartMarker = ctx.model._getMarker(ctx.selectionStartMarkers[idx]); + const positionMarker = ctx.model._getMarker(ctx.positionMarkers[idx]); + return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column); + } + }); + } else { + cursorSelections[i] = ctx.selectionsBefore[i]; + } + } + return cursorSelections; + }); + + // Extract losing cursors + let losingCursors: number[] = []; + for (let losingCursorIndex in loserCursorsMap) { + if (loserCursorsMap.hasOwnProperty(losingCursorIndex)) { + losingCursors.push(parseInt(losingCursorIndex, 10)); + } + } + + // Sort losing cursors descending + losingCursors.sort((a: number, b: number): number => { + return b - a; + }); + + // Remove losing cursors + for (let i = 0; i < losingCursors.length; i++) { + selectionsAfter.splice(losingCursors[i], 1); + } + + return selectionsAfter; + } + + private static _arrayIsEmpty(commands: editorCommon.ICommand[]): boolean { + for (let i = 0, len = commands.length; i < len; i++) { + if (commands[i]) { + return false; + } + } + return true; + } + + private static _getEditOperations(ctx: IExecContext, commands: editorCommon.ICommand[]): ICommandsData { + let operations: editorCommon.IIdentifiedSingleEditOperation[] = []; + let hadTrackedEditOperation: boolean = false; + + for (let i = 0, len = commands.length; i < len; i++) { + if (commands[i]) { + const r = this._getEditOperationsFromCommand(ctx, i, commands[i]); + operations = operations.concat(r.operations); + hadTrackedEditOperation = hadTrackedEditOperation || r.hadTrackedEditOperation; + } + } + return { + operations: operations, + hadTrackedEditOperation: hadTrackedEditOperation + }; + } + + private static _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand): ICommandData { + // This method acts as a transaction, if the command fails + // everything it has done is ignored + let operations: editorCommon.IIdentifiedSingleEditOperation[] = []; + let operationMinor = 0; + + const addEditOperation = (selection: Range, text: string) => { + if (selection.isEmpty() && text === '') { + // This command wants to add a no-op => no thank you + return; + } + operations.push({ + identifier: { + major: majorIdentifier, + minor: operationMinor++ + }, + range: selection, + text: text, + forceMoveMarkers: false, + isAutoWhitespaceEdit: command.insertsAutoWhitespace + }); + }; + + let hadTrackedEditOperation = false; + const addTrackedEditOperation = (selection: Range, text: string) => { + hadTrackedEditOperation = true; + addEditOperation(selection, text); + }; + + const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { + let selectionMarkerStickToPreviousCharacter: boolean; + let positionMarkerStickToPreviousCharacter: boolean; + + if (selection.isEmpty()) { + // Try to lock it with surrounding text + if (typeof trackPreviousOnEmpty === 'boolean') { + selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; + positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; + } else { + const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn === maxLineColumn) { + selectionMarkerStickToPreviousCharacter = true; + positionMarkerStickToPreviousCharacter = true; + } else { + selectionMarkerStickToPreviousCharacter = false; + positionMarkerStickToPreviousCharacter = false; + } + } + } else { + if (selection.getDirection() === SelectionDirection.LTR) { + selectionMarkerStickToPreviousCharacter = false; + positionMarkerStickToPreviousCharacter = true; + } else { + selectionMarkerStickToPreviousCharacter = true; + positionMarkerStickToPreviousCharacter = false; + } + } + + const l = ctx.selectionStartMarkers.length; + ctx.selectionStartMarkers[l] = ctx.model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter); + ctx.positionMarkers[l] = ctx.model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter); + return l.toString(); + }; + + const editOperationBuilder: editorCommon.IEditOperationBuilder = { + addEditOperation: addEditOperation, + addTrackedEditOperation: addTrackedEditOperation, + trackSelection: trackSelection + }; + + try { + command.getEditOperations(ctx.model, editOperationBuilder); + } catch (e) { + e.friendlyMessage = nls.localize('corrupt.commands', "Unexpected exception while executing command."); + onUnexpectedError(e); + return { + operations: [], + hadTrackedEditOperation: false + }; + } + + return { + operations: operations, + hadTrackedEditOperation: hadTrackedEditOperation + }; + } + + private static _getLoserCursorMap(operations: editorCommon.IIdentifiedSingleEditOperation[]): { [index: string]: boolean; } { + // This is destructive on the array + operations = operations.slice(0); + + // Sort operations with last one first + operations.sort((a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation): number => { + // Note the minus! + return -(Range.compareRangesUsingEnds(a.range, b.range)); + }); + + // Operations can not overlap! + let loserCursorsMap: { [index: string]: boolean; } = {}; + + for (let i = 1; i < operations.length; i++) { + const previousOp = operations[i - 1]; + const currentOp = operations[i]; + + if (previousOp.range.getStartPosition().isBefore(currentOp.range.getEndPosition())) { + + let loserMajor: number; + + 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; + } +}