Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { commonPrefixLength, commonSuffixLength } from '../../../../../base/common/strings.js';6import { Position } from '../../../../common/core/position.js';7import { Range } from '../../../../common/core/range.js';8import { SelectionDirection } from '../../../../common/core/selection.js';9import { ISimpleScreenReaderContentState } from '../screenReaderUtils.js';1011export const _debugComposition = false;1213export interface ITextAreaWrapper {14getValue(): string;15setValue(reason: string, value: string): void;1617getSelectionStart(): number;18getSelectionEnd(): number;19setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void;20}2122export interface ITypeData {23text: string;24replacePrevCharCnt: number;25replaceNextCharCnt: number;26positionDelta: number;27}2829export class TextAreaState {3031public static readonly EMPTY = new TextAreaState('', 0, 0, null, undefined);3233constructor(34public readonly value: string,35/** the offset where selection starts inside `value` */36public readonly selectionStart: number,37/** the offset where selection ends inside `value` */38public readonly selectionEnd: number,39/** the editor range in the view coordinate system that matches the selection inside `value` */40public readonly selection: Range | null,41/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */42public readonly newlineCountBeforeSelection: number | undefined,43) { }4445public toString(): string {46return `[ <${this.value}>, selectionStart: ${this.selectionStart}, selectionEnd: ${this.selectionEnd}]`;47}4849public static readFromTextArea(textArea: ITextAreaWrapper, previousState: TextAreaState | null): TextAreaState {50const value = textArea.getValue();51const selectionStart = textArea.getSelectionStart();52const selectionEnd = textArea.getSelectionEnd();53let newlineCountBeforeSelection: number | undefined = undefined;54if (previousState) {55const valueBeforeSelectionStart = value.substring(0, selectionStart);56const previousValueBeforeSelectionStart = previousState.value.substring(0, previousState.selectionStart);57if (valueBeforeSelectionStart === previousValueBeforeSelectionStart) {58newlineCountBeforeSelection = previousState.newlineCountBeforeSelection;59}60}61return new TextAreaState(value, selectionStart, selectionEnd, null, newlineCountBeforeSelection);62}6364public collapseSelection(): TextAreaState {65if (this.selectionStart === this.value.length) {66return this;67}68return new TextAreaState(this.value, this.value.length, this.value.length, null, undefined);69}7071public isWrittenToTextArea(textArea: ITextAreaWrapper, select: boolean): boolean {72const valuesEqual = this.value === textArea.getValue();73if (!select) {74return valuesEqual;75}76const selectionsEqual = this.selectionStart === textArea.getSelectionStart() && this.selectionEnd === textArea.getSelectionEnd();77return selectionsEqual && valuesEqual;78}7980public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void {81if (_debugComposition) {82console.log(`writeToTextArea ${reason}: ${this.toString()}`);83}84textArea.setValue(reason, this.value);85if (select) {86textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);87}88}8990public deduceEditorPosition(offset: number): [Position | null, number, number] {91if (offset <= this.selectionStart) {92const str = this.value.substring(offset, this.selectionStart);93return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str, -1);94}95if (offset >= this.selectionEnd) {96const str = this.value.substring(this.selectionEnd, offset);97return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str, 1);98}99const str1 = this.value.substring(this.selectionStart, offset);100if (str1.indexOf(String.fromCharCode(8230)) === -1) {101return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str1, 1);102}103const str2 = this.value.substring(offset, this.selectionEnd);104return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str2, -1);105}106107private _finishDeduceEditorPosition(anchor: Position | null, deltaText: string, signum: number): [Position | null, number, number] {108let lineFeedCnt = 0;109let lastLineFeedIndex = -1;110while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {111lineFeedCnt++;112}113return [anchor, signum * deltaText.length, lineFeedCnt];114}115116public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {117if (!previousState) {118// This is the EMPTY state119return {120text: '',121replacePrevCharCnt: 0,122replaceNextCharCnt: 0,123positionDelta: 0124};125}126127if (_debugComposition) {128console.log('------------------------deduceInput');129console.log(`PREVIOUS STATE: ${previousState.toString()}`);130console.log(`CURRENT STATE: ${currentState.toString()}`);131}132133const prefixLength = Math.min(134commonPrefixLength(previousState.value, currentState.value),135previousState.selectionStart,136currentState.selectionStart137);138const suffixLength = Math.min(139commonSuffixLength(previousState.value, currentState.value),140previousState.value.length - previousState.selectionEnd,141currentState.value.length - currentState.selectionEnd142);143const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);144const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);145const previousSelectionStart = previousState.selectionStart - prefixLength;146const previousSelectionEnd = previousState.selectionEnd - prefixLength;147const currentSelectionStart = currentState.selectionStart - prefixLength;148const currentSelectionEnd = currentState.selectionEnd - prefixLength;149150if (_debugComposition) {151console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);152console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);153}154155if (currentSelectionStart === currentSelectionEnd) {156// no current selection157const replacePreviousCharacters = (previousState.selectionStart - prefixLength);158if (_debugComposition) {159console.log(`REMOVE PREVIOUS: ${replacePreviousCharacters} chars`);160}161162return {163text: currentValue,164replacePrevCharCnt: replacePreviousCharacters,165replaceNextCharCnt: 0,166positionDelta: 0167};168}169170// there is a current selection => composition case171const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;172return {173text: currentValue,174replacePrevCharCnt: replacePreviousCharacters,175replaceNextCharCnt: 0,176positionDelta: 0177};178}179180public static deduceAndroidCompositionInput(previousState: TextAreaState, currentState: TextAreaState): ITypeData {181if (!previousState) {182// This is the EMPTY state183return {184text: '',185replacePrevCharCnt: 0,186replaceNextCharCnt: 0,187positionDelta: 0188};189}190191if (_debugComposition) {192console.log('------------------------deduceAndroidCompositionInput');193console.log(`PREVIOUS STATE: ${previousState.toString()}`);194console.log(`CURRENT STATE: ${currentState.toString()}`);195}196197if (previousState.value === currentState.value) {198return {199text: '',200replacePrevCharCnt: 0,201replaceNextCharCnt: 0,202positionDelta: currentState.selectionEnd - previousState.selectionEnd203};204}205206const prefixLength = Math.min(commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);207const suffixLength = Math.min(commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);208const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);209const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);210const previousSelectionStart = previousState.selectionStart - prefixLength;211const previousSelectionEnd = previousState.selectionEnd - prefixLength;212const currentSelectionStart = currentState.selectionStart - prefixLength;213const currentSelectionEnd = currentState.selectionEnd - prefixLength;214215if (_debugComposition) {216console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);217console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);218}219220return {221text: currentValue,222replacePrevCharCnt: previousSelectionEnd,223replaceNextCharCnt: previousValue.length - previousSelectionEnd,224positionDelta: currentSelectionEnd - currentValue.length225};226}227228public static fromScreenReaderContentState(screenReaderContentState: ISimpleScreenReaderContentState) {229let selectionStart;230let selectionEnd;231const direction = screenReaderContentState.selection.getDirection();232switch (direction) {233case SelectionDirection.LTR:234selectionStart = screenReaderContentState.selectionStart;235selectionEnd = screenReaderContentState.selectionEnd;236break;237case SelectionDirection.RTL:238selectionStart = screenReaderContentState.selectionEnd;239selectionEnd = screenReaderContentState.selectionStart;240break;241}242return new TextAreaState(243screenReaderContentState.value,244selectionStart,245selectionEnd,246screenReaderContentState.selection,247screenReaderContentState.newlineCountBeforeSelection248);249}250}251252253