Repository: microsoft/vscode (TypeScript) — Bug Fix

Issue:
Quotes do not overtype when using US Intl PC keyboard layout
Extracted from discussion re #144590 with @lszomoru 

* on macOS
* switch to the "US International - PC" keyboard layout
* open a TypeScript file with the content:
```ts
const x = 
```
* press <kbd>'</kbd>, <kbd>'</kbd>, <kbd>;</kbd>
* you end up with:
```ts
const x = '';'
```
* instead of:
```ts
const x = '';
```


https://user-images.githubusercontent.com/5047891/157277270-9979cb5f-7e5d-4e16-ab95-36a52b5971ac.mp4



fyi @hediet


Discussion:

Resolving patch:
diff --git a/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts
--- a/src/vs/editor/common/cursor/cursor.ts
+++ b/src/vs/editor/common/cursor/cursor.ts
@@ -35,8 +35,7 @@ export class CursorsController extends Disposable {
 
 	private _hasFocus: boolean;
 	private _isHandling: boolean;
-	private _isDoingComposition: boolean;
-	private _selectionsWhenCompositionStarted: Selection[] | null;
+	private _compositionState: CompositionState | null;
 	private _columnSelectData: IColumnSelectData | null;
 	private _autoClosedActions: AutoClosedAction[];
 	private _prevEditOperationType: EditOperationType;
@@ -52,8 +51,7 @@ export class CursorsController extends Disposable {
 
 		this._hasFocus = false;
 		this._isHandling = false;
-		this._isDoingComposition = false;
-		this._selectionsWhenCompositionStarted = null;
+		this._compositionState = null;
 		this._columnSelectData = null;
 		this._autoClosedActions = [];
 		this._prevEditOperationType = EditOperationType.Other;
@@ -525,24 +523,22 @@ export class CursorsController extends Disposable {
 		}
 	}
 
-	public setIsDoingComposition(isDoingComposition: boolean): void {
-		this._isDoingComposition = isDoingComposition;
-	}
-
 	public getAutoClosedCharacters(): Range[] {
 		return AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
 	}
 
 	public startComposition(eventsCollector: ViewModelEventsCollector): void {
-		this._selectionsWhenCompositionStarted = this.getSelections().slice(0);
+		this._compositionState = new CompositionState(this._model, this.getSelection());
 	}
 
 	public endComposition(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
+		const compositionInsertText = this._compositionState ? this._compositionState.deduceInsertedText(this._model, this.getSelection()) : null;
+		this._compositionState = null;
+
 		this._executeEdit(() => {
 			if (source === 'keyboard') {
 				// composition finishes, let's check if we need to auto complete if necessary.
-				this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this._selectionsWhenCompositionStarted, this.getSelections(), this.getAutoClosedCharacters()));
-				this._selectionsWhenCompositionStarted = null;
+				this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, compositionInsertText, this.getSelections(), this.getAutoClosedCharacters()));
 			}
 		}, eventsCollector, source);
 	}
@@ -559,7 +555,7 @@ export class CursorsController extends Disposable {
 					const chr = text.substr(offset, charLength);
 
 					// Here we must interpret each typed character individually
-					this._executeEditOperation(TypeOperations.typeWithInterceptors(this._isDoingComposition, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr));
+					this._executeEditOperation(TypeOperations.typeWithInterceptors(!!this._compositionState, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr));
 
 					offset += charLength;
 				}
@@ -1013,3 +1009,53 @@ class CommandExecutor {
 		return loserCursorsMap;
 	}
 }
+
+class CompositionLineState {
+	constructor(
+		public readonly text: string,
+		public readonly pos: number
+	) { }
+}
+
+class CompositionState {
+
+	private readonly _original: CompositionLineState | null;
+
+	private static _capture(textModel: ITextModel, selection: Selection): CompositionLineState | null {
+		if (!selection.isEmpty()) {
+			return null;
+		}
+		return new CompositionLineState(
+			textModel.getLineContent(selection.startLineNumber),
+			selection.startColumn - 1
+		);
+	}
+
+	constructor(textModel: ITextModel, selection: Selection) {
+		this._original = CompositionState._capture(textModel, selection);
+	}
+
+	/**
+	 * Returns the inserted text during this composition.
+	 * If the composition resulted in existing text being changed (i.e. not a pure insertion) it returns null.
+	 */
+	deduceInsertedText(textModel: ITextModel, selection: Selection): string | null {
+		if (!this._original) {
+			return null;
+		}
+		const current = CompositionState._capture(textModel, selection);
+		if (!current) {
+			return null;
+		}
+		const insertedTextLength = current.text.length - this._original.text.length;
+		if (insertedTextLength < 0) {
+			return null;
+		}
+		const originalPrefix = this._original.text.substring(0, this._original.pos);
+		const originalSuffix = this._original.text.substring(this._original.pos);
+		if (!current.text.startsWith(originalPrefix) || !current.text.endsWith(originalSuffix)) {
+			return null;
+		}
+		return current.text.substring(this._original.pos, this._original.pos + insertedTextLength);
+	}
+}
diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts
--- a/src/vs/editor/common/cursor/cursorTypeOperations.ts
+++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts
@@ -821,30 +821,13 @@ export class TypeOperations {
 	/**
 	 * This is very similar with typing, but the character is already in the text buffer!
 	 */
-	public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selectionsWhenCompositionStarted: Selection[] | null, selections: Selection[], autoClosedCharacters: Range[]): EditOperationResult | null {
-		if (!selectionsWhenCompositionStarted || Selection.selectionsArrEqual(selectionsWhenCompositionStarted, selections)) {
-			// no content was typed
+	public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, compositionInsertText: string | null, selections: Selection[], autoClosedCharacters: Range[]): EditOperationResult | null {
+		if (!compositionInsertText || compositionInsertText.length === 0) {
+			// no content was typed or composition was not a pure insertion
 			return null;
 		}
 
-		let ch: string | null = null;
-		// extract last typed character
-		for (const selection of selections) {
-			if (!selection.isEmpty()) {
-				return null;
-			}
-			const position = selection.getPosition();
-			const currentChar = model.getValueInRange(new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column));
-			if (ch === null) {
-				ch = currentChar;
-			} else if (ch !== currentChar) {
-				return null;
-			}
-		}
-
-		if (!ch) {
-			return null;
-		}
+		const ch = compositionInsertText.charAt(compositionInsertText.length - 1);
 
 		if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) {
 			// Unfortunately, the close character is at this point "doubled", so we need to delete it...
@@ -894,7 +877,7 @@ export class TypeOperations {
 			}
 		}
 
-		if (!isDoingComposition && this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) {
+		if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) {
 			return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch);
 		}
 
diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts
--- a/src/vs/editor/common/viewModel/viewModelImpl.ts
+++ b/src/vs/editor/common/viewModel/viewModelImpl.ts
@@ -991,11 +991,9 @@ export class ViewModel extends Disposable implements IViewModel {
 		this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer));
 	}
 	public startComposition(): void {
-		this._cursor.setIsDoingComposition(true);
 		this._executeCursorEdit(eventsCollector => this._cursor.startComposition(eventsCollector));
 	}
 	public endComposition(source?: string | null | undefined): void {
-		this._cursor.setIsDoingComposition(false);
 		this._executeCursorEdit(eventsCollector => this._cursor.endComposition(eventsCollector, source));
 	}
 	public type(text: string, source?: string | null | undefined): void {


Test patch:
diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts
--- a/src/vs/editor/test/browser/controller/cursor.test.ts
+++ b/src/vs/editor/test/browser/controller/cursor.test.ts
@@ -6053,6 +6053,34 @@ suite('autoClosingPairs', () => {
 		mode.dispose();
 	});
 
+	test('issue #144690: Quotes do not overtype when using US Intl PC keyboard layout', () => {
+		const mode = new AutoClosingMode();
+		usingCursor({
+			text: [
+				''
+			],
+			languageId: mode.languageId
+		}, (editor, model, viewModel) => {
+			assertCursor(viewModel, new Position(1, 1));
+
+			// Pressing ' + ' + ;
+
+			viewModel.startComposition();
+			viewModel.type(`'`, 'keyboard');
+			viewModel.compositionType(`'`, 1, 0, 0, 'keyboard');
+			viewModel.compositionType(`'`, 1, 0, 0, 'keyboard');
+			viewModel.endComposition('keyboard');
+			viewModel.startComposition();
+			viewModel.type(`'`, 'keyboard');
+			viewModel.compositionType(`';`, 1, 0, 0, 'keyboard');
+			viewModel.compositionType(`';`, 2, 0, 0, 'keyboard');
+			viewModel.endComposition('keyboard');
+
+			assert.strictEqual(model.getValue(), `'';`);
+		});
+		mode.dispose();
+	});
+
 	test('issue #82701: auto close does not execute when IME is canceled via backspace', () => {
 		let mode = new AutoClosingMode();
 		usingCursor({
diff --git a/src/vs/editor/test/browser/controller/imeRecorder.ts b/src/vs/editor/test/browser/controller/imeRecorder.ts
--- a/src/vs/editor/test/browser/controller/imeRecorder.ts
+++ b/src/vs/editor/test/browser/controller/imeRecorder.ts
@@ -68,18 +68,21 @@ import * as platform from 'vs/base/common/platform';
 
 		return lines.join('\n');
 
+		function printString(str: string) {
+			return str.replace(/\\/g, '\\\\').replace(/'/g, '\\\'');
+		}
 		function printState(state: IRecordedTextareaState) {
-			return `{ value: '${state.value}', selectionStart: ${state.selectionStart}, selectionEnd: ${state.selectionEnd}, selectionDirection: '${state.selectionDirection}' }`;
+			return `{ value: '${printString(state.value)}', selectionStart: ${state.selectionStart}, selectionEnd: ${state.selectionEnd}, selectionDirection: '${state.selectionDirection}' }`;
 		}
 		function printEvent(ev: IRecordedEvent) {
 			if (ev.type === 'keydown' || ev.type === 'keypress' || ev.type === 'keyup') {
 				return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', altKey: ${ev.altKey}, charCode: ${ev.charCode}, code: '${ev.code}', ctrlKey: ${ev.ctrlKey}, isComposing: ${ev.isComposing}, key: '${ev.key}', keyCode: ${ev.keyCode}, location: ${ev.location}, metaKey: ${ev.metaKey}, repeat: ${ev.repeat}, shiftKey: ${ev.shiftKey} }`;
 			}
 			if (ev.type === 'compositionstart' || ev.type === 'compositionupdate' || ev.type === 'compositionend') {
-				return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: '${ev.data}' }`;
+				return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: '${printString(ev.data)}' }`;
 			}
 			if (ev.type === 'beforeinput' || ev.type === 'input') {
-				return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: ${ev.data === null ? 'null' : `'${ev.data}'`}, inputType: '${ev.inputType}', isComposing: ${ev.isComposing} }`;
+				return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: ${ev.data === null ? 'null' : `'${printString(ev.data)}'`}, inputType: '${ev.inputType}', isComposing: ${ev.isComposing} }`;
 			}
 			return JSON.stringify(ev);
 		}
diff --git a/src/vs/editor/test/browser/controller/inputRecorder.html b/src/vs/editor/test/browser/controller/inputRecorder.html
deleted file mode 100644
--- a/src/vs/editor/test/browser/controller/inputRecorder.html
+++ /dev/null
@@ -1,115 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
-	<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
-</head>
-<body>
-
-<h2>Input Recorder</h2>
-<p>Records input events and logs them in the console</p>
-
-<button id="start">Start</button>
-<button id="stop">Stop</button><br/><br/>
-<textarea rows="20" cols="50" id="input"></textarea>
-
-<script>
-var interestingEvents = [
-	'keydown',
-	'keyup',
-	'keypress',
-	'compositionstart',
-	'compositionupdate',
-	'compositionend',
-	'input',
-	'cut',
-	'copy',
-	'paste',
-];
-
-var RECORDED_EVENTS = [];
-
-var input = document.getElementById('input');
-
-var blockedProperties = [
-	'currentTarget',
-	'path',
-	'srcElement',
-	'target',
-	'view'
-];
-blockedProperties = blockedProperties.concat([
-	'AT_TARGET',
-	'BLUR',
-	'BUBBLING_PHASE',
-	'CAPTURING_PHASE',
-	'CHANGE',
-	'CLICK',
-	'DBLCLICK',
-	'DOM_KEY_LOCATION_LEFT',
-	'DOM_KEY_LOCATION_NUMPAD',
-	'DOM_KEY_LOCATION_RIGHT',
-	'DOM_KEY_LOCATION_STANDARD',
-	'DRAGDROP',
-	'FOCUS',
-	'KEYDOWN',
-	'KEYPRESS',
-	'KEYUP',
-	'MOUSEDOWN',
-	'MOUSEDRAG',
-	'MOUSEMOVE',
-	'MOUSEOUT',
-	'MOUSEOVER',
-	'MOUSEUP',
-	'NONE',
-	'SELECT'
-])
-
-function toSerializable(e) {
-	var r = {};
-	for (var k in e) {
-		if (blockedProperties.indexOf(k) >= 0) {
-			continue;
-		}
-		if (typeof e[k] === 'function') {
-			continue;
-		}
-		r[k] = e[k];
-	}
-	console.log(r);
-	return r;
-}
-
-function recordEvent(eventType, e) {
-	RECORDED_EVENTS.push({
-		type: eventType,
-		textarea: {
-			value: input.value,
-			selectionStart: input.selectionStart,
-			selectionEnd: input.selectionEnd
-		},
-		event: toSerializable(e)
-	});
-}
-
-interestingEvents.forEach(function(eventType) {
-	input.addEventListener(eventType, function(e) {
-		recordEvent(eventType, e);
-
-	});
-});
-
-document.getElementById('start').onclick = function() {
-	input.value = 'some  text';
-	input.setSelectionRange(5, 5);
-	input.focus();
-	RECORDED_EVENTS = [];
-};
-
-document.getElementById('stop').onclick = function() {
-	console.log(JSON.stringify(RECORDED_EVENTS));
-};
-
-</script>
-</body>
-</html>
diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts
--- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts
+++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts
@@ -544,6 +544,60 @@ suite('TextAreaInput', () => {
 		assert.deepStrictEqual(actualResultingState, recorded.final);
 	});
 
+	test('macOS - Chrome - pressing quotes on US Intl', async () => {
+		// macOS, US International - PC, press ', ', ;
+		const recorded: IRecorded = {
+			env: { OS: OperatingSystem.Macintosh, browser: { isAndroid: false, isFirefox: false, isChrome: true, isSafari: false } },
+			initial: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' },
+			events: [
+				{ timeStamp: 0.00, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: false, key: 'Dead', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false },
+				{ timeStamp: 2.80, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'compositionstart', data: '' },
+				{ timeStamp: 3.10, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 3.20, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' },
+				{ timeStamp: 3.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 71.90, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 222, location: 0, metaKey: false, repeat: false, shiftKey: false },
+				{ timeStamp: 144.00, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false },
+				{ timeStamp: 146.20, state: { value: 'aa\'aa', selectionStart: 2, selectionEnd: 3, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 146.40, state: { value: 'aa\'aa', selectionStart: 2, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' },
+				{ timeStamp: 146.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 146.80, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionend', data: '\'' },
+				{ timeStamp: 147.20, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionstart', data: '' },
+				{ timeStamp: 147.20, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 147.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' },
+				{ timeStamp: 148.20, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 208.10, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 222, location: 0, metaKey: false, repeat: false, shiftKey: false },
+				{ timeStamp: 323.70, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Semicolon', ctrlKey: false, isComposing: true, key: ';', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false },
+				{ timeStamp: 325.70, state: { value: 'aa\'\'aa', selectionStart: 3, selectionEnd: 4, selectionDirection: 'none' }, type: 'beforeinput', data: '\';', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 325.80, state: { value: 'aa\'\'aa', selectionStart: 3, selectionEnd: 4, selectionDirection: 'none' }, type: 'compositionupdate', data: '\';' },
+				{ timeStamp: 326.30, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'input', data: '\';', inputType: 'insertCompositionText', isComposing: true },
+				{ timeStamp: 326.30, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'compositionend', data: '\';' },
+				{ timeStamp: 428.00, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Semicolon', ctrlKey: false, isComposing: false, key: ';', keyCode: 186, location: 0, metaKey: false, repeat: false, shiftKey: false }
+			],
+			final: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' },
+		};
+
+		const actualOutgoingEvents = await simulateInteraction(recorded);
+		assert.deepStrictEqual(actualOutgoingEvents, ([
+			{ type: "compositionStart", data: "" },
+			{ type: "type", text: "'", replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionUpdate", data: "'" },
+			{ type: "type", text: "'", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionUpdate", data: "'" },
+			{ type: "type", text: "'", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionEnd" },
+			{ type: "compositionStart", data: "" },
+			{ type: "type", text: "'", replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionUpdate", data: "'" },
+			{ type: "type", text: "';", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionUpdate", data: "';" },
+			{ type: "type", text: "';", replacePrevCharCnt: 2, replaceNextCharCnt: 0, positionDelta: 0 },
+			{ type: "compositionEnd" }
+		]));
+
+		const actualResultingState = interpretTypeEvents(recorded.env.OS, recorded.env.browser, recorded.initial, actualOutgoingEvents);
+		assert.deepStrictEqual(actualResultingState, recorded.final);
+	});
+
 	test('macOS - Chrome - inserting emoji using ctrl+cmd+space', async () => {
 		// macOS, English, press ctrl+cmd+space, and then pick an emoji using the mouse
 		// See https://github.com/microsoft/vscode/issues/4271