Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.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 './textAreaEditContext.css';
7
import * as nls from '../../../../../nls.js';
8
import * as browser from '../../../../../base/browser/browser.js';
9
import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js';
10
import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
11
import * as platform from '../../../../../base/common/platform.js';
12
import * as strings from '../../../../../base/common/strings.js';
13
import { applyFontInfo } from '../../../config/domFontInfo.js';
14
import { ViewController } from '../../../view/viewController.js';
15
import { PartFingerprint, PartFingerprints } from '../../../view/viewPart.js';
16
import { LineNumbersOverlay } from '../../../viewParts/lineNumbers/lineNumbers.js';
17
import { Margin } from '../../../viewParts/margin/margin.js';
18
import { RenderLineNumbersType, EditorOption, IComputedEditorOptions, EditorOptions } from '../../../../common/config/editorOptions.js';
19
import { FontInfo } from '../../../../common/config/fontInfo.js';
20
import { Position } from '../../../../common/core/position.js';
21
import { Range } from '../../../../common/core/range.js';
22
import { Selection } from '../../../../common/core/selection.js';
23
import { ScrollType } from '../../../../common/editorCommon.js';
24
import { EndOfLinePreference } from '../../../../common/model.js';
25
import { RenderingContext, RestrictedRenderingContext, HorizontalPosition, LineVisibleRanges } from '../../../view/renderingContext.js';
26
import { ViewContext } from '../../../../common/viewModel/viewContext.js';
27
import * as viewEvents from '../../../../common/viewEvents.js';
28
import { AccessibilitySupport } from '../../../../../platform/accessibility/common/accessibility.js';
29
import { IEditorAriaOptions } from '../../../editorBrowser.js';
30
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../../base/browser/ui/mouseCursor/mouseCursor.js';
31
import { TokenizationRegistry } from '../../../../common/languages.js';
32
import { ColorId, ITokenPresentation } from '../../../../common/encodedTokenAttributes.js';
33
import { Color } from '../../../../../base/common/color.js';
34
import { IME } from '../../../../../base/common/ime.js';
35
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
36
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
37
import { AbstractEditContext } from '../editContext.js';
38
import { ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, TextAreaWrapper } from './textAreaEditContextInput.js';
39
import { ariaLabelForScreenReaderContent, newlinecount, SimplePagedScreenReaderStrategy } from '../screenReaderUtils.js';
40
import { _debugComposition, ITypeData, TextAreaState } from './textAreaEditContextState.js';
41
import { getMapForWordSeparators, WordCharacterClass } from '../../../../common/core/wordCharacterClassifier.js';
42
import { TextAreaEditContextRegistry } from './textAreaEditContextRegistry.js';
43
44
export interface IVisibleRangeProvider {
45
visibleRangeForPosition(position: Position): HorizontalPosition | null;
46
linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null;
47
}
48
49
class VisibleTextAreaData {
50
_visibleTextAreaBrand: void = undefined;
51
52
public startPosition: Position | null = null;
53
public endPosition: Position | null = null;
54
55
public visibleTextareaStart: HorizontalPosition | null = null;
56
public visibleTextareaEnd: HorizontalPosition | null = null;
57
58
/**
59
* When doing composition, the currently composed text might be split up into
60
* multiple tokens, then merged again into a single token, etc. Here we attempt
61
* to keep the presentation of the <textarea> stable by using the previous used
62
* style if multiple tokens come into play. This avoids flickering.
63
*/
64
private _previousPresentation: ITokenPresentation | null = null;
65
66
constructor(
67
private readonly _context: ViewContext,
68
public readonly modelLineNumber: number,
69
public readonly distanceToModelLineStart: number,
70
public readonly widthOfHiddenLineTextBefore: number,
71
public readonly distanceToModelLineEnd: number,
72
) {
73
}
74
75
prepareRender(visibleRangeProvider: IVisibleRangeProvider): void {
76
const startModelPosition = new Position(this.modelLineNumber, this.distanceToModelLineStart + 1);
77
const endModelPosition = new Position(this.modelLineNumber, this._context.viewModel.model.getLineMaxColumn(this.modelLineNumber) - this.distanceToModelLineEnd);
78
79
this.startPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(startModelPosition);
80
this.endPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(endModelPosition);
81
82
if (this.startPosition.lineNumber === this.endPosition.lineNumber) {
83
this.visibleTextareaStart = visibleRangeProvider.visibleRangeForPosition(this.startPosition);
84
this.visibleTextareaEnd = visibleRangeProvider.visibleRangeForPosition(this.endPosition);
85
} else {
86
// TODO: what if the view positions are not on the same line?
87
this.visibleTextareaStart = null;
88
this.visibleTextareaEnd = null;
89
}
90
}
91
92
definePresentation(tokenPresentation: ITokenPresentation | null): ITokenPresentation {
93
if (!this._previousPresentation) {
94
// To avoid flickering, once set, always reuse a presentation throughout the entire IME session
95
if (tokenPresentation) {
96
this._previousPresentation = tokenPresentation;
97
} else {
98
this._previousPresentation = {
99
foreground: ColorId.DefaultForeground,
100
italic: false,
101
bold: false,
102
underline: false,
103
strikethrough: false,
104
};
105
}
106
}
107
return this._previousPresentation;
108
}
109
}
110
111
const canUseZeroSizeTextarea = (browser.isFirefox);
112
113
export class TextAreaEditContext extends AbstractEditContext {
114
115
private readonly _viewController: ViewController;
116
private readonly _visibleRangeProvider: IVisibleRangeProvider;
117
private _scrollLeft: number;
118
private _scrollTop: number;
119
120
private _accessibilitySupport!: AccessibilitySupport;
121
private _accessibilityPageSize!: number;
122
private _textAreaWrapping!: boolean;
123
private _textAreaWidth!: number;
124
private _contentLeft: number;
125
private _contentWidth: number;
126
private _contentHeight: number;
127
private _fontInfo: FontInfo;
128
private _emptySelectionClipboard: boolean;
129
130
/**
131
* Defined only when the text area is visible (composition case).
132
*/
133
private _visibleTextArea: VisibleTextAreaData | null;
134
private _selections: Selection[];
135
private _modelSelections: Selection[];
136
137
/**
138
* The position at which the textarea was rendered.
139
* This is useful for hit-testing and determining the mouse position.
140
*/
141
private _lastRenderPosition: Position | null;
142
143
public readonly textArea: FastDomNode<HTMLTextAreaElement>;
144
public readonly textAreaCover: FastDomNode<HTMLElement>;
145
private readonly _textAreaInput: TextAreaInput;
146
147
constructor(
148
ownerID: string,
149
context: ViewContext,
150
overflowGuardContainer: FastDomNode<HTMLElement>,
151
viewController: ViewController,
152
visibleRangeProvider: IVisibleRangeProvider,
153
@IKeybindingService private readonly _keybindingService: IKeybindingService,
154
@IInstantiationService private readonly _instantiationService: IInstantiationService
155
) {
156
super(context);
157
158
this._viewController = viewController;
159
this._visibleRangeProvider = visibleRangeProvider;
160
this._scrollLeft = 0;
161
this._scrollTop = 0;
162
163
const options = this._context.configuration.options;
164
const layoutInfo = options.get(EditorOption.layoutInfo);
165
166
this._setAccessibilityOptions(options);
167
this._contentLeft = layoutInfo.contentLeft;
168
this._contentWidth = layoutInfo.contentWidth;
169
this._contentHeight = layoutInfo.height;
170
this._fontInfo = options.get(EditorOption.fontInfo);
171
this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);
172
173
this._visibleTextArea = null;
174
this._selections = [new Selection(1, 1, 1, 1)];
175
this._modelSelections = [new Selection(1, 1, 1, 1)];
176
this._lastRenderPosition = null;
177
178
// Text Area (The focus will always be in the textarea when the cursor is blinking)
179
this.textArea = createFastDomNode(document.createElement('textarea'));
180
PartFingerprints.write(this.textArea, PartFingerprint.TextArea);
181
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
182
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
183
const { tabSize } = this._context.viewModel.model.getOptions();
184
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
185
this.textArea.setAttribute('autocorrect', 'off');
186
this.textArea.setAttribute('autocapitalize', 'off');
187
this.textArea.setAttribute('autocomplete', 'off');
188
this.textArea.setAttribute('spellcheck', 'false');
189
this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));
190
this.textArea.setAttribute('aria-required', options.get(EditorOption.ariaRequired) ? 'true' : 'false');
191
this.textArea.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));
192
this.textArea.setAttribute('role', 'textbox');
193
this.textArea.setAttribute('aria-roledescription', nls.localize('editor', "editor"));
194
this.textArea.setAttribute('aria-multiline', 'true');
195
this.textArea.setAttribute('aria-autocomplete', options.get(EditorOption.readOnly) ? 'none' : 'both');
196
197
this._ensureReadOnlyAttribute();
198
199
this.textAreaCover = createFastDomNode(document.createElement('div'));
200
this.textAreaCover.setPosition('absolute');
201
202
overflowGuardContainer.appendChild(this.textArea);
203
overflowGuardContainer.appendChild(this.textAreaCover);
204
205
const simplePagedScreenReaderStrategy = new SimplePagedScreenReaderStrategy();
206
const textAreaInputHost: ITextAreaInputHost = {
207
context: this._context,
208
getScreenReaderContent: (): TextAreaState => {
209
if (this._accessibilitySupport === AccessibilitySupport.Disabled) {
210
// We know for a fact that a screen reader is not attached
211
// On OSX, we write the character before the cursor to allow for "long-press" composition
212
// Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints
213
const selection = this._selections[0];
214
if (platform.isMacintosh && selection.isEmpty()) {
215
const position = selection.getStartPosition();
216
217
let textBefore = this._getWordBeforePosition(position);
218
if (textBefore.length === 0) {
219
textBefore = this._getCharacterBeforePosition(position);
220
}
221
222
if (textBefore.length > 0) {
223
return new TextAreaState(textBefore, textBefore.length, textBefore.length, Range.fromPositions(position), 0);
224
}
225
}
226
// on macOS, write current selection into textarea will allow system text services pick selected text,
227
// but we still want to limit the amount of text given Chromium handles very poorly text even of a few
228
// thousand chars
229
// (https://github.com/microsoft/vscode/issues/27799)
230
const LIMIT_CHARS = 500;
231
if (platform.isMacintosh && !selection.isEmpty() && this._context.viewModel.getValueLengthInRange(selection, EndOfLinePreference.TextDefined) < LIMIT_CHARS) {
232
const text = this._context.viewModel.getValueInRange(selection, EndOfLinePreference.TextDefined);
233
return new TextAreaState(text, 0, text.length, selection, 0);
234
}
235
236
// on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work
237
// if the textarea has no content selected. So if there is an editor selection, ensure something
238
// is selected in the textarea.
239
if (browser.isSafari && !selection.isEmpty()) {
240
const placeholderText = 'vscode-placeholder';
241
return new TextAreaState(placeholderText, 0, placeholderText.length, null, undefined);
242
}
243
244
return TextAreaState.EMPTY;
245
}
246
247
if (browser.isAndroid) {
248
// when tapping in the editor on a word, Android enters composition mode.
249
// in the `compositionstart` event we cannot clear the textarea, because
250
// it then forgets to ever send a `compositionend`.
251
// we therefore only write the current word in the textarea
252
const selection = this._selections[0];
253
if (selection.isEmpty()) {
254
const position = selection.getStartPosition();
255
const [wordAtPosition, positionOffsetInWord] = this._getAndroidWordAtPosition(position);
256
if (wordAtPosition.length > 0) {
257
return new TextAreaState(wordAtPosition, positionOffsetInWord, positionOffsetInWord, Range.fromPositions(position), 0);
258
}
259
}
260
return TextAreaState.EMPTY;
261
}
262
263
const screenReaderContentState = simplePagedScreenReaderStrategy.fromEditorSelection(this._context.viewModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === AccessibilitySupport.Unknown);
264
return TextAreaState.fromScreenReaderContentState(screenReaderContentState);
265
},
266
267
deduceModelPosition: (viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position => {
268
return this._context.viewModel.deduceModelPositionRelativeToViewPosition(viewAnchorPosition, deltaOffset, lineFeedCnt);
269
}
270
};
271
272
const textAreaWrapper = this._register(new TextAreaWrapper(this.textArea.domNode));
273
this._textAreaInput = this._register(this._instantiationService.createInstance(TextAreaInput, textAreaInputHost, textAreaWrapper, platform.OS, {
274
isAndroid: browser.isAndroid,
275
isChrome: browser.isChrome,
276
isFirefox: browser.isFirefox,
277
isSafari: browser.isSafari,
278
}));
279
280
// Relay clipboard events from TextAreaInput
281
this._register(this._textAreaInput.onWillCopy(e => this._onWillCopy.fire(e)));
282
this._register(this._textAreaInput.onWillCut(e => this._onWillCut.fire(e)));
283
this._register(this._textAreaInput.onWillPaste(e => this._onWillPaste.fire(e)));
284
285
this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {
286
this._viewController.emitKeyDown(e);
287
}));
288
289
this._register(this._textAreaInput.onKeyUp((e: IKeyboardEvent) => {
290
this._viewController.emitKeyUp(e);
291
}));
292
293
this._register(this._textAreaInput.onPaste((e: IPasteData) => {
294
let pasteOnNewLine = false;
295
let multicursorText: string[] | null = null;
296
let mode: string | null = null;
297
if (e.metadata) {
298
pasteOnNewLine = (this._emptySelectionClipboard && !!e.metadata.isFromEmptySelection);
299
multicursorText = (typeof e.metadata.multicursorText !== 'undefined' ? e.metadata.multicursorText : null);
300
mode = e.metadata.mode;
301
}
302
this._viewController.paste(e.text, pasteOnNewLine, multicursorText, mode);
303
}));
304
305
this._register(this._textAreaInput.onCut(() => {
306
this._viewController.cut();
307
}));
308
309
this._register(this._textAreaInput.onType((e: ITypeData) => {
310
if (e.replacePrevCharCnt || e.replaceNextCharCnt || e.positionDelta) {
311
// must be handled through the new command
312
if (_debugComposition) {
313
console.log(` => compositionType: <<${e.text}>>, ${e.replacePrevCharCnt}, ${e.replaceNextCharCnt}, ${e.positionDelta}`);
314
}
315
this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);
316
} else {
317
if (_debugComposition) {
318
console.log(` => type: <<${e.text}>>`);
319
}
320
this._viewController.type(e.text);
321
}
322
}));
323
324
this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection: Selection) => {
325
this._viewController.setSelection(modelSelection);
326
}));
327
328
this._register(this._textAreaInput.onCompositionStart((e) => {
329
330
// The textarea might contain some content when composition starts.
331
//
332
// When we make the textarea visible, it always has a height of 1 line,
333
// so we don't need to worry too much about content on lines above or below
334
// the selection.
335
//
336
// However, the text on the current line needs to be made visible because
337
// some IME methods allow to move to other glyphs on the current line
338
// (by pressing arrow keys).
339
//
340
// (1) The textarea might contain only some parts of the current line,
341
// like the word before the selection. Also, the content inside the textarea
342
// can grow or shrink as composition occurs. We therefore anchor the textarea
343
// in terms of distance to a certain line start and line end.
344
//
345
// (2) Also, we should not make \t characters visible, because their rendering
346
// inside the <textarea> will not align nicely with our rendering. We therefore
347
// will hide (if necessary) some of the leading text on the current line.
348
349
const ta = this.textArea.domNode;
350
const modelSelection = this._modelSelections[0];
351
352
const { distanceToModelLineStart, widthOfHiddenTextBefore } = (() => {
353
// Find the text that is on the current line before the selection
354
const textBeforeSelection = ta.value.substring(0, Math.min(ta.selectionStart, ta.selectionEnd));
355
const lineFeedOffset1 = textBeforeSelection.lastIndexOf('\n');
356
const lineTextBeforeSelection = textBeforeSelection.substring(lineFeedOffset1 + 1);
357
358
// We now search to see if we should hide some part of it (if it contains \t)
359
const tabOffset1 = lineTextBeforeSelection.lastIndexOf('\t');
360
const desiredVisibleBeforeCharCount = lineTextBeforeSelection.length - tabOffset1 - 1;
361
const startModelPosition = modelSelection.getStartPosition();
362
const visibleBeforeCharCount = Math.min(startModelPosition.column - 1, desiredVisibleBeforeCharCount);
363
const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;
364
const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);
365
const { tabSize } = this._context.viewModel.model.getOptions();
366
const widthOfHiddenTextBefore = measureText(this.textArea.domNode.ownerDocument, hiddenLineTextBefore, this._fontInfo, tabSize);
367
368
return { distanceToModelLineStart, widthOfHiddenTextBefore };
369
})();
370
371
const { distanceToModelLineEnd } = (() => {
372
// Find the text that is on the current line after the selection
373
const textAfterSelection = ta.value.substring(Math.max(ta.selectionStart, ta.selectionEnd));
374
const lineFeedOffset2 = textAfterSelection.indexOf('\n');
375
const lineTextAfterSelection = lineFeedOffset2 === -1 ? textAfterSelection : textAfterSelection.substring(0, lineFeedOffset2);
376
377
const tabOffset2 = lineTextAfterSelection.indexOf('\t');
378
const desiredVisibleAfterCharCount = (tabOffset2 === -1 ? lineTextAfterSelection.length : lineTextAfterSelection.length - tabOffset2 - 1);
379
const endModelPosition = modelSelection.getEndPosition();
380
const visibleAfterCharCount = Math.min(this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column, desiredVisibleAfterCharCount);
381
const distanceToModelLineEnd = this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column - visibleAfterCharCount;
382
383
return { distanceToModelLineEnd };
384
})();
385
386
// Scroll to reveal the location in the editor where composition occurs
387
this._context.viewModel.revealRange(
388
'keyboard',
389
true,
390
Range.fromPositions(this._selections[0].getStartPosition()),
391
viewEvents.VerticalRevealType.Simple,
392
ScrollType.Immediate
393
);
394
395
this._visibleTextArea = new VisibleTextAreaData(
396
this._context,
397
modelSelection.startLineNumber,
398
distanceToModelLineStart,
399
widthOfHiddenTextBefore,
400
distanceToModelLineEnd,
401
);
402
403
// We turn off wrapping if the <textarea> becomes visible for composition
404
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
405
406
this._visibleTextArea.prepareRender(this._visibleRangeProvider);
407
this._render();
408
409
// Show the textarea
410
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`);
411
412
this._viewController.compositionStart();
413
this._context.viewModel.onCompositionStart();
414
}));
415
416
this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
417
if (!this._visibleTextArea) {
418
return;
419
}
420
421
this._visibleTextArea.prepareRender(this._visibleRangeProvider);
422
this._render();
423
}));
424
425
this._register(this._textAreaInput.onCompositionEnd(() => {
426
427
this._visibleTextArea = null;
428
429
// We turn on wrapping as necessary if the <textarea> hides after composition
430
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
431
432
this._render();
433
434
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
435
this._viewController.compositionEnd();
436
this._context.viewModel.onCompositionEnd();
437
}));
438
439
this._register(this._textAreaInput.onFocus(() => {
440
this._context.viewModel.setHasFocus(true);
441
}));
442
443
this._register(this._textAreaInput.onBlur(() => {
444
this._context.viewModel.setHasFocus(false);
445
}));
446
447
this._register(IME.onDidChange(() => {
448
this._ensureReadOnlyAttribute();
449
}));
450
451
this._register(TextAreaEditContextRegistry.register(ownerID, this));
452
}
453
454
public get domNode() {
455
return this.textArea;
456
}
457
458
public writeScreenReaderContent(reason: string): void {
459
this._textAreaInput.writeNativeTextAreaContent(reason);
460
}
461
462
public getTextAreaDomNode(): HTMLTextAreaElement {
463
return this.textArea.domNode;
464
}
465
466
public override dispose(): void {
467
super.dispose();
468
this.textArea.domNode.remove();
469
this.textAreaCover.domNode.remove();
470
}
471
472
private _getAndroidWordAtPosition(position: Position): [string, number] {
473
const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?';
474
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
475
const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS, []);
476
477
let goingLeft = true;
478
let startColumn = position.column;
479
let goingRight = true;
480
let endColumn = position.column;
481
let distance = 0;
482
while (distance < 50 && (goingLeft || goingRight)) {
483
if (goingLeft && startColumn <= 1) {
484
goingLeft = false;
485
}
486
if (goingLeft) {
487
const charCode = lineContent.charCodeAt(startColumn - 2);
488
const charClass = wordSeparators.get(charCode);
489
if (charClass !== WordCharacterClass.Regular) {
490
goingLeft = false;
491
} else {
492
startColumn--;
493
}
494
}
495
if (goingRight && endColumn > lineContent.length) {
496
goingRight = false;
497
}
498
if (goingRight) {
499
const charCode = lineContent.charCodeAt(endColumn - 1);
500
const charClass = wordSeparators.get(charCode);
501
if (charClass !== WordCharacterClass.Regular) {
502
goingRight = false;
503
} else {
504
endColumn++;
505
}
506
}
507
distance++;
508
}
509
510
return [lineContent.substring(startColumn - 1, endColumn - 1), position.column - startColumn];
511
}
512
513
private _getWordBeforePosition(position: Position): string {
514
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
515
const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(EditorOption.wordSeparators), []);
516
517
let column = position.column;
518
let distance = 0;
519
while (column > 1) {
520
const charCode = lineContent.charCodeAt(column - 2);
521
const charClass = wordSeparators.get(charCode);
522
if (charClass !== WordCharacterClass.Regular || distance > 50) {
523
return lineContent.substring(column - 1, position.column - 1);
524
}
525
distance++;
526
column--;
527
}
528
return lineContent.substring(0, position.column - 1);
529
}
530
531
private _getCharacterBeforePosition(position: Position): string {
532
if (position.column > 1) {
533
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
534
const charBefore = lineContent.charAt(position.column - 2);
535
if (!strings.isHighSurrogate(charBefore.charCodeAt(0))) {
536
return charBefore;
537
}
538
}
539
return '';
540
}
541
542
private _setAccessibilityOptions(options: IComputedEditorOptions): void {
543
this._accessibilitySupport = options.get(EditorOption.accessibilitySupport);
544
const accessibilityPageSize = options.get(EditorOption.accessibilityPageSize);
545
if (this._accessibilitySupport === AccessibilitySupport.Enabled && accessibilityPageSize === EditorOptions.accessibilityPageSize.defaultValue) {
546
// If a screen reader is attached and the default value is not set we should automatically increase the page size to 500 for a better experience
547
this._accessibilityPageSize = 500;
548
} else {
549
this._accessibilityPageSize = accessibilityPageSize;
550
}
551
552
// When wrapping is enabled and a screen reader might be attached,
553
// we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`).
554
// This is because screen readers will read the text in the textarea and we'd like that the
555
// wrapping points in the textarea match the wrapping points in the editor.
556
const layoutInfo = options.get(EditorOption.layoutInfo);
557
const wrappingColumn = layoutInfo.wrappingColumn;
558
if (wrappingColumn !== -1 && this._accessibilitySupport !== AccessibilitySupport.Disabled) {
559
const fontInfo = options.get(EditorOption.fontInfo);
560
this._textAreaWrapping = true;
561
this._textAreaWidth = Math.round(wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
562
} else {
563
this._textAreaWrapping = false;
564
this._textAreaWidth = (canUseZeroSizeTextarea ? 0 : 1);
565
}
566
}
567
568
// --- begin event handlers
569
570
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
571
const options = this._context.configuration.options;
572
const layoutInfo = options.get(EditorOption.layoutInfo);
573
574
this._setAccessibilityOptions(options);
575
this._contentLeft = layoutInfo.contentLeft;
576
this._contentWidth = layoutInfo.contentWidth;
577
this._contentHeight = layoutInfo.height;
578
this._fontInfo = options.get(EditorOption.fontInfo);
579
this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);
580
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
581
const { tabSize } = this._context.viewModel.model.getOptions();
582
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
583
this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));
584
this.textArea.setAttribute('aria-required', options.get(EditorOption.ariaRequired) ? 'true' : 'false');
585
this.textArea.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));
586
587
if (e.hasChanged(EditorOption.domReadOnly) || e.hasChanged(EditorOption.readOnly)) {
588
this._ensureReadOnlyAttribute();
589
}
590
591
if (e.hasChanged(EditorOption.accessibilitySupport)) {
592
this._textAreaInput.writeNativeTextAreaContent('strategy changed');
593
}
594
595
return true;
596
}
597
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
598
this._selections = e.selections.slice(0);
599
this._modelSelections = e.modelSelections.slice(0);
600
// We must update the <textarea> synchronously, otherwise long press IME on macos breaks.
601
// See https://github.com/microsoft/vscode/issues/165821
602
this._textAreaInput.writeNativeTextAreaContent('selection changed');
603
return true;
604
}
605
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
606
// true for inline decorations that can end up relayouting text
607
return true;
608
}
609
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
610
return true;
611
}
612
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
613
return true;
614
}
615
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
616
return true;
617
}
618
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
619
return true;
620
}
621
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
622
this._scrollLeft = e.scrollLeft;
623
this._scrollTop = e.scrollTop;
624
return true;
625
}
626
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
627
return true;
628
}
629
630
// --- end event handlers
631
632
// --- begin view API
633
634
public isFocused(): boolean {
635
return this._textAreaInput.isFocused();
636
}
637
638
public focus(): void {
639
this._textAreaInput.focusTextArea();
640
}
641
642
public refreshFocusState() {
643
this._textAreaInput.refreshFocusState();
644
}
645
646
public getLastRenderData(): Position | null {
647
return this._lastRenderPosition;
648
}
649
650
public setAriaOptions(options: IEditorAriaOptions): void {
651
if (options.activeDescendant) {
652
this.textArea.setAttribute('aria-haspopup', 'true');
653
this.textArea.setAttribute('aria-autocomplete', 'list');
654
this.textArea.setAttribute('aria-activedescendant', options.activeDescendant);
655
} else {
656
this.textArea.setAttribute('aria-haspopup', 'false');
657
this.textArea.setAttribute('aria-autocomplete', 'both');
658
this.textArea.removeAttribute('aria-activedescendant');
659
}
660
if (options.role) {
661
this.textArea.setAttribute('role', options.role);
662
}
663
}
664
665
// --- end view API
666
667
private _ensureReadOnlyAttribute(): void {
668
const options = this._context.configuration.options;
669
// When someone requests to disable IME, we set the "readonly" attribute on the <textarea>.
670
// This will prevent composition.
671
const useReadOnly = !IME.enabled || (options.get(EditorOption.domReadOnly) && options.get(EditorOption.readOnly));
672
if (useReadOnly) {
673
this.textArea.setAttribute('readonly', 'true');
674
} else {
675
this.textArea.removeAttribute('readonly');
676
}
677
}
678
679
private _primaryCursorPosition: Position = new Position(1, 1);
680
private _primaryCursorVisibleRange: HorizontalPosition | null = null;
681
682
public prepareRender(ctx: RenderingContext): void {
683
this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);
684
this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition);
685
this._visibleTextArea?.prepareRender(ctx);
686
}
687
688
public render(ctx: RestrictedRenderingContext): void {
689
this._textAreaInput.writeNativeTextAreaContent('render');
690
this._render();
691
}
692
693
private _render(): void {
694
if (this._visibleTextArea) {
695
// The text area is visible for composition reasons
696
697
const visibleStart = this._visibleTextArea.visibleTextareaStart;
698
const visibleEnd = this._visibleTextArea.visibleTextareaEnd;
699
const startPosition = this._visibleTextArea.startPosition;
700
const endPosition = this._visibleTextArea.endPosition;
701
if (startPosition && endPosition && visibleStart && visibleEnd && visibleEnd.left >= this._scrollLeft && visibleStart.left <= this._scrollLeft + this._contentWidth) {
702
const top = (this._context.viewLayout.getVerticalOffsetForLineNumber(this._primaryCursorPosition.lineNumber) - this._scrollTop);
703
const lineCount = newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
704
705
let scrollLeft = this._visibleTextArea.widthOfHiddenLineTextBefore;
706
let left = (this._contentLeft + visibleStart.left - this._scrollLeft);
707
// See https://github.com/microsoft/vscode/issues/141725#issuecomment-1050670841
708
// Here we are adding +1 to avoid flickering that might be caused by having a width that is too small.
709
// This could be caused by rounding errors that might only show up with certain font families.
710
// In other words, a pixel might be lost when doing something like
711
// `Math.round(end) - Math.round(start)`
712
// vs
713
// `Math.round(end - start)`
714
let width = visibleEnd.left - visibleStart.left + 1;
715
if (left < this._contentLeft) {
716
// the textarea would be rendered on top of the margin,
717
// so reduce its width. We use the same technique as
718
// for hiding text before
719
const delta = (this._contentLeft - left);
720
left += delta;
721
scrollLeft += delta;
722
width -= delta;
723
}
724
if (width > this._contentWidth) {
725
// the textarea would be wider than the content width,
726
// so reduce its width.
727
width = this._contentWidth;
728
}
729
730
// Try to render the textarea with the color/font style to match the text under it
731
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(startPosition.lineNumber);
732
const fontSize = this._context.viewModel.getFontSizeAtPosition(this._primaryCursorPosition);
733
const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber);
734
const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1);
735
const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1);
736
const textareaSpansSingleToken = (startTokenIndex === endTokenIndex);
737
const presentation = this._visibleTextArea.definePresentation(
738
(textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null)
739
);
740
741
this.textArea.domNode.scrollTop = lineCount * lineHeight;
742
this.textArea.domNode.scrollLeft = scrollLeft;
743
744
this._doRender({
745
lastRenderPosition: null,
746
top: top,
747
left: left,
748
width: width,
749
height: lineHeight,
750
useCover: false,
751
color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground],
752
italic: presentation.italic,
753
bold: presentation.bold,
754
underline: presentation.underline,
755
strikethrough: presentation.strikethrough,
756
fontSize
757
});
758
}
759
return;
760
}
761
762
if (!this._primaryCursorVisibleRange) {
763
// The primary cursor is outside the viewport => place textarea to the top left
764
this._renderAtTopLeft();
765
return;
766
}
767
768
const left = this._contentLeft + this._primaryCursorVisibleRange.left - this._scrollLeft;
769
if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {
770
// cursor is outside the viewport
771
this._renderAtTopLeft();
772
return;
773
}
774
775
const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;
776
if (top < 0 || top > this._contentHeight) {
777
// cursor is outside the viewport
778
this._renderAtTopLeft();
779
return;
780
}
781
782
// The primary cursor is in the viewport (at least vertically) => place textarea on the cursor
783
784
if (platform.isMacintosh || this._accessibilitySupport === AccessibilitySupport.Enabled) {
785
// For the popup emoji input, we will make the text area as high as the line height
786
// We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers
787
const lineNumber = this._primaryCursorPosition.lineNumber;
788
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);
789
this._doRender({
790
lastRenderPosition: this._primaryCursorPosition,
791
top,
792
left: this._textAreaWrapping ? this._contentLeft : left,
793
width: this._textAreaWidth,
794
height: lineHeight,
795
useCover: false
796
});
797
// In case the textarea contains a word, we're going to try to align the textarea's cursor
798
// with our cursor by scrolling the textarea as much as possible
799
this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left;
800
const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? newlinecount(this.textArea.domNode.value.substring(0, this.textArea.domNode.selectionStart));
801
this.textArea.domNode.scrollTop = lineCount * lineHeight;
802
return;
803
}
804
805
this._doRender({
806
lastRenderPosition: this._primaryCursorPosition,
807
top: top,
808
left: this._textAreaWrapping ? this._contentLeft : left,
809
width: this._textAreaWidth,
810
height: (canUseZeroSizeTextarea ? 0 : 1),
811
useCover: false
812
});
813
}
814
815
private _renderAtTopLeft(): void {
816
// (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)
817
// specifically, when doing Korean IME, setting the textarea to 0x0 breaks IME badly.
818
this._doRender({
819
lastRenderPosition: null,
820
top: 0,
821
left: 0,
822
width: this._textAreaWidth,
823
height: (canUseZeroSizeTextarea ? 0 : 1),
824
useCover: true
825
});
826
}
827
828
private _doRender(renderData: IRenderData): void {
829
this._lastRenderPosition = renderData.lastRenderPosition;
830
831
const ta = this.textArea;
832
const tac = this.textAreaCover;
833
834
applyFontInfo(ta, this._fontInfo);
835
ta.setTop(renderData.top);
836
ta.setLeft(renderData.left);
837
ta.setWidth(renderData.width);
838
ta.setHeight(renderData.height);
839
ta.setLineHeight(renderData.height);
840
841
ta.setFontSize(renderData.fontSize ?? this._fontInfo.fontSize);
842
ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : '');
843
ta.setFontStyle(renderData.italic ? 'italic' : '');
844
if (renderData.bold) {
845
// fontWeight is also set by `applyFontInfo`, so only overwrite it if necessary
846
ta.setFontWeight('bold');
847
}
848
ta.setTextDecoration(`${renderData.underline ? ' underline' : ''}${renderData.strikethrough ? ' line-through' : ''}`);
849
850
tac.setTop(renderData.useCover ? renderData.top : 0);
851
tac.setLeft(renderData.useCover ? renderData.left : 0);
852
tac.setWidth(renderData.useCover ? renderData.width : 0);
853
tac.setHeight(renderData.useCover ? renderData.height : 0);
854
855
const options = this._context.configuration.options;
856
857
if (options.get(EditorOption.glyphMargin)) {
858
tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);
859
} else {
860
if (options.get(EditorOption.lineNumbers).renderType !== RenderLineNumbersType.Off) {
861
tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
862
} else {
863
tac.setClassName('monaco-editor-background textAreaCover');
864
}
865
}
866
}
867
}
868
869
interface IRenderData {
870
lastRenderPosition: Position | null;
871
top: number;
872
left: number;
873
width: number;
874
height: number;
875
useCover: boolean;
876
877
fontSize?: string | null;
878
color?: Color | null;
879
italic?: boolean;
880
bold?: boolean;
881
underline?: boolean;
882
strikethrough?: boolean;
883
}
884
885
function measureText(targetDocument: Document, text: string, fontInfo: FontInfo, tabSize: number): number {
886
if (text.length === 0) {
887
return 0;
888
}
889
890
const container = targetDocument.createElement('div');
891
container.style.position = 'absolute';
892
container.style.top = '-50000px';
893
container.style.width = '50000px';
894
895
const regularDomNode = targetDocument.createElement('span');
896
applyFontInfo(regularDomNode, fontInfo);
897
regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
898
regularDomNode.style.tabSize = `${tabSize * fontInfo.spaceWidth}px`; // just like the textarea
899
regularDomNode.append(text);
900
container.appendChild(regularDomNode);
901
902
targetDocument.body.appendChild(container);
903
904
const res = regularDomNode.offsetWidth;
905
906
container.remove();
907
908
return res;
909
}
910
911