Fixes #4271: Try to detect the OSX emoji picker case

This commit is contained in:
Alex Dima 2017-05-05 16:51:58 +02:00 committed by VS Code
parent 717f328ab7
commit b14cce8b5d
5 changed files with 146 additions and 9 deletions

View file

@ -503,6 +503,15 @@ export function containsRTL(str: string): boolean {
return CONTAINS_RTL.test(str);
}
/**
* Generated using https://github.com/alexandrudima/unicode-utils/blob/master/generate-emoji-test.js
*/
const CONTAINS_EMOJI = /(?:[\u231A\u231B\u23F0\u23F3\u2600-\u27BF\u2B50\u2B55]|\uD83C[\uDDE6-\uDDFF\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F\uDE80-\uDEF8]|\uD83E[\uDD00-\uDDE6])/;
export function containsEmoji(str: string): boolean {
return CONTAINS_EMOJI.test(str);
}
const IS_BASIC_ASCII = /^[\t\n\r\x20-\x7E]*$/;
/**
* Returns true if `str` contains only basic ASCII characters in the range 32 - 126 (including 32 and 126) or \n, \r, \t

View file

@ -231,6 +231,24 @@ suite('Strings', () => {
assert.equal(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true);
});
test('containsEmoji', () => {
assert.equal(strings.containsEmoji('a'), false);
assert.equal(strings.containsEmoji(''), false);
assert.equal(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false);
assert.equal(strings.containsEmoji('hello world!'), false);
assert.equal(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false);
assert.equal(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false);
assert.equal(strings.containsEmoji('a📚📚b'), true);
assert.equal(strings.containsEmoji('1F600 # 😀 grinning face'), true);
assert.equal(strings.containsEmoji('1F47E # 👾 alien monster'), true);
assert.equal(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true);
assert.equal(strings.containsEmoji('26EA # ⛪ church'), true);
assert.equal(strings.containsEmoji('231B # ⌛ hourglass'), true);
assert.equal(strings.containsEmoji('2702 # ✂ scissors'), true);
assert.equal(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true);
});
// test('containsRTL speed', () => {
// var SIZE = 1000000;
// var REPEAT = 10;

View file

@ -11,6 +11,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { ITypeData, TextAreaState, ITextAreaWrapper } from 'vs/editor/browser/controller/textAreaState';
import * as browser from 'vs/base/browser/browser';
import * as platform from 'vs/base/common/platform';
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent } from "vs/base/browser/keyboardEvent";
import { FastDomNode } from "vs/base/browser/fastDomNode";
@ -138,10 +139,10 @@ export class TextAreaInput extends Disposable {
/**
* Deduce the typed input from a text area's value and the last observed state.
*/
const deduceInputFromTextAreaValue = (): [TextAreaState, ITypeData] => {
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = this._textAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceInput(oldState, newState)];
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)];
};
/**
@ -172,7 +173,7 @@ export class TextAreaInput extends Disposable {
// 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.
const [newState, typeInput] = deduceInputFromTextAreaValue();
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
@ -188,7 +189,7 @@ export class TextAreaInput extends Disposable {
this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => {
if (browser.isEdgeOrIE && e.locale === 'ja') {
// https://github.com/Microsoft/monaco-editor/issues/339
const [newState, typeInput] = deduceInputFromTextAreaValue();
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
}
@ -228,7 +229,7 @@ export class TextAreaInput extends Disposable {
return;
}
const [newState, typeInput] = deduceInputFromTextAreaValue();
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;

View file

@ -81,7 +81,7 @@ export class TextAreaState {
return new TextAreaState(text, 0, text.length, 0);
}
public static deduceInput(previousState: TextAreaState, currentState: TextAreaState): ITypeData {
public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {
if (!previousState) {
// This is the EMPTY state
return {
@ -91,8 +91,8 @@ export class TextAreaState {
}
// console.log('------------------------deduceInput');
// console.log('CURRENT STATE: ' + currentState.toString());
// console.log('PREVIOUS STATE: ' + previousState.toString());
// console.log('CURRENT STATE: ' + currentState.toString());
let previousValue = previousState.value;
let previousSelectionStart = previousState.selectionStart;
@ -118,8 +118,48 @@ export class TextAreaState {
currentSelectionEnd -= prefixLength;
previousSelectionEnd -= prefixLength;
// console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
// console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd);
// console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) {
// on OSX, emojis from the emoji picker are inserted at random locations
// the only hints we can use is that the selection is immediately after the inserted emoji
// and that none of the old text has been deleted
let potentialEmojiInput: string = null;
if (currentSelectionStart === currentValue.length) {
// emoji potentially inserted "somewhere" after the previous selection => it should appear at the end of `currentValue`
if (strings.startsWith(currentValue, previousValue)) {
// only if all of the old text is accounted for
potentialEmojiInput = currentValue.substring(previousValue.length);
}
} else {
// emoji potentially inserted "somewhere" before the previous selection => it should appear at the start of `currentValue`
if (strings.endsWith(currentValue, previousValue)) {
// only if all of the old text is accounted for
potentialEmojiInput = currentValue.substring(0, currentValue.length - previousValue.length);
}
}
if (potentialEmojiInput !== null && potentialEmojiInput.length > 0) {
// now we check that this is indeed an emoji
// emojis can grow quite long, so a length check is of no help
// e.g. 1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 England
// Oftentimes, emojis use Variation Selector-16 (U+FE0F), so that is a good hint
// http://emojipedia.org/variation-selector-16/
// > An invisible codepoint which specifies that the preceding character
// > should be displayed with emoji presentation. Only required if the
// > preceding character defaults to text presentation.
if (/\uFE0F/.test(potentialEmojiInput) || strings.containsEmoji(potentialEmojiInput)) {
return {
text: potentialEmojiInput,
replaceCharCnt: 0
};
}
}
}
if (currentSelectionStart === currentSelectionEnd) {
// composition accept case (noticed in FF + Japanese)

View file

@ -122,7 +122,7 @@ suite('TextAreaState', () => {
textArea._selectionEnd = selectionEnd;
let newState = prevState.readFromTextArea(textArea);
let actual = TextAreaState.deduceInput(prevState, newState);
let actual = TextAreaState.deduceInput(prevState, newState, true);
assert.equal(actual.text, expected);
assert.equal(actual.replaceCharCnt, expectedCharReplaceCnt);
@ -420,6 +420,75 @@ suite('TextAreaState', () => {
);
});
test('issue #4271 (example 1) - When inserting an emoji on OSX, it is placed two spaces left of the cursor', () => {
// The OSX emoji inserter inserts emojis at random positions in the text, unrelated to where the cursor is.
testDeduceInput(
new TextAreaState(
[
'some1 text',
'some2 text',
'some3 text',
'some4 text', // cursor is here in the middle of the two spaces
'some5 text',
'some6 text',
'some7 text'
].join('\n'),
42, 42, 0
),
[
'so📅me1 text',
'some2 text',
'some3 text',
'some4 text',
'some5 text',
'some6 text',
'some7 text'
].join('\n'),
4, 4,
'📅', 0
);
});
test('issue #4271 (example 2) - When inserting an emoji on OSX, it is placed two spaces left of the cursor', () => {
// The OSX emoji inserter inserts emojis at random positions in the text, unrelated to where the cursor is.
testDeduceInput(
new TextAreaState(
'some1 text',
6, 6, 0
),
'some💊1 text',
6, 6,
'💊', 0
);
});
test('issue #4271 (example 3) - When inserting an emoji on OSX, it is placed two spaces left of the cursor', () => {
// The OSX emoji inserter inserts emojis at random positions in the text, unrelated to where the cursor is.
testDeduceInput(
new TextAreaState(
'qwertyu\nasdfghj\nzxcvbnm',
12, 12, 0
),
'qwertyu\nasdfghj\nzxcvbnm🎈',
25, 25,
'🎈', 0
);
});
// an example of an emoji missed by the regex but which has the FE0F variant 16 hint
test('issue #4271 (example 4) - When inserting an emoji on OSX, it is placed two spaces left of the cursor', () => {
// The OSX emoji inserter inserts emojis at random positions in the text, unrelated to where the cursor is.
testDeduceInput(
new TextAreaState(
'some1 text',
6, 6, 0
),
'some⌨1 text',
6, 6,
'⌨️', 0
);
});
function testFromEditorSelectionAndPreviousState(eol: string, lines: string[], range: Range, prevSelectionToken: number): TextAreaState {
let model = new SimpleModel(lines, eol);
let previousState = new TextAreaState('', 0, 0, prevSelectionToken);