Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { commonPrefixLength, commonSuffixLength } from '../../../../../base/common/strings.js';
7
import { Position } from '../../../../common/core/position.js';
8
import { Range } from '../../../../common/core/range.js';
9
import { SelectionDirection } from '../../../../common/core/selection.js';
10
import { ISimpleScreenReaderContentState } from '../screenReaderUtils.js';
11
12
export const _debugComposition = false;
13
14
export interface ITextAreaWrapper {
15
getValue(): string;
16
setValue(reason: string, value: string): void;
17
18
getSelectionStart(): number;
19
getSelectionEnd(): number;
20
setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void;
21
}
22
23
export interface ITypeData {
24
text: string;
25
replacePrevCharCnt: number;
26
replaceNextCharCnt: number;
27
positionDelta: number;
28
}
29
30
export class TextAreaState {
31
32
public static readonly EMPTY = new TextAreaState('', 0, 0, null, undefined);
33
34
constructor(
35
public readonly value: string,
36
/** the offset where selection starts inside `value` */
37
public readonly selectionStart: number,
38
/** the offset where selection ends inside `value` */
39
public readonly selectionEnd: number,
40
/** the editor range in the view coordinate system that matches the selection inside `value` */
41
public readonly selection: Range | null,
42
/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */
43
public readonly newlineCountBeforeSelection: number | undefined,
44
) { }
45
46
public toString(): string {
47
return `[ <${this.value}>, selectionStart: ${this.selectionStart}, selectionEnd: ${this.selectionEnd}]`;
48
}
49
50
public static readFromTextArea(textArea: ITextAreaWrapper, previousState: TextAreaState | null): TextAreaState {
51
const value = textArea.getValue();
52
const selectionStart = textArea.getSelectionStart();
53
const selectionEnd = textArea.getSelectionEnd();
54
let newlineCountBeforeSelection: number | undefined = undefined;
55
if (previousState) {
56
const valueBeforeSelectionStart = value.substring(0, selectionStart);
57
const previousValueBeforeSelectionStart = previousState.value.substring(0, previousState.selectionStart);
58
if (valueBeforeSelectionStart === previousValueBeforeSelectionStart) {
59
newlineCountBeforeSelection = previousState.newlineCountBeforeSelection;
60
}
61
}
62
return new TextAreaState(value, selectionStart, selectionEnd, null, newlineCountBeforeSelection);
63
}
64
65
public collapseSelection(): TextAreaState {
66
if (this.selectionStart === this.value.length) {
67
return this;
68
}
69
return new TextAreaState(this.value, this.value.length, this.value.length, null, undefined);
70
}
71
72
public isWrittenToTextArea(textArea: ITextAreaWrapper, select: boolean): boolean {
73
const valuesEqual = this.value === textArea.getValue();
74
if (!select) {
75
return valuesEqual;
76
}
77
const selectionsEqual = this.selectionStart === textArea.getSelectionStart() && this.selectionEnd === textArea.getSelectionEnd();
78
return selectionsEqual && valuesEqual;
79
}
80
81
public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void {
82
if (_debugComposition) {
83
console.log(`writeToTextArea ${reason}: ${this.toString()}`);
84
}
85
textArea.setValue(reason, this.value);
86
if (select) {
87
textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);
88
}
89
}
90
91
public deduceEditorPosition(offset: number): [Position | null, number, number] {
92
if (offset <= this.selectionStart) {
93
const str = this.value.substring(offset, this.selectionStart);
94
return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str, -1);
95
}
96
if (offset >= this.selectionEnd) {
97
const str = this.value.substring(this.selectionEnd, offset);
98
return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str, 1);
99
}
100
const str1 = this.value.substring(this.selectionStart, offset);
101
if (str1.indexOf(String.fromCharCode(8230)) === -1) {
102
return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str1, 1);
103
}
104
const str2 = this.value.substring(offset, this.selectionEnd);
105
return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str2, -1);
106
}
107
108
private _finishDeduceEditorPosition(anchor: Position | null, deltaText: string, signum: number): [Position | null, number, number] {
109
let lineFeedCnt = 0;
110
let lastLineFeedIndex = -1;
111
while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {
112
lineFeedCnt++;
113
}
114
return [anchor, signum * deltaText.length, lineFeedCnt];
115
}
116
117
public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {
118
if (!previousState) {
119
// This is the EMPTY state
120
return {
121
text: '',
122
replacePrevCharCnt: 0,
123
replaceNextCharCnt: 0,
124
positionDelta: 0
125
};
126
}
127
128
if (_debugComposition) {
129
console.log('------------------------deduceInput');
130
console.log(`PREVIOUS STATE: ${previousState.toString()}`);
131
console.log(`CURRENT STATE: ${currentState.toString()}`);
132
}
133
134
const prefixLength = Math.min(
135
commonPrefixLength(previousState.value, currentState.value),
136
previousState.selectionStart,
137
currentState.selectionStart
138
);
139
const suffixLength = Math.min(
140
commonSuffixLength(previousState.value, currentState.value),
141
previousState.value.length - previousState.selectionEnd,
142
currentState.value.length - currentState.selectionEnd
143
);
144
const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
145
const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
146
const previousSelectionStart = previousState.selectionStart - prefixLength;
147
const previousSelectionEnd = previousState.selectionEnd - prefixLength;
148
const currentSelectionStart = currentState.selectionStart - prefixLength;
149
const currentSelectionEnd = currentState.selectionEnd - prefixLength;
150
151
if (_debugComposition) {
152
console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);
153
console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);
154
}
155
156
if (currentSelectionStart === currentSelectionEnd) {
157
// no current selection
158
const replacePreviousCharacters = (previousState.selectionStart - prefixLength);
159
if (_debugComposition) {
160
console.log(`REMOVE PREVIOUS: ${replacePreviousCharacters} chars`);
161
}
162
163
return {
164
text: currentValue,
165
replacePrevCharCnt: replacePreviousCharacters,
166
replaceNextCharCnt: 0,
167
positionDelta: 0
168
};
169
}
170
171
// there is a current selection => composition case
172
const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
173
return {
174
text: currentValue,
175
replacePrevCharCnt: replacePreviousCharacters,
176
replaceNextCharCnt: 0,
177
positionDelta: 0
178
};
179
}
180
181
public static deduceAndroidCompositionInput(previousState: TextAreaState, currentState: TextAreaState): ITypeData {
182
if (!previousState) {
183
// This is the EMPTY state
184
return {
185
text: '',
186
replacePrevCharCnt: 0,
187
replaceNextCharCnt: 0,
188
positionDelta: 0
189
};
190
}
191
192
if (_debugComposition) {
193
console.log('------------------------deduceAndroidCompositionInput');
194
console.log(`PREVIOUS STATE: ${previousState.toString()}`);
195
console.log(`CURRENT STATE: ${currentState.toString()}`);
196
}
197
198
if (previousState.value === currentState.value) {
199
return {
200
text: '',
201
replacePrevCharCnt: 0,
202
replaceNextCharCnt: 0,
203
positionDelta: currentState.selectionEnd - previousState.selectionEnd
204
};
205
}
206
207
const prefixLength = Math.min(commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);
208
const suffixLength = Math.min(commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);
209
const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
210
const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
211
const previousSelectionStart = previousState.selectionStart - prefixLength;
212
const previousSelectionEnd = previousState.selectionEnd - prefixLength;
213
const currentSelectionStart = currentState.selectionStart - prefixLength;
214
const currentSelectionEnd = currentState.selectionEnd - prefixLength;
215
216
if (_debugComposition) {
217
console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);
218
console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);
219
}
220
221
return {
222
text: currentValue,
223
replacePrevCharCnt: previousSelectionEnd,
224
replaceNextCharCnt: previousValue.length - previousSelectionEnd,
225
positionDelta: currentSelectionEnd - currentValue.length
226
};
227
}
228
229
public static fromScreenReaderContentState(screenReaderContentState: ISimpleScreenReaderContentState) {
230
let selectionStart;
231
let selectionEnd;
232
const direction = screenReaderContentState.selection.getDirection();
233
switch (direction) {
234
case SelectionDirection.LTR:
235
selectionStart = screenReaderContentState.selectionStart;
236
selectionEnd = screenReaderContentState.selectionEnd;
237
break;
238
case SelectionDirection.RTL:
239
selectionStart = screenReaderContentState.selectionEnd;
240
selectionEnd = screenReaderContentState.selectionStart;
241
break;
242
}
243
return new TextAreaState(
244
screenReaderContentState.value,
245
selectionStart,
246
selectionEnd,
247
screenReaderContentState.selection,
248
screenReaderContentState.newlineCountBeforeSelection
249
);
250
}
251
}
252
253