Skip to content

Commit

Permalink
Handle the case where typing occurs at offset 0 after a focus gain (f…
Browse files Browse the repository at this point in the history
…ixes #42251)
  • Loading branch information
alexdima committed Apr 4, 2018
1 parent 6289e70 commit 7335b0c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 44 deletions.
51 changes: 44 additions & 7 deletions src/vs/editor/browser/controller/textAreaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ export interface ITextAreaInputHost {
deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position;
}

const enum TextAreaInputEventType {
none,
compositionstart,
compositionupdate,
compositionend,
input,
cut,
copy,
paste,
focus,
blur
}

/**
* Writes screen reader content to the textarea and is able to analyze its input events to generate:
* - onCut
Expand Down Expand Up @@ -89,6 +102,7 @@ export class TextAreaInput extends Disposable {

private readonly _host: ITextAreaInputHost;
private readonly _textArea: TextAreaWrapper;
private _lastTextAreaEvent: TextAreaInputEventType;
private readonly _asyncTriggerCut: RunOnceScheduler;

private _textAreaState: TextAreaState;
Expand All @@ -101,6 +115,7 @@ export class TextAreaInput extends Disposable {
super();
this._host = host;
this._textArea = this._register(new TextAreaWrapper(textArea));
this._lastTextAreaEvent = TextAreaInputEventType.none;
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));

this._textAreaState = TextAreaState.EMPTY;
Expand Down Expand Up @@ -129,6 +144,8 @@ export class TextAreaInput extends Disposable {
}));

this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionstart;

if (this._isDoingComposition) {
return;
}
Expand All @@ -145,10 +162,10 @@ export class TextAreaInput extends Disposable {
/**
* Deduce the typed input from a text area's value and the last observed state.
*/
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean): [TextAreaState, ITypeData] => {
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)];
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput, couldBeTypingAtOffset0)];
};

/**
Expand Down Expand Up @@ -185,6 +202,8 @@ export class TextAreaInput extends Disposable {
};

this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionupdate;

if (browser.isChromev56) {
// See https://github.com/Microsoft/monaco-editor/issues/320
// where compositionupdate .data is broken in Chrome v55 and v56
Expand All @@ -195,7 +214,7 @@ export class TextAreaInput extends Disposable {
}

if (compositionDataInValid(e.locale)) {
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
Expand All @@ -209,9 +228,11 @@ export class TextAreaInput extends Disposable {
}));

this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionend;

if (compositionDataInValid(e.locale)) {
// https://github.com/Microsoft/monaco-editor/issues/339
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
} else {
Expand All @@ -235,6 +256,10 @@ export class TextAreaInput extends Disposable {
}));

this._register(dom.addDisposableListener(textArea.domNode, 'input', () => {
// We want to find out if this is the first `input` after a `focus`.
const previousEventWasFocus = (this._lastTextAreaEvent === TextAreaInputEventType.focus);
this._lastTextAreaEvent = TextAreaInputEventType.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');
Expand All @@ -254,7 +279,7 @@ export class TextAreaInput extends Disposable {
return;
}

const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh);
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh, /*couldBeTypingAtOffset0*/previousEventWasFocus && 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;
Expand All @@ -279,6 +304,8 @@ export class TextAreaInput extends Disposable {
// --- Clipboard operations

this._register(dom.addDisposableListener(textArea.domNode, 'cut', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.cut;

// 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');
Expand All @@ -288,10 +315,14 @@ export class TextAreaInput extends Disposable {
}));

this._register(dom.addDisposableListener(textArea.domNode, 'copy', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.copy;

this._ensureClipboardGetsEditorSelection(e);
}));

this._register(dom.addDisposableListener(textArea.domNode, 'paste', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.paste;

// 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');
Expand All @@ -312,8 +343,14 @@ export class TextAreaInput extends Disposable {
}
}));

this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => this._setHasFocus(true)));
this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => this._setHasFocus(false)));
this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => {
this._lastTextAreaEvent = TextAreaInputEventType.focus;
this._setHasFocus(true);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => {
this._lastTextAreaEvent = TextAreaInputEventType.blur;
this._setHasFocus(false);
}));


// See https://github.com/Microsoft/vscode/issues/27216
Expand Down
16 changes: 14 additions & 2 deletions src/vs/editor/browser/controller/textAreaState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class TextAreaState {
}

public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void {
// console.log(Date.now() + ': applyToTextArea ' + reason + ': ' + this.toString());
// console.log(Date.now() + ': writeToTextArea ' + reason + ': ' + this.toString());
textArea.setValue(reason, this.value);
if (select) {
textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);
Expand Down Expand Up @@ -97,7 +97,7 @@ export class TextAreaState {
return new TextAreaState(text, 0, text.length, null, null);
}

public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {
public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean): ITypeData {
if (!previousState) {
// This is the EMPTY state
return {
Expand All @@ -117,6 +117,18 @@ export class TextAreaState {
let currentSelectionStart = currentState.selectionStart;
let currentSelectionEnd = currentState.selectionEnd;

if (couldBeTypingAtOffset0 && previousValue.length > 0 && previousSelectionStart === previousSelectionEnd && currentSelectionStart === currentSelectionEnd) {
// See https://github.com/Microsoft/vscode/issues/42251
// where typing always happens at offset 0 in the textarea
// when using a custom title area in OSX and moving the window
if (strings.endsWith(currentValue, previousValue)) {
// Looks like something was typed at offset 0
// ==> pretend we placed the cursor at offset 0 to begin with...
previousSelectionStart = 0;
previousSelectionEnd = 0;
}
}

// Strip the previous suffix from the value (without interfering with the current selection)
const previousSuffix = previousValue.substring(previousSelectionEnd);
const currentSuffix = currentValue.substring(currentSelectionEnd);
Expand Down

0 comments on commit 7335b0c

Please sign in to comment.