Enable edit context (#237497)

* Revert "Revert Enablement of EditContext on Insiders (#235062)"

This reverts commit 45385e1c6f.

* adding product import

* adding som code

* removing console logs

* make sure editor selection is observed after keybinding dispatches

Co-authored-with: Aiday Mar <amarlenkyzy@microsoft.com>

* removing empty lines

---------

Co-authored-by: João Moreno <joao.moreno@microsoft.com>
This commit is contained in:
Aiday Marlen Kyzy
2025-01-13 09:47:32 +01:00
committed by GitHub
parent 964233731a
commit 3ff1dceedf
13 changed files with 118 additions and 38 deletions
+2 -2
View File
@@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap {
type EventHandler<TEvent extends Event = Event> = (event: TEvent) => void;
interface TextUpdateEvent extends Event {
new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent;
declare class TextUpdateEvent extends Event {
constructor(type: DOMString, options?: TextUpdateEventInit);
readonly updateRangeStart: number;
readonly updateRangeEnd: number;
+2 -1
View File
@@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js';
import * as nls from '../../../nls.js';
import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js';
import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js';
import product from '../../../platform/product/common/product.js';
//#region typed options
@@ -5814,7 +5815,7 @@ export const EditorOptions = {
emptySelectionClipboard: register(new EditorEmptySelectionClipboard()),
dropIntoEditor: register(new EditorDropIntoEditor()),
experimentalEditContextEnabled: register(new EditorBooleanOption(
EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false,
EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable',
{
description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."),
included: platform.isChrome || platform.isEdge || platform.isNative
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js';
import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { language, locale } from '../../../../base/common/platform.js';
@@ -133,18 +133,54 @@ export class BrowserWindowDriver implements IWindowDriver {
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
if (isHTMLDivElement(element)) {
// Edit context is enabled
const editContext = element.editContext;
if (!editContext) {
throw new Error(`Edit context not found: ${selector}`);
}
const selectionStart = editContext.selectionStart;
const selectionEnd = editContext.selectionEnd;
const event = new TextUpdateEvent('textupdate', {
updateRangeStart: selectionStart,
updateRangeEnd: selectionEnd,
text,
selectionStart: selectionStart + text.length,
selectionEnd: selectionStart + text.length,
compositionStart: 0,
compositionEnd: 0
});
editContext.dispatchEvent(event);
} else if (isHTMLTextAreaElement(element)) {
const start = element.selectionStart;
const newStart = start + text.length;
const value = element.value;
const newValue = value.substr(0, start) + text + value.substr(start);
const textarea = element as HTMLTextAreaElement;
const start = textarea.selectionStart;
const newStart = start + text.length;
const value = textarea.value;
const newValue = value.substr(0, start) + text + value.substr(start);
element.value = newValue;
element.setSelectionRange(newStart, newStart);
textarea.value = newValue;
textarea.setSelectionRange(newStart, newStart);
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
element.dispatchEvent(event);
}
}
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
async getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }> {
const element = mainWindow.document.querySelector(selector);
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
if (isHTMLDivElement(element)) {
const editContext = element.editContext;
if (!editContext) {
throw new Error(`Edit context not found: ${selector}`);
}
return { selectionStart: editContext.selectionStart, selectionEnd: editContext.selectionEnd };
} else if (isHTMLTextAreaElement(element)) {
return { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd };
} else {
throw new Error(`Unknown type of element: ${selector}`);
}
}
async getTerminalBuffer(selector: string): Promise<string[]> {
@@ -38,6 +38,7 @@ export interface IWindowDriver {
getElements(selector: string, recursive: boolean): Promise<IElement[]>;
getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }>;
typeInEditor(selector: string, text: string): Promise<void>;
getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }>;
getTerminalBuffer(selector: string): Promise<string[]>;
writeInTerminal(selector: string, text: string): Promise<void>;
getLocaleInfo(): Promise<ILocaleInfo>;
+10 -3
View File
@@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser';
import { PlaywrightDriver } from './playwrightDriver';
import { launch as launchPlaywrightElectron } from './playwrightElectron';
import { teardown } from './processes';
import { Quality } from './application';
export interface LaunchOptions {
codePath?: string;
@@ -28,6 +29,7 @@ export interface LaunchOptions {
readonly tracing?: boolean;
readonly headless?: boolean;
readonly browser?: 'chromium' | 'webkit' | 'firefox';
readonly quality: Quality;
}
interface ICodeInstance {
@@ -77,7 +79,7 @@ export async function launch(options: LaunchOptions): Promise<Code> {
const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger);
registerInstance(serverProcess, options.logger, 'server');
return new Code(driver, options.logger, serverProcess);
return new Code(driver, options.logger, serverProcess, options.quality);
}
// Electron smoke tests (playwright)
@@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise<Code> {
const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger);
registerInstance(electronProcess, options.logger, 'electron');
return new Code(driver, options.logger, electronProcess);
return new Code(driver, options.logger, electronProcess, options.quality);
}
}
@@ -96,7 +98,8 @@ export class Code {
constructor(
driver: PlaywrightDriver,
readonly logger: Logger,
private readonly mainProcess: cp.ChildProcess
private readonly mainProcess: cp.ChildProcess,
readonly quality: Quality
) {
this.driver = new Proxy(driver, {
get(target, prop) {
@@ -242,6 +245,10 @@ export class Code {
await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`);
}
async waitForEditorSelection(selector: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise<void> {
await this.poll(() => this.driver.getEditorSelection(selector), accept, `get editor selection '${selector}'`);
}
async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise<void> {
await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`);
}
+6 -3
View File
@@ -9,6 +9,7 @@ import { Code, findElement } from './code';
import { Editors } from './editors';
import { Editor } from './editor';
import { IElement } from './driver';
import { Quality } from './application';
const VIEWLET = 'div[id="workbench.view.debug"]';
const DEBUG_VIEW = `${VIEWLET}`;
@@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`;
const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`;
const CONSOLE_LINK = `.repl .value a.link`;
const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea';
const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context';
const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea';
export interface IStackFrame {
name: string;
@@ -127,8 +129,9 @@ export class Debug extends Viewlet {
async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise<void> {
await this.commands.runCommand('Debug: Focus on Debug Console View');
await this.code.waitForActiveElement(REPL_FOCUSED);
await this.code.waitForSetValue(REPL_FOCUSED, text);
const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT;
await this.code.waitForActiveElement(selector);
await this.code.waitForSetValue(selector, text);
// Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed
await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0);
+15 -5
View File
@@ -6,6 +6,7 @@
import { References } from './peek';
import { Commands } from './workbench';
import { Code } from './code';
import { Quality } from './application';
const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box';
const RENAME_INPUT = `${RENAME_BOX} .rename-input`;
@@ -78,10 +79,10 @@ export class Editor {
async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise<void> {
const editor = [selectorPrefix || '', EDITOR(filename)].join(' ');
const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`;
const textarea = `${editor} textarea`;
const editContext = `${editor} ${this._editContextSelector()}`;
await this.code.waitAndClick(line, 1, 1);
await this.code.waitForActiveElement(textarea);
await this.code.waitForActiveElement(editContext);
}
async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise<any> {
@@ -92,14 +93,23 @@ export class Editor {
await this.code.waitForElement(editor);
const textarea = `${editor} textarea`;
await this.code.waitForActiveElement(textarea);
const editContext = `${editor} ${this._editContextSelector()}`;
await this.code.waitForActiveElement(editContext);
await this.code.waitForTypeInEditor(textarea, text);
await this.code.waitForTypeInEditor(editContext, text);
await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix);
}
async waitForEditorSelection(filename: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise<void> {
const selector = `${EDITOR(filename)} ${this._editContextSelector()}`;
await this.code.waitForEditorSelection(selector, accept);
}
private _editContextSelector() {
return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context';
}
async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise<any> {
const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' ');
return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' ')));
+2 -1
View File
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Quality } from './application';
import { Code } from './code';
export class Editors {
@@ -53,7 +54,7 @@ export class Editors {
}
async waitForActiveEditor(fileName: string, retryCount?: number): Promise<any> {
const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`;
const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`;
return this.code.waitForActiveElement(selector, retryCount);
}
+2 -1
View File
@@ -8,6 +8,7 @@ import { Code } from './code';
import { ncp } from 'ncp';
import { promisify } from 'util';
import { Commands } from './workbench';
import { Quality } from './application';
import path = require('path');
import fs = require('fs');
@@ -20,7 +21,7 @@ export class Extensions extends Viewlet {
async searchForExtension(id: string): Promise<any> {
await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true });
await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`);
await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`);
await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace');
let retrials = 1;
+4 -3
View File
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Quality } from './application';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { QuickInput } from './quickinput';
@@ -46,10 +47,10 @@ export class Notebook {
await this.code.waitForElement(editor);
const textarea = `${editor} textarea`;
await this.code.waitForActiveElement(textarea);
const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`;
await this.code.waitForActiveElement(editContext);
await this.code.waitForTypeInEditor(textarea, text);
await this.code.waitForTypeInEditor(editContext, text);
await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1);
}
+4
View File
@@ -288,6 +288,10 @@ export class PlaywrightDriver {
return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const);
}
async getEditorSelection(selector: string) {
return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const);
}
async getTerminalBuffer(selector: string) {
return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const);
}
+11 -5
View File
@@ -6,9 +6,11 @@
import { Viewlet } from './viewlet';
import { IElement } from './driver';
import { findElement, findElements, Code } from './code';
import { Quality } from './application';
const VIEWLET = 'div[id="workbench.view.scm"]';
const SCM_INPUT = `${VIEWLET} .scm-editor textarea`;
const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`;
const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`;
const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`;
const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`;
const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`;
@@ -44,7 +46,7 @@ export class SCM extends Viewlet {
async openSCMViewlet(): Promise<any> {
await this.code.dispatchKeybinding('ctrl+shift+g');
await this.code.waitForElement(SCM_INPUT);
await this.code.waitForElement(this._editContextSelector());
}
async waitForChange(name: string, type?: string): Promise<void> {
@@ -71,9 +73,13 @@ export class SCM extends Viewlet {
}
async commit(message: string): Promise<void> {
await this.code.waitAndClick(SCM_INPUT);
await this.code.waitForActiveElement(SCM_INPUT);
await this.code.waitForSetValue(SCM_INPUT, message);
await this.code.waitAndClick(this._editContextSelector());
await this.code.waitForActiveElement(this._editContextSelector());
await this.code.waitForSetValue(this._editContextSelector(), message);
await this.code.waitAndClick(COMMIT_COMMAND);
}
private _editContextSelector(): string {
return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT;
}
}
+13 -4
View File
@@ -7,8 +7,10 @@ import { Editor } from './editor';
import { Editors } from './editors';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { Quality } from './application';
const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea';
const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context';
const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea';
export class SettingsEditor {
constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { }
@@ -23,6 +25,7 @@ export class SettingsEditor {
await this.openUserSettingsFile();
await this.code.dispatchKeybinding('right');
await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1);
await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`);
await this.editors.saveOpenedFile();
}
@@ -37,6 +40,7 @@ export class SettingsEditor {
await this.openUserSettingsFile();
await this.code.dispatchKeybinding('right');
await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1);
await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join(''));
await this.editors.saveOpenedFile();
}
@@ -45,6 +49,7 @@ export class SettingsEditor {
await this.openUserSettingsFile();
await this.quickaccess.runCommand('editor.action.selectAll');
await this.code.dispatchKeybinding('Delete');
await this.editor.waitForEditorContents('settings.json', contents => contents === '');
await this.editor.waitForTypeInEditor('settings.json', `{`); // will auto close }
await this.editors.saveOpenedFile();
await this.quickaccess.runCommand('workbench.action.closeActiveEditor');
@@ -57,13 +62,13 @@ export class SettingsEditor {
async openUserSettingsUI(): Promise<void> {
await this.quickaccess.runCommand('workbench.action.openSettings2');
await this.code.waitForActiveElement(SEARCH_BOX);
await this.code.waitForActiveElement(this._editContextSelector());
}
async searchSettingsUI(query: string): Promise<void> {
await this.openUserSettingsUI();
await this.code.waitAndClick(SEARCH_BOX);
await this.code.waitAndClick(this._editContextSelector());
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+a');
} else {
@@ -71,7 +76,11 @@ export class SettingsEditor {
}
await this.code.dispatchKeybinding('Delete');
await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent));
await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query);
await this.code.waitForTypeInEditor(this._editContextSelector(), query);
await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found'));
}
private _editContextSelector() {
return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT;
}
}