Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts
3296 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 { ISimpleScreenReaderContentState } from '../screenReaderUtils.js';910export const _debugComposition = false;1112export interface ITextAreaWrapper {13getValue(): string;14setValue(reason: string, value: string): void;1516getSelectionStart(): number;17getSelectionEnd(): number;18setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void;19}2021export interface ITypeData {22text: string;23replacePrevCharCnt: number;24replaceNextCharCnt: number;25positionDelta: number;26}2728export class TextAreaState {2930public static readonly EMPTY = new TextAreaState('', 0, 0, null, undefined);3132constructor(33public readonly value: string,34/** the offset where selection starts inside `value` */35public readonly selectionStart: number,36/** the offset where selection ends inside `value` */37public readonly selectionEnd: number,38/** the editor range in the view coordinate system that matches the selection inside `value` */39public readonly selection: Range | null,40/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */41public readonly newlineCountBeforeSelection: number | undefined,42) { }4344public toString(): string {45return `[ <${this.value}>, selectionStart: ${this.selectionStart}, selectionEnd: ${this.selectionEnd}]`;46}4748public static readFromTextArea(textArea: ITextAreaWrapper, previousState: TextAreaState | null): TextAreaState {49const value = textArea.getValue();50const selectionStart = textArea.getSelectionStart();51const selectionEnd = textArea.getSelectionEnd();52let newlineCountBeforeSelection: number | undefined = undefined;53if (previousState) {54const valueBeforeSelectionStart = value.substring(0, selectionStart);55const previousValueBeforeSelectionStart = previousState.value.substring(0, previousState.selectionStart);56if (valueBeforeSelectionStart === previousValueBeforeSelectionStart) {57newlineCountBeforeSelection = previousState.newlineCountBeforeSelection;58}59}60return new TextAreaState(value, selectionStart, selectionEnd, null, newlineCountBeforeSelection);61}6263public collapseSelection(): TextAreaState {64if (this.selectionStart === this.value.length) {65return this;66}67return new TextAreaState(this.value, this.value.length, this.value.length, null, undefined);68}6970public isWrittenToTextArea(textArea: ITextAreaWrapper, select: boolean): boolean {71const valuesEqual = this.value === textArea.getValue();72if (!select) {73return valuesEqual;74}75const selectionsEqual = this.selectionStart === textArea.getSelectionStart() && this.selectionEnd === textArea.getSelectionEnd();76return selectionsEqual && valuesEqual;77}7879public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void {80if (_debugComposition) {81console.log(`writeToTextArea ${reason}: ${this.toString()}`);82}83textArea.setValue(reason, this.value);84if (select) {85textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);86}87}8889public deduceEditorPosition(offset: number): [Position | null, number, number] {90if (offset <= this.selectionStart) {91const str = this.value.substring(offset, this.selectionStart);92return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str, -1);93}94if (offset >= this.selectionEnd) {95const str = this.value.substring(this.selectionEnd, offset);96return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str, 1);97}98const str1 = this.value.substring(this.selectionStart, offset);99if (str1.indexOf(String.fromCharCode(8230)) === -1) {100return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str1, 1);101}102const str2 = this.value.substring(offset, this.selectionEnd);103return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str2, -1);104}105106private _finishDeduceEditorPosition(anchor: Position | null, deltaText: string, signum: number): [Position | null, number, number] {107let lineFeedCnt = 0;108let lastLineFeedIndex = -1;109while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {110lineFeedCnt++;111}112return [anchor, signum * deltaText.length, lineFeedCnt];113}114115public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {116if (!previousState) {117// This is the EMPTY state118return {119text: '',120replacePrevCharCnt: 0,121replaceNextCharCnt: 0,122positionDelta: 0123};124}125126if (_debugComposition) {127console.log('------------------------deduceInput');128console.log(`PREVIOUS STATE: ${previousState.toString()}`);129console.log(`CURRENT STATE: ${currentState.toString()}`);130}131132const prefixLength = Math.min(133commonPrefixLength(previousState.value, currentState.value),134previousState.selectionStart,135currentState.selectionStart136);137const suffixLength = Math.min(138commonSuffixLength(previousState.value, currentState.value),139previousState.value.length - previousState.selectionEnd,140currentState.value.length - currentState.selectionEnd141);142const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);143const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);144const previousSelectionStart = previousState.selectionStart - prefixLength;145const previousSelectionEnd = previousState.selectionEnd - prefixLength;146const currentSelectionStart = currentState.selectionStart - prefixLength;147const currentSelectionEnd = currentState.selectionEnd - prefixLength;148149if (_debugComposition) {150console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);151console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);152}153154if (currentSelectionStart === currentSelectionEnd) {155// no current selection156const replacePreviousCharacters = (previousState.selectionStart - prefixLength);157if (_debugComposition) {158console.log(`REMOVE PREVIOUS: ${replacePreviousCharacters} chars`);159}160161return {162text: currentValue,163replacePrevCharCnt: replacePreviousCharacters,164replaceNextCharCnt: 0,165positionDelta: 0166};167}168169// there is a current selection => composition case170const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;171return {172text: currentValue,173replacePrevCharCnt: replacePreviousCharacters,174replaceNextCharCnt: 0,175positionDelta: 0176};177}178179public static deduceAndroidCompositionInput(previousState: TextAreaState, currentState: TextAreaState): ITypeData {180if (!previousState) {181// This is the EMPTY state182return {183text: '',184replacePrevCharCnt: 0,185replaceNextCharCnt: 0,186positionDelta: 0187};188}189190if (_debugComposition) {191console.log('------------------------deduceAndroidCompositionInput');192console.log(`PREVIOUS STATE: ${previousState.toString()}`);193console.log(`CURRENT STATE: ${currentState.toString()}`);194}195196if (previousState.value === currentState.value) {197return {198text: '',199replacePrevCharCnt: 0,200replaceNextCharCnt: 0,201positionDelta: currentState.selectionEnd - previousState.selectionEnd202};203}204205const prefixLength = Math.min(commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);206const suffixLength = Math.min(commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);207const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);208const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);209const previousSelectionStart = previousState.selectionStart - prefixLength;210const previousSelectionEnd = previousState.selectionEnd - prefixLength;211const currentSelectionStart = currentState.selectionStart - prefixLength;212const currentSelectionEnd = currentState.selectionEnd - prefixLength;213214if (_debugComposition) {215console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);216console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);217}218219return {220text: currentValue,221replacePrevCharCnt: previousSelectionEnd,222replaceNextCharCnt: previousValue.length - previousSelectionEnd,223positionDelta: currentSelectionEnd - currentValue.length224};225}226227public static fromScreenReaderContentState(screenReaderContentState: ISimpleScreenReaderContentState) {228return new TextAreaState(229screenReaderContentState.value,230screenReaderContentState.selectionStart,231screenReaderContentState.selectionEnd,232screenReaderContentState.selection,233screenReaderContentState.newlineCountBeforeSelection234);235}236}237238239