/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { Position } from 'vs/editor/common/core/position'; import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { ICommand, TextModelResolvedOptions, IConfiguration, IModel, ScrollType } from 'vs/editor/common/editorCommon'; import { TextModel } from 'vs/editor/common/model/textModel'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { Range } from 'vs/editor/common/core/range'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; export interface IColumnSelectData { toViewLineNumber: number; toViewVisualColumn: number; } export const enum RevealTarget { Primary = 0, TopMost = 1, BottomMost = 2 } /** * This is an operation type that will be recorded for undo/redo purposes. * The goal is to introduce an undo stop when the controller switches between different operation types. */ export const enum EditOperationType { Other = 0, Typing = 1, DeletingLeft = 2, DeletingRight = 3 } export interface ICursors { readonly context: CursorContext; getPrimaryCursor(): CursorState; getLastAddedCursorIndex(): number; getAll(): CursorState[]; getColumnSelectData(): IColumnSelectData; setColumnSelectData(columnSelectData: IColumnSelectData): void; setStates(source: string, reason: CursorChangeReason, states: CursorState[]): void; reveal(horizontal: boolean, target: RevealTarget, scrollType: ScrollType): void; revealRange(revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void; scrollTo(desiredScrollTop: number): void; getPrevEditOperationType(): EditOperationType; setPrevEditOperationType(type: EditOperationType): void; } export interface CharacterMap { [char: string]: string; } export class CursorConfiguration { _cursorMoveConfigurationBrand: void; public readonly readOnly: boolean; public readonly tabSize: number; public readonly insertSpaces: boolean; public readonly oneIndent: string; public readonly pageSize: number; public readonly lineHeight: number; public readonly useTabStops: boolean; public readonly wordSeparators: string; public readonly emptySelectionClipboard: boolean; public readonly autoClosingBrackets: boolean; public readonly autoIndent: boolean; public readonly autoClosingPairsOpen: CharacterMap; public readonly autoClosingPairsClose: CharacterMap; public readonly surroundingPairs: CharacterMap; private readonly _languageIdentifier: LanguageIdentifier; private _electricChars: { [key: string]: boolean; }; public static shouldRecreate(e: IConfigurationChangedEvent): boolean { return ( e.layoutInfo || e.wordSeparators || e.emptySelectionClipboard || e.autoClosingBrackets || e.useTabStops || e.lineHeight || e.readOnly ); } constructor( languageIdentifier: LanguageIdentifier, oneIndent: string, modelOptions: TextModelResolvedOptions, configuration: IConfiguration ) { this._languageIdentifier = languageIdentifier; let c = configuration.editor; this.readOnly = c.readOnly; this.tabSize = modelOptions.tabSize; this.insertSpaces = modelOptions.insertSpaces; this.oneIndent = oneIndent; this.pageSize = Math.floor(c.layoutInfo.height / c.fontInfo.lineHeight) - 2; this.lineHeight = c.lineHeight; this.useTabStops = c.useTabStops; this.wordSeparators = c.wordSeparators; this.emptySelectionClipboard = c.emptySelectionClipboard; this.autoClosingBrackets = c.autoClosingBrackets; this.autoIndent = c.autoIndent; this.autoClosingPairsOpen = {}; this.autoClosingPairsClose = {}; this.surroundingPairs = {}; this._electricChars = null; let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier); if (autoClosingPairs) { for (let i = 0; i < autoClosingPairs.length; i++) { this.autoClosingPairsOpen[autoClosingPairs[i].open] = autoClosingPairs[i].close; this.autoClosingPairsClose[autoClosingPairs[i].close] = autoClosingPairs[i].open; } } let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageIdentifier); if (surroundingPairs) { for (let i = 0; i < surroundingPairs.length; i++) { this.surroundingPairs[surroundingPairs[i].open] = surroundingPairs[i].close; } } } public get electricChars() { if (!this._electricChars) { this._electricChars = {}; let electricChars = CursorConfiguration._getElectricCharacters(this._languageIdentifier); if (electricChars) { for (let i = 0; i < electricChars.length; i++) { this._electricChars[electricChars[i]] = true; } } } return this._electricChars; } public normalizeIndentation(str: string): string { return TextModel.normalizeIndentation(str, this.tabSize, this.insertSpaces); } private static _getElectricCharacters(languageIdentifier: LanguageIdentifier): string[] { try { return LanguageConfigurationRegistry.getElectricCharacters(languageIdentifier.id); } catch (e) { onUnexpectedError(e); return null; } } private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] { try { return LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id); } catch (e) { onUnexpectedError(e); return null; } } private static _getSurroundingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] { try { return LanguageConfigurationRegistry.getSurroundingPairs(languageIdentifier.id); } catch (e) { onUnexpectedError(e); return null; } } } /** * Represents a simple model (either the model or the view model). */ export interface ICursorSimpleModel { getLineCount(): number; getLineContent(lineNumber: number): string; getLineMinColumn(lineNumber: number): number; getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; } /** * Represents the cursor state on either the model or on the view model. */ export class SingleCursorState { _singleCursorStateBrand: void; // --- selection can start as a range (think double click and drag) public readonly selectionStart: Range; public readonly selectionStartLeftoverVisibleColumns: number; public readonly position: Position; public readonly leftoverVisibleColumns: number; public readonly selection: Selection; constructor( selectionStart: Range, selectionStartLeftoverVisibleColumns: number, position: Position, leftoverVisibleColumns: number, ) { this.selectionStart = selectionStart; this.selectionStartLeftoverVisibleColumns = selectionStartLeftoverVisibleColumns; this.position = position; this.leftoverVisibleColumns = leftoverVisibleColumns; this.selection = SingleCursorState._computeSelection(this.selectionStart, this.position); } public equals(other: SingleCursorState) { return ( this.selectionStartLeftoverVisibleColumns === other.selectionStartLeftoverVisibleColumns && this.leftoverVisibleColumns === other.leftoverVisibleColumns && this.position.equals(other.position) && this.selectionStart.equalsRange(other.selectionStart) ); } public hasSelection(): boolean { return (!this.selection.isEmpty() || !this.selectionStart.isEmpty()); } public move(inSelectionMode: boolean, lineNumber: number, column: number, leftoverVisibleColumns: number): SingleCursorState { if (inSelectionMode) { // move just position return new SingleCursorState( this.selectionStart, this.selectionStartLeftoverVisibleColumns, new Position(lineNumber, column), leftoverVisibleColumns ); } else { // move everything return new SingleCursorState( new Range(lineNumber, column, lineNumber, column), leftoverVisibleColumns, new Position(lineNumber, column), leftoverVisibleColumns ); } } private static _computeSelection(selectionStart: Range, position: Position): Selection { let startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; if (selectionStart.isEmpty()) { startLineNumber = selectionStart.startLineNumber; startColumn = selectionStart.startColumn; endLineNumber = position.lineNumber; endColumn = position.column; } else { if (position.isBeforeOrEqual(selectionStart.getStartPosition())) { startLineNumber = selectionStart.endLineNumber; startColumn = selectionStart.endColumn; endLineNumber = position.lineNumber; endColumn = position.column; } else { startLineNumber = selectionStart.startLineNumber; startColumn = selectionStart.startColumn; endLineNumber = position.lineNumber; endColumn = position.column; } } return new Selection( startLineNumber, startColumn, endLineNumber, endColumn ); } } export class CursorContext { _cursorContextBrand: void; public readonly model: IModel; public readonly viewModel: IViewModel; public readonly config: CursorConfiguration; constructor(configuration: IConfiguration, model: IModel, viewModel: IViewModel) { this.model = model; this.viewModel = viewModel; this.config = new CursorConfiguration( this.model.getLanguageIdentifier(), this.model.getOneIndent(), this.model.getOptions(), configuration ); } public validateViewPosition(viewPosition: Position, modelPosition: Position): Position { return this.viewModel.coordinatesConverter.validateViewPosition(viewPosition, modelPosition); } public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { return this.viewModel.coordinatesConverter.validateViewRange(viewRange, expectedModelRange); } public convertViewRangeToModelRange(viewRange: Range): Range { return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange); } public convertViewPositionToModelPosition(lineNumber: number, column: number): Position { return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, column)); } public convertModelPositionToViewPosition(modelPosition: Position): Position { return this.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); } public convertModelRangeToViewRange(modelRange: Range): Range { return this.viewModel.coordinatesConverter.convertModelRangeToViewRange(modelRange); } public getCurrentScrollTop(): number { return this.viewModel.viewLayout.getCurrentScrollTop(); } public getCompletelyVisibleViewRange(): Range { return this.viewModel.getCompletelyVisibleViewRange(); } public getCompletelyVisibleModelRange(): Range { const viewRange = this.viewModel.getCompletelyVisibleViewRange(); return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange); } public getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range { return this.viewModel.getCompletelyVisibleViewRangeAtScrollTop(scrollTop); } public getVerticalOffsetForViewLine(viewLineNumber: number): number { return this.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewLineNumber); } } export class CursorState { _cursorStateBrand: void; public static fromModelState(modelState: SingleCursorState): CursorState { return new CursorState(modelState, null); } public static fromViewState(viewState: SingleCursorState): CursorState { return new CursorState(null, viewState); } public static fromModelSelection(modelSelection: ISelection): CursorState { const selectionStartLineNumber = modelSelection.selectionStartLineNumber; const selectionStartColumn = modelSelection.selectionStartColumn; const positionLineNumber = modelSelection.positionLineNumber; const positionColumn = modelSelection.positionColumn; const modelState = new SingleCursorState( new Range(selectionStartLineNumber, selectionStartColumn, selectionStartLineNumber, selectionStartColumn), 0, new Position(positionLineNumber, positionColumn), 0 ); return CursorState.fromModelState(modelState); } public static fromModelSelections(modelSelections: ISelection[]): CursorState[] { let states: CursorState[] = []; for (let i = 0, len = modelSelections.length; i < len; i++) { states[i] = this.fromModelSelection(modelSelections[i]); } return states; } public static ensureInEditableRange(context: CursorContext, states: CursorState[]): CursorState[] { const model = context.model; if (!model.hasEditableRange()) { return states; } const modelEditableRange = model.getEditableRange(); const viewEditableRange = context.convertModelRangeToViewRange(modelEditableRange); let result: CursorState[] = []; for (let i = 0, len = states.length; i < len; i++) { const state = states[i]; if (state.modelState) { const newModelState = CursorState._ensureInEditableRange(state.modelState, modelEditableRange); result[i] = newModelState ? CursorState.fromModelState(newModelState) : state; } else { const newViewState = CursorState._ensureInEditableRange(state.viewState, viewEditableRange); result[i] = newViewState ? CursorState.fromViewState(newViewState) : state; } } return result; } private static _ensureInEditableRange(state: SingleCursorState, editableRange: Range): SingleCursorState { const position = state.position; if (position.lineNumber < editableRange.startLineNumber || (position.lineNumber === editableRange.startLineNumber && position.column < editableRange.startColumn)) { return new SingleCursorState( state.selectionStart, state.selectionStartLeftoverVisibleColumns, new Position(editableRange.startLineNumber, editableRange.startColumn), 0 ); } if (position.lineNumber > editableRange.endLineNumber || (position.lineNumber === editableRange.endLineNumber && position.column > editableRange.endColumn)) { return new SingleCursorState( state.selectionStart, state.selectionStartLeftoverVisibleColumns, new Position(editableRange.endLineNumber, editableRange.endColumn), 0 ); } return null; } readonly modelState: SingleCursorState; readonly viewState: SingleCursorState; constructor(modelState: SingleCursorState, viewState: SingleCursorState) { this.modelState = modelState; this.viewState = viewState; } public equals(other: CursorState): boolean { return (this.viewState.equals(other.viewState) && this.modelState.equals(other.modelState)); } } export class EditOperationResult { _editOperationResultBrand: void; readonly type: EditOperationType; readonly commands: ICommand[]; readonly shouldPushStackElementBefore: boolean; readonly shouldPushStackElementAfter: boolean; constructor( type: EditOperationType, commands: ICommand[], opts: { shouldPushStackElementBefore: boolean; shouldPushStackElementAfter: boolean; } ) { this.type = type; this.commands = commands; this.shouldPushStackElementBefore = opts.shouldPushStackElementBefore; this.shouldPushStackElementAfter = opts.shouldPushStackElementAfter; } } /** * Common operations that work and make sense both on the model and on the view model. */ export class CursorColumns { public static isLowSurrogate(model: ICursorSimpleModel, lineNumber: number, charOffset: number): boolean { let lineContent = model.getLineContent(lineNumber); if (charOffset < 0 || charOffset >= lineContent.length) { return false; } return strings.isLowSurrogate(lineContent.charCodeAt(charOffset)); } public static isHighSurrogate(model: ICursorSimpleModel, lineNumber: number, charOffset: number): boolean { let lineContent = model.getLineContent(lineNumber); if (charOffset < 0 || charOffset >= lineContent.length) { return false; } return strings.isHighSurrogate(lineContent.charCodeAt(charOffset)); } public static isInsideSurrogatePair(model: ICursorSimpleModel, lineNumber: number, column: number): boolean { return this.isHighSurrogate(model, lineNumber, column - 2); } public static visibleColumnFromColumn(lineContent: string, column: number, tabSize: number): number { let endOffset = lineContent.length; if (endOffset > column - 1) { endOffset = column - 1; } let result = 0; for (let i = 0; i < endOffset; i++) { let charCode = lineContent.charCodeAt(i); if (charCode === CharCode.Tab) { result = this.nextTabStop(result, tabSize); } else { result = result + 1; } } return result; } public static visibleColumnFromColumn2(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): number { return this.visibleColumnFromColumn(model.getLineContent(position.lineNumber), position.column, config.tabSize); } public static columnFromVisibleColumn(lineContent: string, visibleColumn: number, tabSize: number): number { if (visibleColumn <= 0) { return 1; } const lineLength = lineContent.length; let beforeVisibleColumn = 0; for (let i = 0; i < lineLength; i++) { let charCode = lineContent.charCodeAt(i); let afterVisibleColumn: number; if (charCode === CharCode.Tab) { afterVisibleColumn = this.nextTabStop(beforeVisibleColumn, tabSize); } else { afterVisibleColumn = beforeVisibleColumn + 1; } if (afterVisibleColumn >= visibleColumn) { let prevDelta = visibleColumn - beforeVisibleColumn; let afterDelta = afterVisibleColumn - visibleColumn; if (afterDelta < prevDelta) { return i + 2; } else { return i + 1; } } beforeVisibleColumn = afterVisibleColumn; } // walked the entire string return lineLength + 1; } public static columnFromVisibleColumn2(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, visibleColumn: number): number { let result = this.columnFromVisibleColumn(model.getLineContent(lineNumber), visibleColumn, config.tabSize); let minColumn = model.getLineMinColumn(lineNumber); if (result < minColumn) { return minColumn; } let maxColumn = model.getLineMaxColumn(lineNumber); if (result > maxColumn) { return maxColumn; } return result; } /** * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) */ public static nextTabStop(visibleColumn: number, tabSize: number): number { return visibleColumn + tabSize - visibleColumn % tabSize; } /** * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) */ public static prevTabStop(column: number, tabSize: number): number { return column - 1 - (column - 1) % tabSize; } }