mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 13:46:13 +00:00
Fixes #4271: Try to detect the OSX emoji picker case
This commit is contained in:
parent
717f328ab7
commit
b14cce8b5d
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue