Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as browser from '../../../../../base/browser/browser.js';6import * as dom from '../../../../../base/browser/dom.js';7import { DomEmitter } from '../../../../../base/browser/event.js';8import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';9import { inputLatency } from '../../../../../base/browser/performance.js';10import { RunOnceScheduler } from '../../../../../base/common/async.js';11import { Emitter, Event } from '../../../../../base/common/event.js';12import { KeyCode } from '../../../../../base/common/keyCodes.js';13import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';14import { OperatingSystem } from '../../../../../base/common/platform.js';15import * as strings from '../../../../../base/common/strings.js';16import { Position } from '../../../../common/core/position.js';17import { Selection } from '../../../../common/core/selection.js';18import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';19import { ILogService } from '../../../../../platform/log/common/log.js';20import { ClipboardStoredMetadata, CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js';21import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js';22import { ViewContext } from '../../../../common/viewModel/viewContext.js';2324export namespace TextAreaSyntethicEvents {25export const Tap = '-monaco-textarea-synthetic-tap';26}2728export interface ICompositionData {29data: string;30}313233export interface IPasteData {34text: string;35metadata: ClipboardStoredMetadata | null;36}3738export interface ITextAreaInputHost {39readonly context: ViewContext;40getScreenReaderContent(): TextAreaState;41deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position;42}4344export interface ICompositionStartEvent {45data: string;46}4748export interface ICompleteTextAreaWrapper extends ITextAreaWrapper {49readonly onKeyDown: Event<KeyboardEvent>;50readonly onKeyPress: Event<KeyboardEvent>;51readonly onKeyUp: Event<KeyboardEvent>;52readonly onCompositionStart: Event<CompositionEvent>;53readonly onCompositionUpdate: Event<CompositionEvent>;54readonly onCompositionEnd: Event<CompositionEvent>;55readonly onBeforeInput: Event<InputEvent>;56readonly onInput: Event<InputEvent>;57readonly onCut: Event<ClipboardEvent>;58readonly onCopy: Event<ClipboardEvent>;59readonly onPaste: Event<ClipboardEvent>;60readonly onFocus: Event<FocusEvent>;61readonly onBlur: Event<FocusEvent>;62readonly onSyntheticTap: Event<void>;6364readonly ownerDocument: Document;6566setIgnoreSelectionChangeTime(reason: string): void;67getIgnoreSelectionChangeTime(): number;68resetSelectionChangeTime(): void;6970hasFocus(): boolean;71}7273export interface IBrowser {74isAndroid: boolean;75isFirefox: boolean;76isChrome: boolean;77isSafari: boolean;78}7980class CompositionContext {8182private _lastTypeTextLength: number;8384constructor() {85this._lastTypeTextLength = 0;86}8788public handleCompositionUpdate(text: string | null | undefined): ITypeData {89text = text || '';90const typeInput: ITypeData = {91text: text,92replacePrevCharCnt: this._lastTypeTextLength,93replaceNextCharCnt: 0,94positionDelta: 095};96this._lastTypeTextLength = text.length;97return typeInput;98}99}100101/**102* Writes screen reader content to the textarea and is able to analyze its input events to generate:103* - onCut104* - onPaste105* - onType106*107* Composition events are generated for presentation purposes (composition input is reflected in onType).108*/109export class TextAreaInput extends Disposable {110111private _onFocus = this._register(new Emitter<void>());112public readonly onFocus: Event<void> = this._onFocus.event;113114private _onBlur = this._register(new Emitter<void>());115public readonly onBlur: Event<void> = this._onBlur.event;116117private _onKeyDown = this._register(new Emitter<IKeyboardEvent>());118public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;119120private _onKeyUp = this._register(new Emitter<IKeyboardEvent>());121public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;122123private _onCut = this._register(new Emitter<void>());124public readonly onCut: Event<void> = this._onCut.event;125126private _onPaste = this._register(new Emitter<IPasteData>());127public readonly onPaste: Event<IPasteData> = this._onPaste.event;128129private _onWillCopy = this._register(new Emitter<IClipboardCopyEvent>());130public readonly onWillCopy: Event<IClipboardCopyEvent> = this._onWillCopy.event;131132private _onWillCut = this._register(new Emitter<IClipboardCopyEvent>());133public readonly onWillCut: Event<IClipboardCopyEvent> = this._onWillCut.event;134135private _onWillPaste = this._register(new Emitter<IClipboardPasteEvent>());136public readonly onWillPaste: Event<IClipboardPasteEvent> = this._onWillPaste.event;137138private _onType = this._register(new Emitter<ITypeData>());139public readonly onType: Event<ITypeData> = this._onType.event;140141private _onCompositionStart = this._register(new Emitter<ICompositionStartEvent>());142public readonly onCompositionStart: Event<ICompositionStartEvent> = this._onCompositionStart.event;143144private _onCompositionUpdate = this._register(new Emitter<ICompositionData>());145public readonly onCompositionUpdate: Event<ICompositionData> = this._onCompositionUpdate.event;146147private _onCompositionEnd = this._register(new Emitter<void>());148public readonly onCompositionEnd: Event<void> = this._onCompositionEnd.event;149150private _onSelectionChangeRequest = this._register(new Emitter<Selection>());151public readonly onSelectionChangeRequest: Event<Selection> = this._onSelectionChangeRequest.event;152153// ---154155private readonly _asyncTriggerCut: RunOnceScheduler;156157private readonly _asyncFocusGainWriteScreenReaderContent: MutableDisposable<RunOnceScheduler> = this._register(new MutableDisposable());158159private _textAreaState: TextAreaState;160161public get textAreaState(): TextAreaState {162return this._textAreaState;163}164165private _selectionChangeListener: IDisposable | null;166167private _hasFocus: boolean;168private _currentComposition: CompositionContext | null;169170constructor(171private readonly _host: ITextAreaInputHost,172private readonly _textArea: ICompleteTextAreaWrapper,173private readonly _OS: OperatingSystem,174private readonly _browser: IBrowser,175@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,176@ILogService private readonly _logService: ILogService177) {178super();179this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));180this._textAreaState = TextAreaState.EMPTY;181this._selectionChangeListener = null;182if (this._accessibilityService.isScreenReaderOptimized()) {183this.writeNativeTextAreaContent('ctor');184}185this._register(Event.runAndSubscribe(this._accessibilityService.onDidChangeScreenReaderOptimized, () => {186if (this._accessibilityService.isScreenReaderOptimized() && !this._asyncFocusGainWriteScreenReaderContent.value) {187// Don't use this._register() here - the MutableDisposable already handles cleanup188this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);189} else {190this._asyncFocusGainWriteScreenReaderContent.clear();191}192}));193this._hasFocus = false;194this._currentComposition = null;195196let lastKeyDown: IKeyboardEvent | null = null;197198this._register(this._textArea.onKeyDown((_e) => {199const e = new StandardKeyboardEvent(_e);200if (e.keyCode === KeyCode.KEY_IN_COMPOSITION201|| (this._currentComposition && e.keyCode === KeyCode.Backspace)) {202// Stop propagation for keyDown events if the IME is processing key input203e.stopPropagation();204}205206if (e.equals(KeyCode.Escape)) {207// Prevent default always for `Esc`, otherwise it will generate a keypress208// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx209e.preventDefault();210}211212lastKeyDown = e;213this._onKeyDown.fire(e);214}));215216this._register(this._textArea.onKeyUp((_e) => {217const e = new StandardKeyboardEvent(_e);218this._onKeyUp.fire(e);219}));220221this._register(this._textArea.onCompositionStart((e) => {222if (_debugComposition) {223console.log(`[compositionstart]`, e);224}225226const currentComposition = new CompositionContext();227if (this._currentComposition) {228// simply reset the composition context229this._currentComposition = currentComposition;230return;231}232this._currentComposition = currentComposition;233234if (235this._OS === OperatingSystem.Macintosh236&& lastKeyDown237&& lastKeyDown.equals(KeyCode.KEY_IN_COMPOSITION)238&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd239&& this._textAreaState.selectionStart > 0240&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data241&& (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft')242) {243// Handling long press case on Chromium/Safari macOS + arrow key => pretend the character was selected244if (_debugComposition) {245console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e);246}247// Pretend the previous character was composed (in order to get it removed by subsequent compositionupdate events)248currentComposition.handleCompositionUpdate('x');249this._onCompositionStart.fire({ data: e.data });250return;251}252253if (this._browser.isAndroid) {254// when tapping on the editor, Android enters composition mode to edit the current word255// so we cannot clear the textarea on Android and we must pretend the current word was selected256this._onCompositionStart.fire({ data: e.data });257return;258}259260this._onCompositionStart.fire({ data: e.data });261}));262263this._register(this._textArea.onCompositionUpdate((e) => {264if (_debugComposition) {265console.log(`[compositionupdate]`, e);266}267const currentComposition = this._currentComposition;268if (!currentComposition) {269// should not be possible to receive a 'compositionupdate' without a 'compositionstart'270return;271}272if (this._browser.isAndroid) {273// On Android, the data sent with the composition update event is unusable.274// For example, if the cursor is in the middle of a word like Mic|osoft275// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".276// This is not really usable because it doesn't tell us where the edit began and where it ended.277const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);278const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);279this._textAreaState = newState;280this._onType.fire(typeInput);281this._onCompositionUpdate.fire(e);282return;283}284const typeInput = currentComposition.handleCompositionUpdate(e.data);285this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);286this._onType.fire(typeInput);287this._onCompositionUpdate.fire(e);288}));289290this._register(this._textArea.onCompositionEnd((e) => {291if (_debugComposition) {292console.log(`[compositionend]`, e);293}294const currentComposition = this._currentComposition;295if (!currentComposition) {296// https://github.com/microsoft/monaco-editor/issues/1663297// On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data298return;299}300this._currentComposition = null;301302if (this._browser.isAndroid) {303// On Android, the data sent with the composition update event is unusable.304// For example, if the cursor is in the middle of a word like Mic|osoft305// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".306// This is not really usable because it doesn't tell us where the edit began and where it ended.307const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);308const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);309this._textAreaState = newState;310this._onType.fire(typeInput);311this._onCompositionEnd.fire();312return;313}314315const typeInput = currentComposition.handleCompositionUpdate(e.data);316this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);317this._onType.fire(typeInput);318this._onCompositionEnd.fire();319}));320321this._register(this._textArea.onInput((e) => {322if (_debugComposition) {323console.log(`[input]`, e);324}325326// Pretend here we touched the text area, as the `input` event will most likely327// result in a `selectionchange` event which we want to ignore328this._textArea.setIgnoreSelectionChangeTime('received input event');329330if (this._currentComposition) {331return;332}333334const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);335const typeInput = TextAreaState.deduceInput(this._textAreaState, newState, /*couldBeEmojiInput*/this._OS === OperatingSystem.Macintosh);336337if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1) {338// one character was typed339if (340strings.isHighSurrogate(typeInput.text.charCodeAt(0))341|| typeInput.text.charCodeAt(0) === 0x7f /* Delete */342) {343// Ignore invalid input but keep it around for next time344return;345}346}347348this._textAreaState = newState;349if (350typeInput.text !== ''351|| typeInput.replacePrevCharCnt !== 0352|| typeInput.replaceNextCharCnt !== 0353|| typeInput.positionDelta !== 0354) {355// https://w3c.github.io/input-events/#interface-InputEvent-Attributes356if (e.inputType === 'insertFromPaste') {357this._onPaste.fire({358text: typeInput.text,359metadata: InMemoryClipboardMetadataManager.INSTANCE.get(typeInput.text)360});361} else {362this._onType.fire(typeInput);363}364}365}));366367// --- Clipboard operations368369this._register(this._textArea.onCut((e) => {370this._logService.trace(`TextAreaInput#onCut`, e);371372// Fire onWillCut event to allow interception373const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._host.context, this._logService, this._browser.isFirefox);374this._onWillCut.fire(cutEvent);375if (cutEvent.isHandled) {376// Event was handled externally, skip default processing377return;378}379380// Pretend here we touched the text area, as the `cut` event will most likely381// result in a `selectionchange` event which we want to ignore382this._textArea.setIgnoreSelectionChangeTime('received cut event');383384cutEvent.ensureClipboardGetsEditorData();385this._asyncTriggerCut.schedule();386}));387388this._register(this._textArea.onCopy((e) => {389this._logService.trace(`TextAreaInput#onCopy`, e);390391// !!!!!392// This is a workaround for what we think is an Electron bug where393// execCommand('copy') does not always work (it does not fire a clipboard event)394// !!!!!395// We signal that we have executed a copy command396CopyOptions.electronBugWorkaroundCopyEventHasFired = true;397398// Fire onWillCopy event to allow interception399const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._host.context, this._logService, this._browser.isFirefox);400this._onWillCopy.fire(copyEvent);401if (copyEvent.isHandled) {402// Event was handled externally, skip default processing403return;404}405406copyEvent.ensureClipboardGetsEditorData();407}));408409this._register(this._textArea.onPaste((e) => {410this._logService.trace(`TextAreaInput#onPaste`, e);411412// Fire onWillPaste event to allow interception413const pasteEvent = createClipboardPasteEvent(e);414this._onWillPaste.fire(pasteEvent);415if (pasteEvent.isHandled) {416// Event was handled externally, skip default processing417return;418}419420// Pretend here we touched the text area, as the `paste` event will most likely421// result in a `selectionchange` event which we want to ignore422this._textArea.setIgnoreSelectionChangeTime('received paste event');423424e.preventDefault();425426this._logService.trace(`TextAreaInput#onPaste with id : `, pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length);427if (!pasteEvent.text) {428return;429}430431this._logService.trace(`TextAreaInput#onPaste (before onPaste)`);432this._onPaste.fire({433text: pasteEvent.text,434metadata: pasteEvent.metadata435});436}));437438this._register(this._textArea.onFocus(() => {439const hadFocus = this._hasFocus;440441this._setHasFocus(true);442443if (this._accessibilityService.isScreenReaderOptimized() && this._browser.isSafari && !hadFocus && this._hasFocus) {444// When "tabbing into" the textarea, immediately after dispatching the 'focus' event,445// Safari will always move the selection at offset 0 in the textarea446if (!this._asyncFocusGainWriteScreenReaderContent.value) {447this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);448}449this._asyncFocusGainWriteScreenReaderContent.value.schedule();450}451}));452this._register(this._textArea.onBlur(() => {453if (this._currentComposition) {454// See https://github.com/microsoft/vscode/issues/112621455// where compositionend is not triggered when the editor456// is taken off-dom during a composition457458// Clear the flag to be able to write to the textarea459this._currentComposition = null;460461// Clear the textarea to avoid an unwanted cursor type462this.writeNativeTextAreaContent('blurWithoutCompositionEnd');463464// Fire artificial composition end465this._onCompositionEnd.fire();466}467this._setHasFocus(false);468}));469this._register(this._textArea.onSyntheticTap(() => {470if (this._browser.isAndroid && this._currentComposition) {471// on Android, tapping does not cancel the current composition, so the472// textarea is stuck showing the old composition473474// Clear the flag to be able to write to the textarea475this._currentComposition = null;476477// Clear the textarea to avoid an unwanted cursor type478this.writeNativeTextAreaContent('tapWithoutCompositionEnd');479480// Fire artificial composition end481this._onCompositionEnd.fire();482}483}));484}485486_initializeFromTest(): void {487this._hasFocus = true;488this._textAreaState = TextAreaState.readFromTextArea(this._textArea, null);489}490491private _installSelectionChangeListener(): IDisposable {492// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256493// When using a Braille display, it is possible for users to reposition the494// system caret. This is reflected in Chrome as a `selectionchange` event.495//496// The `selectionchange` event appears to be emitted under numerous other circumstances,497// so it is quite a challenge to distinguish a `selectionchange` coming in from a user498// using a Braille display from all the other cases.499//500// The problems with the `selectionchange` event are:501// * the event is emitted when the textarea is focused programmatically -- textarea.focus()502// * the event is emitted when the selection is changed in the textarea programmatically -- textarea.setSelectionRange(...)503// * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'504// * the event is emitted when tabbing into the textarea505// * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)506// * the event sometimes comes in bursts for a single logical textarea operation507508// `selectionchange` events often come multiple times for a single logical change509// so throttle multiple `selectionchange` events that burst in a short period of time.510let previousSelectionChangeEventTime = 0;511return dom.addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {//todo512inputLatency.onSelectionChange();513514if (!this._hasFocus) {515return;516}517if (this._currentComposition) {518return;519}520if (!this._browser.isChrome) {521// Support only for Chrome until testing happens on other browsers522return;523}524525const now = Date.now();526527const delta1 = now - previousSelectionChangeEventTime;528previousSelectionChangeEventTime = now;529if (delta1 < 5) {530// received another `selectionchange` event within 5ms of the previous `selectionchange` event531// => ignore it532return;533}534535const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();536this._textArea.resetSelectionChangeTime();537if (delta2 < 100) {538// received a `selectionchange` event within 100ms since we touched the textarea539// => ignore it, since we caused it540return;541}542543if (!this._textAreaState.selection) {544// Cannot correlate a position in the textarea with a position in the editor...545return;546}547548const newValue = this._textArea.getValue();549if (this._textAreaState.value !== newValue) {550// Cannot correlate a position in the textarea with a position in the editor...551return;552}553554const newSelectionStart = this._textArea.getSelectionStart();555const newSelectionEnd = this._textArea.getSelectionEnd();556if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {557// Nothing to do...558return;559}560561const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);562const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0]!, _newSelectionStartPosition[1], _newSelectionStartPosition[2]);563564const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);565const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0]!, _newSelectionEndPosition[1], _newSelectionEndPosition[2]);566567const newSelection = new Selection(568newSelectionStartPosition.lineNumber, newSelectionStartPosition.column,569newSelectionEndPosition.lineNumber, newSelectionEndPosition.column570);571572this._onSelectionChangeRequest.fire(newSelection);573});574}575576public override dispose(): void {577super.dispose();578if (this._selectionChangeListener) {579this._selectionChangeListener.dispose();580this._selectionChangeListener = null;581}582}583584public focusTextArea(): void {585// Setting this._hasFocus and writing the screen reader content586// will result in a focus() and setSelectionRange() in the textarea587this._setHasFocus(true);588589// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus590this.refreshFocusState();591}592593public isFocused(): boolean {594return this._hasFocus;595}596597public refreshFocusState(): void {598this._setHasFocus(this._textArea.hasFocus());599}600601private _setHasFocus(newHasFocus: boolean): void {602if (this._hasFocus === newHasFocus) {603// no change604return;605}606this._hasFocus = newHasFocus;607608if (this._selectionChangeListener) {609this._selectionChangeListener.dispose();610this._selectionChangeListener = null;611}612if (this._hasFocus) {613this._selectionChangeListener = this._installSelectionChangeListener();614}615616if (this._hasFocus) {617this.writeNativeTextAreaContent('focusgain');618}619620if (this._hasFocus) {621this._onFocus.fire();622} else {623this._onBlur.fire();624}625}626627private _setAndWriteTextAreaState(reason: string, textAreaState: TextAreaState): void {628if (!this._hasFocus) {629textAreaState = textAreaState.collapseSelection();630}631if (!textAreaState.isWrittenToTextArea(this._textArea, this._hasFocus)) {632this._logService.trace(`writeTextAreaState(reason: ${reason})`);633}634textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);635this._textAreaState = textAreaState;636}637638public writeNativeTextAreaContent(reason: string): void {639if ((!this._accessibilityService.isScreenReaderOptimized() && reason === 'render') || this._currentComposition) {640// Do not write to the text on render unless a screen reader is being used #192278641// Do not write to the text area when doing composition642return;643}644this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent());645}646}647648export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrapper {649650public readonly onKeyDown: Event<KeyboardEvent>;651public readonly onKeyPress: Event<KeyboardEvent>;652public readonly onKeyUp: Event<KeyboardEvent>;653public readonly onCompositionStart: Event<CompositionEvent>;654public readonly onCompositionUpdate: Event<CompositionEvent>;655public readonly onCompositionEnd: Event<CompositionEvent>;656public readonly onBeforeInput: Event<InputEvent>;657public readonly onInput: Event<InputEvent>;658public readonly onCut: Event<ClipboardEvent>;659public readonly onCopy: Event<ClipboardEvent>;660public readonly onPaste: Event<ClipboardEvent>;661public readonly onFocus: Event<FocusEvent>;662public readonly onBlur: Event<FocusEvent>; // = this._register(new DomEmitter(this._actual, 'blur')).event;663664public get ownerDocument(): Document {665return this._actual.ownerDocument;666}667668private _onSyntheticTap = this._register(new Emitter<void>());669public readonly onSyntheticTap: Event<void> = this._onSyntheticTap.event;670671private _ignoreSelectionChangeTime: number;672673constructor(674private readonly _actual: HTMLTextAreaElement675) {676super();677this._ignoreSelectionChangeTime = 0;678this.onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event;679this.onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event;680this.onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event;681this.onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event;682this.onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event;683this.onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event;684this.onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event;685this.onInput = <Event<InputEvent>>this._register(new DomEmitter(this._actual, 'input')).event;686this.onCut = this._register(new DomEmitter(this._actual, 'cut')).event;687this.onCopy = this._register(new DomEmitter(this._actual, 'copy')).event;688this.onPaste = this._register(new DomEmitter(this._actual, 'paste')).event;689this.onFocus = this._register(new DomEmitter(this._actual, 'focus')).event;690this.onBlur = this._register(new DomEmitter(this._actual, 'blur')).event;691692this._register(this.onKeyDown(() => inputLatency.onKeyDown()));693this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));694this._register(this.onInput(() => inputLatency.onInput()));695this._register(this.onKeyUp(() => inputLatency.onKeyUp()));696this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));697}698699public hasFocus(): boolean {700const shadowRoot = dom.getShadowRoot(this._actual);701if (shadowRoot) {702return shadowRoot.activeElement === this._actual;703} else if (this._actual.isConnected) {704return dom.getActiveElement() === this._actual;705} else {706return false;707}708}709710public setIgnoreSelectionChangeTime(reason: string): void {711this._ignoreSelectionChangeTime = Date.now();712}713714public getIgnoreSelectionChangeTime(): number {715return this._ignoreSelectionChangeTime;716}717718public resetSelectionChangeTime(): void {719this._ignoreSelectionChangeTime = 0;720}721722public getValue(): string {723// console.log('current value: ' + this._textArea.value);724return this._actual.value;725}726727public setValue(reason: string, value: string): void {728const textArea = this._actual;729if (textArea.value === value) {730// No change731return;732}733// console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);734this.setIgnoreSelectionChangeTime('setValue');735textArea.value = value;736}737738public getSelectionStart(): number {739return this._actual.selectionDirection === 'backward' ? this._actual.selectionEnd : this._actual.selectionStart;740}741742public getSelectionEnd(): number {743return this._actual.selectionDirection === 'backward' ? this._actual.selectionStart : this._actual.selectionEnd;744}745746public setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void {747const textArea = this._actual;748749let activeElement: Element | null = null;750const shadowRoot = dom.getShadowRoot(textArea);751if (shadowRoot) {752activeElement = shadowRoot.activeElement;753} else {754activeElement = dom.getActiveElement();755}756const activeWindow = dom.getWindow(activeElement);757758const currentIsFocused = (activeElement === textArea);759const currentSelectionStart = textArea.selectionStart;760const currentSelectionEnd = textArea.selectionEnd;761762if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {763// No change764// Firefox iframe bug https://github.com/microsoft/monaco-editor/issues/643#issuecomment-367871377765if (browser.isFirefox && activeWindow.parent !== activeWindow) {766textArea.focus();767}768return;769}770771// console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);772773if (currentIsFocused) {774// No need to focus, only need to change the selection range775this.setIgnoreSelectionChangeTime('setSelectionRange');776textArea.setSelectionRange(selectionStart, selectionEnd);777if (browser.isFirefox && activeWindow.parent !== activeWindow) {778textArea.focus();779}780return;781}782783// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.784// Here, we try to undo the browser's desperate reveal.785try {786const scrollState = dom.saveParentsScrollTop(textArea);787this.setIgnoreSelectionChangeTime('setSelectionRange');788textArea.focus();789textArea.setSelectionRange(selectionStart, selectionEnd);790dom.restoreParentsScrollTop(textArea, scrollState);791} catch (e) {792// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)793}794}795}796797798