Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.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 './textAreaEditContext.css';6import * as nls from '../../../../../nls.js';7import * as browser from '../../../../../base/browser/browser.js';8import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js';9import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';10import * as platform from '../../../../../base/common/platform.js';11import * as strings from '../../../../../base/common/strings.js';12import { applyFontInfo } from '../../../config/domFontInfo.js';13import { ViewController } from '../../../view/viewController.js';14import { PartFingerprint, PartFingerprints } from '../../../view/viewPart.js';15import { LineNumbersOverlay } from '../../../viewParts/lineNumbers/lineNumbers.js';16import { Margin } from '../../../viewParts/margin/margin.js';17import { RenderLineNumbersType, EditorOption, IComputedEditorOptions, EditorOptions } from '../../../../common/config/editorOptions.js';18import { FontInfo } from '../../../../common/config/fontInfo.js';19import { Position } from '../../../../common/core/position.js';20import { Range } from '../../../../common/core/range.js';21import { Selection } from '../../../../common/core/selection.js';22import { ScrollType } from '../../../../common/editorCommon.js';23import { EndOfLinePreference } from '../../../../common/model.js';24import { RenderingContext, RestrictedRenderingContext, HorizontalPosition, LineVisibleRanges } from '../../../view/renderingContext.js';25import { ViewContext } from '../../../../common/viewModel/viewContext.js';26import * as viewEvents from '../../../../common/viewEvents.js';27import { AccessibilitySupport } from '../../../../../platform/accessibility/common/accessibility.js';28import { IEditorAriaOptions } from '../../../editorBrowser.js';29import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../../base/browser/ui/mouseCursor/mouseCursor.js';30import { TokenizationRegistry } from '../../../../common/languages.js';31import { ColorId, ITokenPresentation } from '../../../../common/encodedTokenAttributes.js';32import { Color } from '../../../../../base/common/color.js';33import { IME } from '../../../../../base/common/ime.js';34import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';35import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';36import { AbstractEditContext } from '../editContext.js';37import { ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, TextAreaWrapper } from './textAreaEditContextInput.js';38import { ariaLabelForScreenReaderContent, newlinecount, SimplePagedScreenReaderStrategy } from '../screenReaderUtils.js';39import { ClipboardDataToCopy, getDataToCopy } from '../clipboardUtils.js';40import { _debugComposition, ITypeData, TextAreaState } from './textAreaEditContextState.js';41import { getMapForWordSeparators, WordCharacterClass } from '../../../../common/core/wordCharacterClassifier.js';4243export interface IVisibleRangeProvider {44visibleRangeForPosition(position: Position): HorizontalPosition | null;45linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null;46}4748class VisibleTextAreaData {49_visibleTextAreaBrand: void = undefined;5051public startPosition: Position | null = null;52public endPosition: Position | null = null;5354public visibleTextareaStart: HorizontalPosition | null = null;55public visibleTextareaEnd: HorizontalPosition | null = null;5657/**58* When doing composition, the currently composed text might be split up into59* multiple tokens, then merged again into a single token, etc. Here we attempt60* to keep the presentation of the <textarea> stable by using the previous used61* style if multiple tokens come into play. This avoids flickering.62*/63private _previousPresentation: ITokenPresentation | null = null;6465constructor(66private readonly _context: ViewContext,67public readonly modelLineNumber: number,68public readonly distanceToModelLineStart: number,69public readonly widthOfHiddenLineTextBefore: number,70public readonly distanceToModelLineEnd: number,71) {72}7374prepareRender(visibleRangeProvider: IVisibleRangeProvider): void {75const startModelPosition = new Position(this.modelLineNumber, this.distanceToModelLineStart + 1);76const endModelPosition = new Position(this.modelLineNumber, this._context.viewModel.model.getLineMaxColumn(this.modelLineNumber) - this.distanceToModelLineEnd);7778this.startPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(startModelPosition);79this.endPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(endModelPosition);8081if (this.startPosition.lineNumber === this.endPosition.lineNumber) {82this.visibleTextareaStart = visibleRangeProvider.visibleRangeForPosition(this.startPosition);83this.visibleTextareaEnd = visibleRangeProvider.visibleRangeForPosition(this.endPosition);84} else {85// TODO: what if the view positions are not on the same line?86this.visibleTextareaStart = null;87this.visibleTextareaEnd = null;88}89}9091definePresentation(tokenPresentation: ITokenPresentation | null): ITokenPresentation {92if (!this._previousPresentation) {93// To avoid flickering, once set, always reuse a presentation throughout the entire IME session94if (tokenPresentation) {95this._previousPresentation = tokenPresentation;96} else {97this._previousPresentation = {98foreground: ColorId.DefaultForeground,99italic: false,100bold: false,101underline: false,102strikethrough: false,103};104}105}106return this._previousPresentation;107}108}109110const canUseZeroSizeTextarea = (browser.isFirefox);111112export class TextAreaEditContext extends AbstractEditContext {113114private readonly _viewController: ViewController;115private readonly _visibleRangeProvider: IVisibleRangeProvider;116private _scrollLeft: number;117private _scrollTop: number;118119private _accessibilitySupport!: AccessibilitySupport;120private _accessibilityPageSize!: number;121private _textAreaWrapping!: boolean;122private _textAreaWidth!: number;123private _contentLeft: number;124private _contentWidth: number;125private _contentHeight: number;126private _fontInfo: FontInfo;127private _emptySelectionClipboard: boolean;128private _copyWithSyntaxHighlighting: boolean;129130/**131* Defined only when the text area is visible (composition case).132*/133private _visibleTextArea: VisibleTextAreaData | null;134private _selections: Selection[];135private _modelSelections: Selection[];136137/**138* The position at which the textarea was rendered.139* This is useful for hit-testing and determining the mouse position.140*/141private _lastRenderPosition: Position | null;142143public readonly textArea: FastDomNode<HTMLTextAreaElement>;144public readonly textAreaCover: FastDomNode<HTMLElement>;145private readonly _textAreaInput: TextAreaInput;146147constructor(148context: ViewContext,149overflowGuardContainer: FastDomNode<HTMLElement>,150viewController: ViewController,151visibleRangeProvider: IVisibleRangeProvider,152@IKeybindingService private readonly _keybindingService: IKeybindingService,153@IInstantiationService private readonly _instantiationService: IInstantiationService154) {155super(context);156157this._viewController = viewController;158this._visibleRangeProvider = visibleRangeProvider;159this._scrollLeft = 0;160this._scrollTop = 0;161162const options = this._context.configuration.options;163const layoutInfo = options.get(EditorOption.layoutInfo);164165this._setAccessibilityOptions(options);166this._contentLeft = layoutInfo.contentLeft;167this._contentWidth = layoutInfo.contentWidth;168this._contentHeight = layoutInfo.height;169this._fontInfo = options.get(EditorOption.fontInfo);170this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);171this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting);172173this._visibleTextArea = null;174this._selections = [new Selection(1, 1, 1, 1)];175this._modelSelections = [new Selection(1, 1, 1, 1)];176this._lastRenderPosition = null;177178// Text Area (The focus will always be in the textarea when the cursor is blinking)179this.textArea = createFastDomNode(document.createElement('textarea'));180PartFingerprints.write(this.textArea, PartFingerprint.TextArea);181this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);182this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');183const { tabSize } = this._context.viewModel.model.getOptions();184this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;185this.textArea.setAttribute('autocorrect', 'off');186this.textArea.setAttribute('autocapitalize', 'off');187this.textArea.setAttribute('autocomplete', 'off');188this.textArea.setAttribute('spellcheck', 'false');189this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));190this.textArea.setAttribute('aria-required', options.get(EditorOption.ariaRequired) ? 'true' : 'false');191this.textArea.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));192this.textArea.setAttribute('role', 'textbox');193this.textArea.setAttribute('aria-roledescription', nls.localize('editor', "editor"));194this.textArea.setAttribute('aria-multiline', 'true');195this.textArea.setAttribute('aria-autocomplete', options.get(EditorOption.readOnly) ? 'none' : 'both');196197this._ensureReadOnlyAttribute();198199this.textAreaCover = createFastDomNode(document.createElement('div'));200this.textAreaCover.setPosition('absolute');201202overflowGuardContainer.appendChild(this.textArea);203overflowGuardContainer.appendChild(this.textAreaCover);204205const simplePagedScreenReaderStrategy = new SimplePagedScreenReaderStrategy();206const textAreaInputHost: ITextAreaInputHost = {207getDataToCopy: (): ClipboardDataToCopy => {208return getDataToCopy(this._context.viewModel, this._modelSelections, this._emptySelectionClipboard, this._copyWithSyntaxHighlighting);209},210getScreenReaderContent: (): TextAreaState => {211if (this._accessibilitySupport === AccessibilitySupport.Disabled) {212// We know for a fact that a screen reader is not attached213// On OSX, we write the character before the cursor to allow for "long-press" composition214// Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints215const selection = this._selections[0];216if (platform.isMacintosh && selection.isEmpty()) {217const position = selection.getStartPosition();218219let textBefore = this._getWordBeforePosition(position);220if (textBefore.length === 0) {221textBefore = this._getCharacterBeforePosition(position);222}223224if (textBefore.length > 0) {225return new TextAreaState(textBefore, textBefore.length, textBefore.length, Range.fromPositions(position), 0);226}227}228// on macOS, write current selection into textarea will allow system text services pick selected text,229// but we still want to limit the amount of text given Chromium handles very poorly text even of a few230// thousand chars231// (https://github.com/microsoft/vscode/issues/27799)232const LIMIT_CHARS = 500;233if (platform.isMacintosh && !selection.isEmpty() && this._context.viewModel.getValueLengthInRange(selection, EndOfLinePreference.TextDefined) < LIMIT_CHARS) {234const text = this._context.viewModel.getValueInRange(selection, EndOfLinePreference.TextDefined);235return new TextAreaState(text, 0, text.length, selection, 0);236}237238// on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work239// if the textarea has no content selected. So if there is an editor selection, ensure something240// is selected in the textarea.241if (browser.isSafari && !selection.isEmpty()) {242const placeholderText = 'vscode-placeholder';243return new TextAreaState(placeholderText, 0, placeholderText.length, null, undefined);244}245246return TextAreaState.EMPTY;247}248249if (browser.isAndroid) {250// when tapping in the editor on a word, Android enters composition mode.251// in the `compositionstart` event we cannot clear the textarea, because252// it then forgets to ever send a `compositionend`.253// we therefore only write the current word in the textarea254const selection = this._selections[0];255if (selection.isEmpty()) {256const position = selection.getStartPosition();257const [wordAtPosition, positionOffsetInWord] = this._getAndroidWordAtPosition(position);258if (wordAtPosition.length > 0) {259return new TextAreaState(wordAtPosition, positionOffsetInWord, positionOffsetInWord, Range.fromPositions(position), 0);260}261}262return TextAreaState.EMPTY;263}264265const screenReaderContentState = simplePagedScreenReaderStrategy.fromEditorSelection(this._context.viewModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === AccessibilitySupport.Unknown);266return TextAreaState.fromScreenReaderContentState(screenReaderContentState);267},268269deduceModelPosition: (viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position => {270return this._context.viewModel.deduceModelPositionRelativeToViewPosition(viewAnchorPosition, deltaOffset, lineFeedCnt);271}272};273274const textAreaWrapper = this._register(new TextAreaWrapper(this.textArea.domNode));275this._textAreaInput = this._register(this._instantiationService.createInstance(TextAreaInput, textAreaInputHost, textAreaWrapper, platform.OS, {276isAndroid: browser.isAndroid,277isChrome: browser.isChrome,278isFirefox: browser.isFirefox,279isSafari: browser.isSafari,280}));281282this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {283this._viewController.emitKeyDown(e);284}));285286this._register(this._textAreaInput.onKeyUp((e: IKeyboardEvent) => {287this._viewController.emitKeyUp(e);288}));289290this._register(this._textAreaInput.onPaste((e: IPasteData) => {291let pasteOnNewLine = false;292let multicursorText: string[] | null = null;293let mode: string | null = null;294if (e.metadata) {295pasteOnNewLine = (this._emptySelectionClipboard && !!e.metadata.isFromEmptySelection);296multicursorText = (typeof e.metadata.multicursorText !== 'undefined' ? e.metadata.multicursorText : null);297mode = e.metadata.mode;298}299this._viewController.paste(e.text, pasteOnNewLine, multicursorText, mode);300}));301302this._register(this._textAreaInput.onCut(() => {303this._viewController.cut();304}));305306this._register(this._textAreaInput.onType((e: ITypeData) => {307if (e.replacePrevCharCnt || e.replaceNextCharCnt || e.positionDelta) {308// must be handled through the new command309if (_debugComposition) {310console.log(` => compositionType: <<${e.text}>>, ${e.replacePrevCharCnt}, ${e.replaceNextCharCnt}, ${e.positionDelta}`);311}312this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);313} else {314if (_debugComposition) {315console.log(` => type: <<${e.text}>>`);316}317this._viewController.type(e.text);318}319}));320321this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection: Selection) => {322this._viewController.setSelection(modelSelection);323}));324325this._register(this._textAreaInput.onCompositionStart((e) => {326327// The textarea might contain some content when composition starts.328//329// When we make the textarea visible, it always has a height of 1 line,330// so we don't need to worry too much about content on lines above or below331// the selection.332//333// However, the text on the current line needs to be made visible because334// some IME methods allow to move to other glyphs on the current line335// (by pressing arrow keys).336//337// (1) The textarea might contain only some parts of the current line,338// like the word before the selection. Also, the content inside the textarea339// can grow or shrink as composition occurs. We therefore anchor the textarea340// in terms of distance to a certain line start and line end.341//342// (2) Also, we should not make \t characters visible, because their rendering343// inside the <textarea> will not align nicely with our rendering. We therefore344// will hide (if necessary) some of the leading text on the current line.345346const ta = this.textArea.domNode;347const modelSelection = this._modelSelections[0];348349const { distanceToModelLineStart, widthOfHiddenTextBefore } = (() => {350// Find the text that is on the current line before the selection351const textBeforeSelection = ta.value.substring(0, Math.min(ta.selectionStart, ta.selectionEnd));352const lineFeedOffset1 = textBeforeSelection.lastIndexOf('\n');353const lineTextBeforeSelection = textBeforeSelection.substring(lineFeedOffset1 + 1);354355// We now search to see if we should hide some part of it (if it contains \t)356const tabOffset1 = lineTextBeforeSelection.lastIndexOf('\t');357const desiredVisibleBeforeCharCount = lineTextBeforeSelection.length - tabOffset1 - 1;358const startModelPosition = modelSelection.getStartPosition();359const visibleBeforeCharCount = Math.min(startModelPosition.column - 1, desiredVisibleBeforeCharCount);360const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;361const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);362const { tabSize } = this._context.viewModel.model.getOptions();363const widthOfHiddenTextBefore = measureText(this.textArea.domNode.ownerDocument, hiddenLineTextBefore, this._fontInfo, tabSize);364365return { distanceToModelLineStart, widthOfHiddenTextBefore };366})();367368const { distanceToModelLineEnd } = (() => {369// Find the text that is on the current line after the selection370const textAfterSelection = ta.value.substring(Math.max(ta.selectionStart, ta.selectionEnd));371const lineFeedOffset2 = textAfterSelection.indexOf('\n');372const lineTextAfterSelection = lineFeedOffset2 === -1 ? textAfterSelection : textAfterSelection.substring(0, lineFeedOffset2);373374const tabOffset2 = lineTextAfterSelection.indexOf('\t');375const desiredVisibleAfterCharCount = (tabOffset2 === -1 ? lineTextAfterSelection.length : lineTextAfterSelection.length - tabOffset2 - 1);376const endModelPosition = modelSelection.getEndPosition();377const visibleAfterCharCount = Math.min(this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column, desiredVisibleAfterCharCount);378const distanceToModelLineEnd = this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column - visibleAfterCharCount;379380return { distanceToModelLineEnd };381})();382383// Scroll to reveal the location in the editor where composition occurs384this._context.viewModel.revealRange(385'keyboard',386true,387Range.fromPositions(this._selections[0].getStartPosition()),388viewEvents.VerticalRevealType.Simple,389ScrollType.Immediate390);391392this._visibleTextArea = new VisibleTextAreaData(393this._context,394modelSelection.startLineNumber,395distanceToModelLineStart,396widthOfHiddenTextBefore,397distanceToModelLineEnd,398);399400// We turn off wrapping if the <textarea> becomes visible for composition401this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');402403this._visibleTextArea.prepareRender(this._visibleRangeProvider);404this._render();405406// Show the textarea407this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`);408409this._viewController.compositionStart();410this._context.viewModel.onCompositionStart();411}));412413this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {414if (!this._visibleTextArea) {415return;416}417418this._visibleTextArea.prepareRender(this._visibleRangeProvider);419this._render();420}));421422this._register(this._textAreaInput.onCompositionEnd(() => {423424this._visibleTextArea = null;425426// We turn on wrapping as necessary if the <textarea> hides after composition427this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');428429this._render();430431this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);432this._viewController.compositionEnd();433this._context.viewModel.onCompositionEnd();434}));435436this._register(this._textAreaInput.onFocus(() => {437this._context.viewModel.setHasFocus(true);438}));439440this._register(this._textAreaInput.onBlur(() => {441this._context.viewModel.setHasFocus(false);442}));443444this._register(IME.onDidChange(() => {445this._ensureReadOnlyAttribute();446}));447}448449public get domNode() {450return this.textArea;451}452453public writeScreenReaderContent(reason: string): void {454this._textAreaInput.writeNativeTextAreaContent(reason);455}456457public getTextAreaDomNode(): HTMLTextAreaElement {458return this.textArea.domNode;459}460461public override dispose(): void {462super.dispose();463this.textArea.domNode.remove();464this.textAreaCover.domNode.remove();465}466467private _getAndroidWordAtPosition(position: Position): [string, number] {468const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?';469const lineContent = this._context.viewModel.getLineContent(position.lineNumber);470const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS, []);471472let goingLeft = true;473let startColumn = position.column;474let goingRight = true;475let endColumn = position.column;476let distance = 0;477while (distance < 50 && (goingLeft || goingRight)) {478if (goingLeft && startColumn <= 1) {479goingLeft = false;480}481if (goingLeft) {482const charCode = lineContent.charCodeAt(startColumn - 2);483const charClass = wordSeparators.get(charCode);484if (charClass !== WordCharacterClass.Regular) {485goingLeft = false;486} else {487startColumn--;488}489}490if (goingRight && endColumn > lineContent.length) {491goingRight = false;492}493if (goingRight) {494const charCode = lineContent.charCodeAt(endColumn - 1);495const charClass = wordSeparators.get(charCode);496if (charClass !== WordCharacterClass.Regular) {497goingRight = false;498} else {499endColumn++;500}501}502distance++;503}504505return [lineContent.substring(startColumn - 1, endColumn - 1), position.column - startColumn];506}507508private _getWordBeforePosition(position: Position): string {509const lineContent = this._context.viewModel.getLineContent(position.lineNumber);510const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(EditorOption.wordSeparators), []);511512let column = position.column;513let distance = 0;514while (column > 1) {515const charCode = lineContent.charCodeAt(column - 2);516const charClass = wordSeparators.get(charCode);517if (charClass !== WordCharacterClass.Regular || distance > 50) {518return lineContent.substring(column - 1, position.column - 1);519}520distance++;521column--;522}523return lineContent.substring(0, position.column - 1);524}525526private _getCharacterBeforePosition(position: Position): string {527if (position.column > 1) {528const lineContent = this._context.viewModel.getLineContent(position.lineNumber);529const charBefore = lineContent.charAt(position.column - 2);530if (!strings.isHighSurrogate(charBefore.charCodeAt(0))) {531return charBefore;532}533}534return '';535}536537private _setAccessibilityOptions(options: IComputedEditorOptions): void {538this._accessibilitySupport = options.get(EditorOption.accessibilitySupport);539const accessibilityPageSize = options.get(EditorOption.accessibilityPageSize);540if (this._accessibilitySupport === AccessibilitySupport.Enabled && accessibilityPageSize === EditorOptions.accessibilityPageSize.defaultValue) {541// 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 experience542this._accessibilityPageSize = 500;543} else {544this._accessibilityPageSize = accessibilityPageSize;545}546547// When wrapping is enabled and a screen reader might be attached,548// we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`).549// This is because screen readers will read the text in the textarea and we'd like that the550// wrapping points in the textarea match the wrapping points in the editor.551const layoutInfo = options.get(EditorOption.layoutInfo);552const wrappingColumn = layoutInfo.wrappingColumn;553if (wrappingColumn !== -1 && this._accessibilitySupport !== AccessibilitySupport.Disabled) {554const fontInfo = options.get(EditorOption.fontInfo);555this._textAreaWrapping = true;556this._textAreaWidth = Math.round(wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);557} else {558this._textAreaWrapping = false;559this._textAreaWidth = (canUseZeroSizeTextarea ? 0 : 1);560}561}562563// --- begin event handlers564565public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {566const options = this._context.configuration.options;567const layoutInfo = options.get(EditorOption.layoutInfo);568569this._setAccessibilityOptions(options);570this._contentLeft = layoutInfo.contentLeft;571this._contentWidth = layoutInfo.contentWidth;572this._contentHeight = layoutInfo.height;573this._fontInfo = options.get(EditorOption.fontInfo);574this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);575this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting);576this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');577const { tabSize } = this._context.viewModel.model.getOptions();578this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;579this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));580this.textArea.setAttribute('aria-required', options.get(EditorOption.ariaRequired) ? 'true' : 'false');581this.textArea.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));582583if (e.hasChanged(EditorOption.domReadOnly) || e.hasChanged(EditorOption.readOnly)) {584this._ensureReadOnlyAttribute();585}586587if (e.hasChanged(EditorOption.accessibilitySupport)) {588this._textAreaInput.writeNativeTextAreaContent('strategy changed');589}590591return true;592}593public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {594this._selections = e.selections.slice(0);595this._modelSelections = e.modelSelections.slice(0);596// We must update the <textarea> synchronously, otherwise long press IME on macos breaks.597// See https://github.com/microsoft/vscode/issues/165821598this._textAreaInput.writeNativeTextAreaContent('selection changed');599return true;600}601public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {602// true for inline decorations that can end up relayouting text603return true;604}605public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {606return true;607}608public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {609return true;610}611public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {612return true;613}614public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {615return true;616}617public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {618this._scrollLeft = e.scrollLeft;619this._scrollTop = e.scrollTop;620return true;621}622public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {623return true;624}625626// --- end event handlers627628// --- begin view API629630public isFocused(): boolean {631return this._textAreaInput.isFocused();632}633634public focus(): void {635this._textAreaInput.focusTextArea();636}637638public refreshFocusState() {639this._textAreaInput.refreshFocusState();640}641642public getLastRenderData(): Position | null {643return this._lastRenderPosition;644}645646public setAriaOptions(options: IEditorAriaOptions): void {647if (options.activeDescendant) {648this.textArea.setAttribute('aria-haspopup', 'true');649this.textArea.setAttribute('aria-autocomplete', 'list');650this.textArea.setAttribute('aria-activedescendant', options.activeDescendant);651} else {652this.textArea.setAttribute('aria-haspopup', 'false');653this.textArea.setAttribute('aria-autocomplete', 'both');654this.textArea.removeAttribute('aria-activedescendant');655}656if (options.role) {657this.textArea.setAttribute('role', options.role);658}659}660661// --- end view API662663private _ensureReadOnlyAttribute(): void {664const options = this._context.configuration.options;665// When someone requests to disable IME, we set the "readonly" attribute on the <textarea>.666// This will prevent composition.667const useReadOnly = !IME.enabled || (options.get(EditorOption.domReadOnly) && options.get(EditorOption.readOnly));668if (useReadOnly) {669this.textArea.setAttribute('readonly', 'true');670} else {671this.textArea.removeAttribute('readonly');672}673}674675private _primaryCursorPosition: Position = new Position(1, 1);676private _primaryCursorVisibleRange: HorizontalPosition | null = null;677678public prepareRender(ctx: RenderingContext): void {679this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);680this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition);681this._visibleTextArea?.prepareRender(ctx);682}683684public render(ctx: RestrictedRenderingContext): void {685this._textAreaInput.writeNativeTextAreaContent('render');686this._render();687}688689private _render(): void {690if (this._visibleTextArea) {691// The text area is visible for composition reasons692693const visibleStart = this._visibleTextArea.visibleTextareaStart;694const visibleEnd = this._visibleTextArea.visibleTextareaEnd;695const startPosition = this._visibleTextArea.startPosition;696const endPosition = this._visibleTextArea.endPosition;697if (startPosition && endPosition && visibleStart && visibleEnd && visibleEnd.left >= this._scrollLeft && visibleStart.left <= this._scrollLeft + this._contentWidth) {698const top = (this._context.viewLayout.getVerticalOffsetForLineNumber(this._primaryCursorPosition.lineNumber) - this._scrollTop);699const lineCount = newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));700701let scrollLeft = this._visibleTextArea.widthOfHiddenLineTextBefore;702let left = (this._contentLeft + visibleStart.left - this._scrollLeft);703// See https://github.com/microsoft/vscode/issues/141725#issuecomment-1050670841704// Here we are adding +1 to avoid flickering that might be caused by having a width that is too small.705// This could be caused by rounding errors that might only show up with certain font families.706// In other words, a pixel might be lost when doing something like707// `Math.round(end) - Math.round(start)`708// vs709// `Math.round(end - start)`710let width = visibleEnd.left - visibleStart.left + 1;711if (left < this._contentLeft) {712// the textarea would be rendered on top of the margin,713// so reduce its width. We use the same technique as714// for hiding text before715const delta = (this._contentLeft - left);716left += delta;717scrollLeft += delta;718width -= delta;719}720if (width > this._contentWidth) {721// the textarea would be wider than the content width,722// so reduce its width.723width = this._contentWidth;724}725726// Try to render the textarea with the color/font style to match the text under it727const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(startPosition.lineNumber);728const fontSize = this._context.viewModel.getFontSizeAtPosition(this._primaryCursorPosition);729const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber);730const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1);731const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1);732const textareaSpansSingleToken = (startTokenIndex === endTokenIndex);733const presentation = this._visibleTextArea.definePresentation(734(textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null)735);736737this.textArea.domNode.scrollTop = lineCount * lineHeight;738this.textArea.domNode.scrollLeft = scrollLeft;739740this._doRender({741lastRenderPosition: null,742top: top,743left: left,744width: width,745height: lineHeight,746useCover: false,747color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground],748italic: presentation.italic,749bold: presentation.bold,750underline: presentation.underline,751strikethrough: presentation.strikethrough,752fontSize753});754}755return;756}757758if (!this._primaryCursorVisibleRange) {759// The primary cursor is outside the viewport => place textarea to the top left760this._renderAtTopLeft();761return;762}763764const left = this._contentLeft + this._primaryCursorVisibleRange.left - this._scrollLeft;765if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {766// cursor is outside the viewport767this._renderAtTopLeft();768return;769}770771const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;772if (top < 0 || top > this._contentHeight) {773// cursor is outside the viewport774this._renderAtTopLeft();775return;776}777778// The primary cursor is in the viewport (at least vertically) => place textarea on the cursor779780if (platform.isMacintosh || this._accessibilitySupport === AccessibilitySupport.Enabled) {781// For the popup emoji input, we will make the text area as high as the line height782// We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers783const lineNumber = this._primaryCursorPosition.lineNumber;784const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);785this._doRender({786lastRenderPosition: this._primaryCursorPosition,787top,788left: this._textAreaWrapping ? this._contentLeft : left,789width: this._textAreaWidth,790height: lineHeight,791useCover: false792});793// In case the textarea contains a word, we're going to try to align the textarea's cursor794// with our cursor by scrolling the textarea as much as possible795this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left;796const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? newlinecount(this.textArea.domNode.value.substring(0, this.textArea.domNode.selectionStart));797this.textArea.domNode.scrollTop = lineCount * lineHeight;798return;799}800801this._doRender({802lastRenderPosition: this._primaryCursorPosition,803top: top,804left: this._textAreaWrapping ? this._contentLeft : left,805width: this._textAreaWidth,806height: (canUseZeroSizeTextarea ? 0 : 1),807useCover: false808});809}810811private _renderAtTopLeft(): void {812// (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)813// specifically, when doing Korean IME, setting the textarea to 0x0 breaks IME badly.814this._doRender({815lastRenderPosition: null,816top: 0,817left: 0,818width: this._textAreaWidth,819height: (canUseZeroSizeTextarea ? 0 : 1),820useCover: true821});822}823824private _doRender(renderData: IRenderData): void {825this._lastRenderPosition = renderData.lastRenderPosition;826827const ta = this.textArea;828const tac = this.textAreaCover;829830applyFontInfo(ta, this._fontInfo);831ta.setTop(renderData.top);832ta.setLeft(renderData.left);833ta.setWidth(renderData.width);834ta.setHeight(renderData.height);835ta.setLineHeight(renderData.height);836837ta.setFontSize(renderData.fontSize ?? this._fontInfo.fontSize);838ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : '');839ta.setFontStyle(renderData.italic ? 'italic' : '');840if (renderData.bold) {841// fontWeight is also set by `applyFontInfo`, so only overwrite it if necessary842ta.setFontWeight('bold');843}844ta.setTextDecoration(`${renderData.underline ? ' underline' : ''}${renderData.strikethrough ? ' line-through' : ''}`);845846tac.setTop(renderData.useCover ? renderData.top : 0);847tac.setLeft(renderData.useCover ? renderData.left : 0);848tac.setWidth(renderData.useCover ? renderData.width : 0);849tac.setHeight(renderData.useCover ? renderData.height : 0);850851const options = this._context.configuration.options;852853if (options.get(EditorOption.glyphMargin)) {854tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);855} else {856if (options.get(EditorOption.lineNumbers).renderType !== RenderLineNumbersType.Off) {857tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);858} else {859tac.setClassName('monaco-editor-background textAreaCover');860}861}862}863}864865interface IRenderData {866lastRenderPosition: Position | null;867top: number;868left: number;869width: number;870height: number;871useCover: boolean;872873fontSize?: string | null;874color?: Color | null;875italic?: boolean;876bold?: boolean;877underline?: boolean;878strikethrough?: boolean;879}880881function measureText(targetDocument: Document, text: string, fontInfo: FontInfo, tabSize: number): number {882if (text.length === 0) {883return 0;884}885886const container = targetDocument.createElement('div');887container.style.position = 'absolute';888container.style.top = '-50000px';889container.style.width = '50000px';890891const regularDomNode = targetDocument.createElement('span');892applyFontInfo(regularDomNode, fontInfo);893regularDomNode.style.whiteSpace = 'pre'; // just like the textarea894regularDomNode.style.tabSize = `${tabSize * fontInfo.spaceWidth}px`; // just like the textarea895regularDomNode.append(text);896container.appendChild(regularDomNode);897898targetDocument.body.appendChild(container);899900const res = regularDomNode.offsetWidth;901902container.remove();903904return res;905}906907908