in src/vs/editor/browser/controller/textAreaInput.ts [158:380]
constructor(host: ITextAreaInputHost, private textArea: FastDomNode<HTMLTextAreaElement>) {
super();
this._host = host;
this._textArea = this._register(new TextAreaWrapper(textArea));
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
this._textAreaState = TextAreaState.EMPTY;
this._selectionChangeListener = null;
this.writeScreenReaderContent('ctor');
this._hasFocus = false;
this._isDoingComposition = false;
this._nextCommand = ReadFromTextArea.Type;
let lastKeyDown: IKeyboardEvent | null = null;
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keydown', (e: IKeyboardEvent) => {
if (e.keyCode === KeyCode.KEY_IN_COMPOSITION
|| (this._isDoingComposition && e.keyCode === KeyCode.Backspace)) {
// Stop propagation for keyDown events if the IME is processing key input
e.stopPropagation();
}
if (e.equals(KeyCode.Escape)) {
// Prevent default always for `Esc`, otherwise it will generate a keypress
// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
e.preventDefault();
}
lastKeyDown = e;
this._onKeyDown.fire(e);
}));
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keyup', (e: IKeyboardEvent) => {
this._onKeyUp.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => {
if (this._isDoingComposition) {
return;
}
this._isDoingComposition = true;
let moveOneCharacterLeft = false;
if (
platform.isMacintosh
&& lastKeyDown
&& lastKeyDown.equals(KeyCode.KEY_IN_COMPOSITION)
&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd
&& this._textAreaState.selectionStart > 0
&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data
) {
// Handling long press case on macOS + arrow key => pretend the character was selected
if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') {
moveOneCharacterLeft = true;
}
}
if (moveOneCharacterLeft) {
this._textAreaState = new TextAreaState(
this._textAreaState.value,
this._textAreaState.selectionStart - 1,
this._textAreaState.selectionEnd,
this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null,
this._textAreaState.selectionEndPosition
);
} else if (!browser.isEdge) {
// In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled.
this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY);
}
this._onCompositionStart.fire({ moveOneCharacterLeft });
}));
/**
* Deduce the typed input from a text area's value and the last observed state.
*/
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)];
};
/**
* Deduce the composition input from a string.
*/
const deduceComposition = (text: string): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.selectedText(text);
const typeInput: ITypeData = {
text: newState.value,
replaceCharCnt: oldState.selectionEnd - oldState.selectionStart
};
return [newState, typeInput];
};
const compositionDataInValid = (locale: string): boolean => {
// https://github.com/Microsoft/monaco-editor/issues/339
// Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue.
// The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME,
// which breaks this path of code.
if (browser.isEdge && locale === 'ja') {
return true;
}
return false;
};
this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => {
if (compositionDataInValid(e.locale)) {
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
return;
}
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => {
// https://github.com/microsoft/monaco-editor/issues/1663
// On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data
if (!this._isDoingComposition) {
return;
}
if (compositionDataInValid(e.locale)) {
// https://github.com/Microsoft/monaco-editor/issues/339
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
} else {
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
}
// Due to isEdgeOrIE (where the textarea was not cleared initially) and isChrome (the textarea is not updated correctly when composition ends)
// we cannot assume the text at the end consists only of the composited text
if (browser.isEdge || browser.isChrome) {
this._textAreaState = TextAreaState.readFromTextArea(this._textArea);
}
if (!this._isDoingComposition) {
return;
}
this._isDoingComposition = false;
this._onCompositionEnd.fire();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'input', () => {
// Pretend here we touched the text area, as the `input` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received input event');
if (this._isDoingComposition) {
return;
}
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh);
if (typeInput.replaceCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
// Ignore invalid input but keep it around for next time
return;
}
this._textAreaState = newState;
if (this._nextCommand === ReadFromTextArea.Type) {
if (typeInput.text !== '') {
this._onType.fire(typeInput);
}
} else {
if (typeInput.text !== '' || typeInput.replaceCharCnt !== 0) {
this._firePaste(typeInput.text, null);
}
this._nextCommand = ReadFromTextArea.Type;
}
}));
// --- Clipboard operations
this._register(dom.addDisposableListener(textArea.domNode, 'cut', (e: ClipboardEvent) => {
// Pretend here we touched the text area, as the `cut` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received cut event');
this._ensureClipboardGetsEditorSelection(e);
this._asyncTriggerCut.schedule();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'copy', (e: ClipboardEvent) => {
this._ensureClipboardGetsEditorSelection(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'paste', (e: ClipboardEvent) => {
// Pretend here we touched the text area, as the `paste` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received paste event');
if (ClipboardEventUtils.canUseTextData(e)) {
const [pastePlainText, metadata] = ClipboardEventUtils.getTextData(e);
if (pastePlainText !== '') {
this._firePaste(pastePlainText, metadata);
}
} else {
if (this._textArea.getSelectionStart() !== this._textArea.getSelectionEnd()) {
// Clean up the textarea, to get a clean paste
this._setAndWriteTextAreaState('paste', TextAreaState.EMPTY);
}
this._nextCommand = ReadFromTextArea.Paste;
}
}));
this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => {
this._setHasFocus(true);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => {
this._setHasFocus(false);
}));
}