/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CharCode } from '../../../../vs/base/common/charCode'; import { onUnexpectedError } from '../../../../vs/base/common/errors'; import * as strings from '../../../../vs/base/common/strings'; import { EditorAutoClosingStrategy, EditorAutoSurroundStrategy, ConfigurationChangedEvent, EditorAutoClosingEditStrategy, EditorOption, EditorAutoIndentStrategy, } from '../../../../vs/editor/common/config/editorOptions'; import { Position } from '../../../../vs/editor/common/core/position'; import { Range } from '../../../../vs/editor/common/core/range'; import { ISelection, Selection, } from '../../../../vs/editor/common/core/selection'; import { ICommand, IConfiguration, } from '../../../../vs/editor/common/editorCommon'; import { ITextModel, PositionNormalizationAffinity, TextModelResolvedOptions, } from '../../../../vs/editor/common/model'; import { TextModel } from '../../../../vs/editor/common/model/textModel'; import { LanguageIdentifier } from '../../../../vs/editor/common/modes'; import { AutoClosingPairs, IAutoClosingPair, } from '../../../../vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from '../../../../vs/editor/common/modes/languageConfigurationRegistry'; import { ICoordinatesConverter } from '../../../../vs/editor/common/viewModel/viewModel'; import { Constants } from '../../../../vs/base/common/uint'; export interface IColumnSelectData { isReal: boolean; fromViewLineNumber: number; fromViewVisualColumn: number; toViewLineNumber: number; toViewVisualColumn: number; } /** * 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, DeletingLeft = 2, DeletingRight = 3, TypingOther = 4, TypingFirstSpace = 5, TypingConsecutiveSpace = 6, } export interface CharacterMap { [char: string]: string; } const autoCloseAlways = () => true; const autoCloseNever = () => false; const autoCloseBeforeWhitespace = (chr: string) => chr === ' ' || chr === '\t'; export class CursorConfiguration { _cursorMoveConfigurationBrand: void; public readonly readOnly: boolean; public readonly tabSize: number; public readonly indentSize: number; public readonly insertSpaces: boolean; public readonly stickyTabStops: boolean; public readonly pageSize: number; public readonly lineHeight: number; public readonly useTabStops: boolean; public readonly wordSeparators: string; public readonly emptySelectionClipboard: boolean; public readonly copyWithSyntaxHighlighting: boolean; public readonly multiCursorMergeOverlapping: boolean; public readonly multiCursorPaste: 'spread' | 'full'; public readonly autoClosingBrackets: EditorAutoClosingStrategy; public readonly autoClosingQuotes: EditorAutoClosingStrategy; public readonly autoClosingDelete: EditorAutoClosingEditStrategy; public readonly autoClosingOvertype: EditorAutoClosingEditStrategy; public readonly autoSurround: EditorAutoSurroundStrategy; public readonly autoIndent: EditorAutoIndentStrategy; public readonly autoClosingPairs: AutoClosingPairs; public readonly surroundingPairs: CharacterMap; public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean; bracket: (ch: string) => boolean; }; private readonly _languageIdentifier: LanguageIdentifier; private _electricChars: { [key: string]: boolean } | null; public static shouldRecreate(e: ConfigurationChangedEvent): boolean { return ( e.hasChanged(EditorOption.layoutInfo) || e.hasChanged(EditorOption.wordSeparators) || e.hasChanged(EditorOption.emptySelectionClipboard) || e.hasChanged(EditorOption.multiCursorMergeOverlapping) || e.hasChanged(EditorOption.multiCursorPaste) || e.hasChanged(EditorOption.autoClosingBrackets) || e.hasChanged(EditorOption.autoClosingQuotes) || e.hasChanged(EditorOption.autoClosingDelete) || e.hasChanged(EditorOption.autoClosingOvertype) || e.hasChanged(EditorOption.autoSurround) || e.hasChanged(EditorOption.useTabStops) || e.hasChanged(EditorOption.lineHeight) || e.hasChanged(EditorOption.readOnly) ); } constructor( languageIdentifier: LanguageIdentifier, modelOptions: TextModelResolvedOptions, configuration: IConfiguration ) { this._languageIdentifier = languageIdentifier; const options = configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); this.readOnly = options.get(EditorOption.readOnly); this.tabSize = modelOptions.tabSize; this.indentSize = modelOptions.indentSize; this.insertSpaces = modelOptions.insertSpaces; this.stickyTabStops = options.get(EditorOption.stickyTabStops); this.lineHeight = options.get(EditorOption.lineHeight); this.pageSize = Math.max( 1, Math.floor(layoutInfo.height / this.lineHeight) - 2 ); this.useTabStops = options.get(EditorOption.useTabStops); this.wordSeparators = options.get(EditorOption.wordSeparators); this.emptySelectionClipboard = options.get( EditorOption.emptySelectionClipboard ); this.copyWithSyntaxHighlighting = options.get( EditorOption.copyWithSyntaxHighlighting ); this.multiCursorMergeOverlapping = options.get( EditorOption.multiCursorMergeOverlapping ); this.multiCursorPaste = options.get(EditorOption.multiCursorPaste); this.autoClosingBrackets = options.get(EditorOption.autoClosingBrackets); this.autoClosingQuotes = options.get(EditorOption.autoClosingQuotes); this.autoClosingDelete = options.get(EditorOption.autoClosingDelete); this.autoClosingOvertype = options.get(EditorOption.autoClosingOvertype); this.autoSurround = options.get(EditorOption.autoSurround); this.autoIndent = options.get(EditorOption.autoIndent); this.surroundingPairs = {}; this._electricChars = null; this.shouldAutoCloseBefore = { quote: CursorConfiguration._getShouldAutoClose( languageIdentifier, this.autoClosingQuotes ), bracket: CursorConfiguration._getShouldAutoClose( languageIdentifier, this.autoClosingBrackets ), }; this.autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs( languageIdentifier.id ); let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageIdentifier); if (surroundingPairs) { for (const pair of surroundingPairs) { this.surroundingPairs[pair.open] = pair.close; } } } public get electricChars() { if (!this._electricChars) { this._electricChars = {}; let electricChars = CursorConfiguration._getElectricCharacters( this._languageIdentifier ); if (electricChars) { for (const char of electricChars) { this._electricChars[char] = true; } } } return this._electricChars; } public normalizeIndentation(str: string): string { return TextModel.normalizeIndentation( str, this.indentSize, this.insertSpaces ); } private static _getElectricCharacters( languageIdentifier: LanguageIdentifier ): string[] | null { try { return LanguageConfigurationRegistry.getElectricCharacters( languageIdentifier.id ); } catch (e) { onUnexpectedError(e); return null; } } private static _getShouldAutoClose( languageIdentifier: LanguageIdentifier, autoCloseConfig: EditorAutoClosingStrategy ): (ch: string) => boolean { switch (autoCloseConfig) { case 'beforeWhitespace': return autoCloseBeforeWhitespace; case 'languageDefined': return CursorConfiguration._getLanguageDefinedShouldAutoClose( languageIdentifier ); case 'always': return autoCloseAlways; case 'never': return autoCloseNever; } } private static _getLanguageDefinedShouldAutoClose( languageIdentifier: LanguageIdentifier ): (ch: string) => boolean { try { const autoCloseBeforeSet = LanguageConfigurationRegistry.getAutoCloseBeforeSet( languageIdentifier.id ); return (c) => autoCloseBeforeSet.indexOf(c) !== -1; } catch (e) { onUnexpectedError(e); return autoCloseNever; } } private static _getSurroundingPairs( languageIdentifier: LanguageIdentifier ): IAutoClosingPair[] | null { 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; normalizePosition( position: Position, affinity: PositionNormalizationAffinity ): Position; /** * Gets the column at which indentation stops at a given line. * @internal */ getLineIndentColumn(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: ITextModel; public readonly coordinatesConverter: ICoordinatesConverter; public readonly cursorConfig: CursorConfiguration; constructor( model: ITextModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration ) { this.model = model; this.coordinatesConverter = coordinatesConverter; this.cursorConfig = cursorConfig; } } export class PartialModelCursorState { readonly modelState: SingleCursorState; readonly viewState: null; constructor(modelState: SingleCursorState) { this.modelState = modelState; this.viewState = null; } } export class PartialViewCursorState { readonly modelState: null; readonly viewState: SingleCursorState; constructor(viewState: SingleCursorState) { this.modelState = null; this.viewState = viewState; } } export type PartialCursorState = | CursorState | PartialModelCursorState | PartialViewCursorState; export class CursorState { _cursorStateBrand: void; public static fromModelState( modelState: SingleCursorState ): PartialModelCursorState { return new PartialModelCursorState(modelState); } public static fromViewState( viewState: SingleCursorState ): PartialViewCursorState { return new PartialViewCursorState(viewState); } public static fromModelSelection( modelSelection: ISelection ): PartialModelCursorState { 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: readonly ISelection[] ): PartialModelCursorState[] { let states: PartialModelCursorState[] = []; for (let i = 0, len = modelSelections.length; i < len; i++) { states[i] = this.fromModelSelection(modelSelections[i]); } return states; } 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: Array; readonly shouldPushStackElementBefore: boolean; readonly shouldPushStackElementAfter: boolean; constructor( type: EditOperationType, commands: Array, 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 visibleColumnFromColumn( lineContent: string, column: number, tabSize: number ): number { const lineContentLength = lineContent.length; const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength; let result = 0; let i = 0; while (i < endOffset) { const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); i += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; if (codePoint === CharCode.Tab) { result = CursorColumns.nextRenderTabStop(result, tabSize); } else { let graphemeBreakType = strings.getGraphemeBreakType(codePoint); while (i < endOffset) { const nextCodePoint = strings.getNextCodePoint( lineContent, endOffset, i ); const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); if ( strings.breakBetweenGraphemeBreakType( graphemeBreakType, nextGraphemeBreakType ) ) { break; } i += nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; graphemeBreakType = nextGraphemeBreakType; } if ( strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint) ) { result = result + 2; } else { result = result + 1; } } } return result; } /** * Returns an array that maps one based columns to one based visible columns. The entry at position 0 is -1. */ public static visibleColumnsByColumns( lineContent: string, tabSize: number ): number[] { const endOffset = lineContent.length; let result = new Array(); result.push(-1); let pos = 0; let i = 0; while (i < endOffset) { const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); i += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; result.push(pos); if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { result.push(pos); } if (codePoint === CharCode.Tab) { pos = CursorColumns.nextRenderTabStop(pos, tabSize); } else { let graphemeBreakType = strings.getGraphemeBreakType(codePoint); while (i < endOffset) { const nextCodePoint = strings.getNextCodePoint( lineContent, endOffset, i ); const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); if ( strings.breakBetweenGraphemeBreakType( graphemeBreakType, nextGraphemeBreakType ) ) { break; } i += nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; result.push(pos); if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { result.push(pos); } graphemeBreakType = nextGraphemeBreakType; } if ( strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint) ) { pos = pos + 2; } else { pos = pos + 1; } } } result.push(pos); 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; let beforeColumn = 1; let i = 0; while (i < lineLength) { const codePoint = strings.getNextCodePoint(lineContent, lineLength, i); i += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; let afterVisibleColumn: number; if (codePoint === CharCode.Tab) { afterVisibleColumn = CursorColumns.nextRenderTabStop( beforeVisibleColumn, tabSize ); } else { let graphemeBreakType = strings.getGraphemeBreakType(codePoint); while (i < lineLength) { const nextCodePoint = strings.getNextCodePoint( lineContent, lineLength, i ); const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); if ( strings.breakBetweenGraphemeBreakType( graphemeBreakType, nextGraphemeBreakType ) ) { break; } i += nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; graphemeBreakType = nextGraphemeBreakType; } if ( strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint) ) { afterVisibleColumn = beforeVisibleColumn + 2; } else { afterVisibleColumn = beforeVisibleColumn + 1; } } const afterColumn = i + 1; if (afterVisibleColumn >= visibleColumn) { const beforeDelta = visibleColumn - beforeVisibleColumn; const afterDelta = afterVisibleColumn - visibleColumn; if (afterDelta < beforeDelta) { return afterColumn; } else { return beforeColumn; } } beforeVisibleColumn = afterVisibleColumn; beforeColumn = afterColumn; } // 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 nextRenderTabStop( 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 nextIndentTabStop( visibleColumn: number, indentSize: number ): number { return visibleColumn + indentSize - (visibleColumn % indentSize); } /** * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) */ public static prevRenderTabStop(column: number, tabSize: number): number { return Math.max(0, column - 1 - ((column - 1) % tabSize)); } /** * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) */ public static prevIndentTabStop(column: number, indentSize: number): number { return Math.max(0, column - 1 - ((column - 1) % indentSize)); } } export function isQuote(ch: string): boolean { return ch === "'" || ch === '"' || ch === '`'; }