mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-19 13:04:39 +08:00
Reland "Port the console prompt to CodeMirror 6"
This reverts commitd635360712. Reason for revert: tests should be fixed now Original change's description: > Revert "Port the console prompt to CodeMirror 6" > > This reverts commit71cd14a337. > > Reason for revert: This CL caused the waterfall to go red > https://ci.chromium.org/ui/p/devtools-frontend/builders/ci/Stand-alone%20Mac/6200/test-results > > Original change's description: > > Port the console prompt to CodeMirror 6 > > > > And fix some autocompletion issues that came up when testing it. > > > > R=tvanderlippe@chromium.org > > > > Bug: 1241848 > > Change-Id: I70d7fefa5fe3c9e09429d0b9a8fc4e39f0fe3f44 > > Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3289305 > > Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> > > Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> > > Bug: 1241848 > Change-Id: Ied3d3eb14ddf495e70445a1cb7076cdfab5f38fa > No-Presubmit: true > No-Tree-Checks: true > No-Try: true > Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3291584 > Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com> > Commit-Queue: Nancy Li <nancyly@chromium.org> > Auto-Submit: Nancy Li <nancyly@chromium.org> > Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> Bug: 1241848 Change-Id: If88bafb86bd1730a0b81a0f362ed781584013289 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3295531 Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Victor Porof <victorporof@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
This commit is contained in:
committed by
Devtools-frontend LUCI CQ
parent
e53f51a1b0
commit
f034f0b144
@@ -48,7 +48,6 @@ devtools_module("console") {
|
||||
"../../ui/legacy:bundle",
|
||||
"../../ui/legacy/components/data_grid:bundle",
|
||||
"../../ui/legacy/components/object_ui:bundle",
|
||||
"../../ui/legacy/components/text_editor:bundle",
|
||||
"../../ui/legacy/components/utils:bundle",
|
||||
"../../ui/legacy/theme_support:bundle",
|
||||
"../console_counters:bundle",
|
||||
|
||||
@@ -255,6 +255,7 @@ export class ConsolePin {
|
||||
]),
|
||||
CodeMirror.EditorView.domEventHandlers({blur: (_e, view) => this.onBlur(view)}),
|
||||
TextEditor.Config.baseConfiguration(expression),
|
||||
TextEditor.Config.closeBrackets,
|
||||
TextEditor.Config.autocompletion,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as Common from '../../core/common/common.js';
|
||||
import * as Host from '../../core/host/host.js';
|
||||
import * as i18n from '../../core/i18n/i18n.js';
|
||||
import * as SDK from '../../core/sdk/sdk.js';
|
||||
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
||||
import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
|
||||
import * as TextEditor from '../../ui/components/text_editor/text_editor.js';
|
||||
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
|
||||
import * as TextEditor from '../../ui/legacy/components/text_editor/text_editor.js';
|
||||
import * as UI from '../../ui/legacy/legacy.js';
|
||||
|
||||
import {ConsolePanel} from './ConsolePanel.js';
|
||||
@@ -27,17 +27,17 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
private addCompletionsFromHistory: boolean;
|
||||
private historyInternal: ConsoleHistoryManager;
|
||||
private initialText: string;
|
||||
private editor: UI.TextEditor.TextEditor|null;
|
||||
private editor: TextEditor.TextEditor.TextEditor;
|
||||
private readonly eagerPreviewElement: HTMLDivElement;
|
||||
private textChangeThrottler: Common.Throttler.Throttler;
|
||||
private readonly formatter: ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter;
|
||||
private requestPreviewBound: () => Promise<void>;
|
||||
private requestPreviewCurrent = 0;
|
||||
private readonly innerPreviewElement: HTMLElement;
|
||||
private readonly promptIcon: UI.Icon.Icon;
|
||||
private readonly iconThrottler: Common.Throttler.Throttler;
|
||||
private readonly eagerEvalSetting: Common.Settings.Setting<boolean>;
|
||||
private previewRequestForTest: Promise<void>|null;
|
||||
private defaultAutocompleteConfig: UI.TextEditor.AutocompleteConfig|null;
|
||||
private highlightingNode: boolean;
|
||||
|
||||
constructor() {
|
||||
@@ -46,7 +46,6 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
this.historyInternal = new ConsoleHistoryManager();
|
||||
|
||||
this.initialText = '';
|
||||
this.editor = null;
|
||||
this.eagerPreviewElement = document.createElement('div');
|
||||
this.eagerPreviewElement.classList.add('console-eager-preview');
|
||||
this.textChangeThrottler = new Common.Throttler.Throttler(150);
|
||||
@@ -68,41 +67,41 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
|
||||
this.element.tabIndex = 0;
|
||||
this.previewRequestForTest = null;
|
||||
|
||||
this.defaultAutocompleteConfig = null;
|
||||
|
||||
this.highlightingNode = false;
|
||||
|
||||
const factory = TextEditor.CodeMirrorTextEditor.CodeMirrorTextEditorFactory.instance();
|
||||
const editorState = CodeMirror.EditorState.create({
|
||||
doc: this.initialText,
|
||||
extensions: [
|
||||
CodeMirror.keymap.of(this.editorKeymap()),
|
||||
CodeMirror.EditorView.updateListener.of(update => this.editorUpdate(update)),
|
||||
TextEditor.JavaScript.argumentHints(),
|
||||
TextEditor.JavaScript.completion(),
|
||||
TextEditor.Config.showCompletionHint,
|
||||
CodeMirror.javascript.javascript(),
|
||||
TextEditor.Config.baseConfiguration(this.initialText),
|
||||
TextEditor.Config.autocompletion,
|
||||
CodeMirror.javascript.javascriptLanguage.data.of({
|
||||
autocomplete: (context: CodeMirror.CompletionContext): CodeMirror.CompletionResult | null =>
|
||||
this.historyCompletions(context),
|
||||
}),
|
||||
CodeMirror.EditorView.contentAttributes.of({'aria-label': i18nString(UIStrings.consolePrompt)}),
|
||||
CodeMirror.EditorView.lineWrapping,
|
||||
CodeMirror.autocompletion({aboveCursor: true}),
|
||||
],
|
||||
});
|
||||
|
||||
const options = {
|
||||
devtoolsAccessibleName: (i18nString(UIStrings.consolePrompt) as string),
|
||||
lineNumbers: false,
|
||||
lineWrapping: true,
|
||||
mimeType: 'javascript',
|
||||
autoHeight: true,
|
||||
};
|
||||
this.editor = factory.createEditor((options as UI.TextEditor.Options));
|
||||
this.editor = new TextEditor.TextEditor.TextEditor(editorState);
|
||||
this.editor.addEventListener('keydown', (event): void => {
|
||||
if (event.defaultPrevented) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
editorContainerElement.appendChild(this.editor);
|
||||
|
||||
this.defaultAutocompleteConfig =
|
||||
ObjectUI.JavaScriptAutocomplete.JavaScriptAutocompleteConfig.createConfigForEditor(this.editor);
|
||||
this.editor.configureAutocomplete(Object.assign({}, this.defaultAutocompleteConfig, {
|
||||
suggestionsCallback: this.wordsWithQuery.bind(this),
|
||||
anchorBehavior: UI.GlassPane.AnchorBehavior.PreferTop,
|
||||
}));
|
||||
this.editor.widget().element.addEventListener('keydown', this.editorKeyDown.bind(this), true);
|
||||
this.editor.widget().show(editorContainerElement);
|
||||
this.editor.addEventListener(UI.TextEditor.Events.CursorChanged, this.updatePromptIcon, this);
|
||||
this.editor.addEventListener(UI.TextEditor.Events.TextChanged, this.onTextChanged, this);
|
||||
this.editor.addEventListener(UI.TextEditor.Events.SuggestionChanged, this.onTextChanged, this);
|
||||
|
||||
this.setText(this.initialText);
|
||||
this.initialText = '';
|
||||
if (this.hasFocus()) {
|
||||
this.focus();
|
||||
}
|
||||
this.element.removeAttribute('tabindex');
|
||||
this.editor.widget().element.tabIndex = -1;
|
||||
|
||||
this.editorSetForTest();
|
||||
|
||||
@@ -126,7 +125,7 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
// ConsoleView and prompt both use a throttler, so we clear the preview
|
||||
// ASAP to avoid inconsistency between a fresh viewport and stale preview.
|
||||
if (this.eagerEvalSetting.get()) {
|
||||
const asSoonAsPossible = !this.editor || !this.editor.textWithCurrentSuggestion();
|
||||
const asSoonAsPossible = !TextEditor.Config.contentIncludingHint(this.editor.editor);
|
||||
this.previewRequestForTest = this.textChangeThrottler.schedule(this.requestPreviewBound, asSoonAsPossible);
|
||||
}
|
||||
this.updatePromptIcon();
|
||||
@@ -134,15 +133,16 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
}
|
||||
|
||||
private async requestPreview(): Promise<void> {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const text = this.editor.textWithCurrentSuggestion().trim();
|
||||
const id = ++this.requestPreviewCurrent;
|
||||
const text = TextEditor.Config.contentIncludingHint(this.editor.editor).trim();
|
||||
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
|
||||
const {preview, result} = await ObjectUI.JavaScriptREPL.JavaScriptREPL.evaluateAndBuildPreview(
|
||||
text, true /* throwOnSideEffect */, true /* replMode */, 500 /* timeout */);
|
||||
if (this.requestPreviewCurrent !== id) {
|
||||
return;
|
||||
}
|
||||
this.innerPreviewElement.removeChildren();
|
||||
if (preview.deepTextContent() !== this.editor.textWithCurrentSuggestion().trim()) {
|
||||
if (preview.deepTextContent() !== TextEditor.Config.contentIncludingHint(this.editor.editor).trim()) {
|
||||
this.innerPreviewElement.appendChild(preview);
|
||||
}
|
||||
if (result && 'object' in result && result.object && result.object.subtype === 'node') {
|
||||
@@ -174,149 +174,102 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
}
|
||||
|
||||
clearAutocomplete(): void {
|
||||
if (this.editor) {
|
||||
this.editor.clearAutocomplete();
|
||||
}
|
||||
CodeMirror.closeCompletion(this.editor.editor);
|
||||
}
|
||||
|
||||
private isCaretAtEndOfPrompt(): boolean {
|
||||
return this.editor !== null &&
|
||||
this.editor.selection().collapseToEnd().equal(this.editor.fullRange().collapseToEnd());
|
||||
return this.editor.state.selection.main.head === this.editor.state.doc.length;
|
||||
}
|
||||
|
||||
moveCaretToEndOfPrompt(): void {
|
||||
if (this.editor) {
|
||||
this.editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(Infinity, Infinity));
|
||||
}
|
||||
this.editor.dispatch({
|
||||
selection: CodeMirror.EditorSelection.cursor(this.editor.state.doc.length),
|
||||
});
|
||||
}
|
||||
|
||||
setText(text: string): void {
|
||||
if (this.editor) {
|
||||
this.editor.setText(text);
|
||||
} else {
|
||||
this.initialText = text;
|
||||
}
|
||||
this.dispatchEventToListeners(Events.TextChanged);
|
||||
clear(): void {
|
||||
this.editor.dispatch({
|
||||
changes: {from: 0, to: this.editor.state.doc.length},
|
||||
});
|
||||
}
|
||||
|
||||
text(): string {
|
||||
return this.editor ? this.editor.text() : this.initialText;
|
||||
return this.editor.state.doc.toString();
|
||||
}
|
||||
|
||||
setAddCompletionsFromHistory(value: boolean): void {
|
||||
this.addCompletionsFromHistory = value;
|
||||
}
|
||||
|
||||
private editorKeyDown(event: Event): void {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const keyboardEvent = (event as KeyboardEvent);
|
||||
let newText;
|
||||
let isPrevious;
|
||||
// Check against visual coordinates in case lines wrap.
|
||||
const selection = this.editor.selection();
|
||||
const cursorY = this.editor.visualCoordinates(selection.endLine, selection.endColumn).y;
|
||||
|
||||
switch (keyboardEvent.keyCode) {
|
||||
case UI.KeyboardShortcut.Keys.Up.code: {
|
||||
const startY = this.editor.visualCoordinates(0, 0).y;
|
||||
if (keyboardEvent.shiftKey || !selection.isEmpty() || cursorY !== startY) {
|
||||
break;
|
||||
}
|
||||
newText = this.historyInternal.previous(this.text());
|
||||
isPrevious = true;
|
||||
break;
|
||||
}
|
||||
case UI.KeyboardShortcut.Keys.Down.code: {
|
||||
const fullRange = this.editor.fullRange();
|
||||
const endY = this.editor.visualCoordinates(fullRange.endLine, fullRange.endColumn).y;
|
||||
if (keyboardEvent.shiftKey || !selection.isEmpty() || cursorY !== endY) {
|
||||
break;
|
||||
}
|
||||
newText = this.historyInternal.next();
|
||||
break;
|
||||
}
|
||||
case UI.KeyboardShortcut.Keys.P.code: { // Ctrl+P = Previous
|
||||
if (Host.Platform.isMac() && keyboardEvent.ctrlKey && !keyboardEvent.metaKey && !keyboardEvent.altKey &&
|
||||
!keyboardEvent.shiftKey) {
|
||||
newText = this.historyInternal.previous(this.text());
|
||||
isPrevious = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UI.KeyboardShortcut.Keys.N.code: { // Ctrl+N = Next
|
||||
if (Host.Platform.isMac() && keyboardEvent.ctrlKey && !keyboardEvent.metaKey && !keyboardEvent.altKey &&
|
||||
!keyboardEvent.shiftKey) {
|
||||
newText = this.historyInternal.next();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UI.KeyboardShortcut.Keys.Enter.code: {
|
||||
this.enterKeyPressed(keyboardEvent);
|
||||
break;
|
||||
}
|
||||
case UI.KeyboardShortcut.Keys.Tab.code: {
|
||||
if (!this.text()) {
|
||||
keyboardEvent.consume();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newText === undefined) {
|
||||
return;
|
||||
}
|
||||
keyboardEvent.consume(true);
|
||||
this.setText(newText);
|
||||
|
||||
if (isPrevious) {
|
||||
this.editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(0, Infinity));
|
||||
} else {
|
||||
this.moveCaretToEndOfPrompt();
|
||||
}
|
||||
private editorKeymap(): readonly CodeMirror.KeyBinding[] {
|
||||
return [
|
||||
{key: 'ArrowUp', run: (): boolean => this.moveHistory(-1)},
|
||||
{key: 'ArrowDown', run: (): boolean => this.moveHistory(1)},
|
||||
{mac: 'Ctrl-p', run: (): boolean => this.moveHistory(-1, true)},
|
||||
{mac: 'Ctrl-n', run: (): boolean => this.moveHistory(1, true)},
|
||||
{key: 'Enter', run: (): boolean => this.evaluate(), shift: CodeMirror.insertNewlineAndIndent},
|
||||
];
|
||||
}
|
||||
|
||||
private async enterWillEvaluate(): Promise<boolean> {
|
||||
if (!this.isCaretAtEndOfPrompt()) {
|
||||
return true;
|
||||
private moveHistory(dir: -1|1, force = false): boolean {
|
||||
const {editor} = this.editor, {main} = editor.state.selection;
|
||||
if (!force) {
|
||||
if (!main.empty) {
|
||||
return false;
|
||||
}
|
||||
const cursorCoords = editor.coordsAtPos(main.head);
|
||||
const endCoords = editor.coordsAtPos(dir < 0 ? 0 : editor.state.doc.length);
|
||||
// Check if there are wrapped lines in this direction, and let
|
||||
// the cursor move normally if there are.
|
||||
if (cursorCoords && endCoords &&
|
||||
(dir < 0 ? cursorCoords.top > endCoords.top + 5 : cursorCoords.bottom < endCoords.bottom - 5)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return await ObjectUI.JavaScriptAutocomplete.JavaScriptAutocomplete.isExpressionComplete(this.text());
|
||||
|
||||
const history = this.historyInternal;
|
||||
const newText = dir < 0 ? history.previous(this.text()) : history.next();
|
||||
if (newText === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursorPos = dir < 0 ? newText.search(/\n|$/) : newText.length;
|
||||
this.editor.dispatch({
|
||||
changes: {from: 0, to: this.editor.state.doc.length, insert: newText},
|
||||
selection: CodeMirror.EditorSelection.cursor(cursorPos),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private enterWillEvaluate(): boolean {
|
||||
const {state} = this.editor;
|
||||
return state.doc.length > 0 && TextEditor.JavaScript.isExpressionComplete(state);
|
||||
}
|
||||
|
||||
private evaluate(): boolean {
|
||||
if (this.enterWillEvaluate()) {
|
||||
this.appendCommand(this.text(), true);
|
||||
this.editor.dispatch({
|
||||
changes: {from: 0, to: this.editor.state.doc.length},
|
||||
scrollIntoView: true,
|
||||
});
|
||||
} else if (this.editor.state.doc.length) {
|
||||
return CodeMirror.insertNewlineAndIndent(this.editor.editor);
|
||||
} else {
|
||||
this.editor.dispatch({scrollIntoView: true});
|
||||
}
|
||||
this.enterProcessedForTest();
|
||||
return true;
|
||||
}
|
||||
|
||||
private updatePromptIcon(): void {
|
||||
this.iconThrottler.schedule(async () => {
|
||||
const canComplete = await this.enterWillEvaluate();
|
||||
this.promptIcon.classList.toggle('console-prompt-incomplete', !canComplete);
|
||||
this.promptIcon.classList.toggle('console-prompt-incomplete', !this.enterWillEvaluate());
|
||||
});
|
||||
}
|
||||
|
||||
private async enterKeyPressed(event: KeyboardEvent): Promise<void> {
|
||||
if (event.altKey || event.ctrlKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.consume(true);
|
||||
|
||||
// Since we prevent default, manually emulate the native "scroll on key input" behavior.
|
||||
this.element.scrollIntoView();
|
||||
this.clearAutocomplete();
|
||||
|
||||
const str = this.text();
|
||||
if (!str.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.enterWillEvaluate()) {
|
||||
await this.appendCommand(str, true);
|
||||
} else if (this.editor) {
|
||||
this.editor.newlineAndIndent();
|
||||
}
|
||||
this.enterProcessedForTest();
|
||||
}
|
||||
|
||||
private async appendCommand(text: string, useCommandLineAPI: boolean): Promise<void> {
|
||||
this.setText('');
|
||||
private appendCommand(text: string, useCommandLineAPI: boolean): void {
|
||||
const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
|
||||
if (currentExecutionContext) {
|
||||
const executionContext = currentExecutionContext;
|
||||
@@ -333,10 +286,19 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
private enterProcessedForTest(): void {
|
||||
}
|
||||
|
||||
private historyCompletions(prefix: string, force?: boolean): UI.SuggestBox.Suggestions {
|
||||
private editorUpdate(update: CodeMirror.ViewUpdate): void {
|
||||
if (update.docChanged ||
|
||||
CodeMirror.selectedCompletion(update.state) !== CodeMirror.selectedCompletion(update.startState)) {
|
||||
this.onTextChanged();
|
||||
} else if (update.selectionSet) {
|
||||
this.updatePromptIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private historyCompletions(context: CodeMirror.CompletionContext): CodeMirror.CompletionResult|null {
|
||||
const text = this.text();
|
||||
if (!this.addCompletionsFromHistory || !this.isCaretAtEndOfPrompt() || (!text && !force)) {
|
||||
return [];
|
||||
if (!this.addCompletionsFromHistory || !this.isCaretAtEndOfPrompt() || (!text.length && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
const result = [];
|
||||
const set = new Set<string>();
|
||||
@@ -350,30 +312,18 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
|
||||
continue;
|
||||
}
|
||||
set.add(item);
|
||||
result.push(
|
||||
{text: item.substring(text.length - prefix.length), iconType: 'smallicon-text-prompt', isSecondary: true});
|
||||
result.push({label: item, type: 'secondary', boost: -1e5});
|
||||
}
|
||||
return result as UI.SuggestBox.Suggestions;
|
||||
return result.length ? {
|
||||
from: 0,
|
||||
to: text.length,
|
||||
options: result,
|
||||
} :
|
||||
null;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.editor) {
|
||||
this.editor.widget().focus();
|
||||
} else {
|
||||
this.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private async wordsWithQuery(
|
||||
queryRange: TextUtils.TextRange.TextRange, substituteRange: TextUtils.TextRange.TextRange,
|
||||
force?: boolean): Promise<UI.SuggestBox.Suggestions> {
|
||||
if (!this.editor || !this.defaultAutocompleteConfig || !this.defaultAutocompleteConfig.suggestionsCallback) {
|
||||
return [];
|
||||
}
|
||||
const query = this.editor.text(queryRange);
|
||||
const words = await this.defaultAutocompleteConfig.suggestionsCallback(queryRange, substituteRange, force);
|
||||
const historyWords = this.historyCompletions(query, force);
|
||||
return words ? words.concat(historyWords) : historyWords;
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
private editorSetForTest(): void {
|
||||
|
||||
@@ -1218,7 +1218,7 @@ export class ConsoleView extends UI.Widget.VBox implements UI.SearchableView.Sea
|
||||
}
|
||||
|
||||
private clearPromptBackwards(): void {
|
||||
this.prompt.setText('');
|
||||
this.prompt.clear();
|
||||
}
|
||||
|
||||
private promptKeyDown(event: Event): void {
|
||||
|
||||
@@ -1044,6 +1044,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
||||
},
|
||||
]),
|
||||
TextEditor.Config.baseConfiguration(initialValue),
|
||||
TextEditor.Config.closeBrackets,
|
||||
TextEditor.Config.autocompletion,
|
||||
CodeMirror.html.html(),
|
||||
TextEditor.Config.domWordWrap.instance(),
|
||||
|
||||
@@ -71,6 +71,7 @@ export class BreakpointEditDialog extends UI.Widget.Widget {
|
||||
const editorConfig = [
|
||||
CodeMirror.javascript.javascriptLanguage,
|
||||
TextEditor.Config.baseConfiguration(oldCondition || ''),
|
||||
TextEditor.Config.closeBrackets,
|
||||
TextEditor.Config.autocompletion,
|
||||
CodeMirror.EditorView.lineWrapping,
|
||||
TextEditor.Config.showCompletionHint,
|
||||
|
||||
@@ -68,7 +68,10 @@ export const tabMovesFocus = DynamicSetting.bool('textEditorTabMovesFocus', CM.k
|
||||
shift: (view: CM.EditorView): boolean => view.state.doc.length ? CM.indentLess(view) : false,
|
||||
}]));
|
||||
|
||||
export const autocompletion = CM.autocompletion({icons: false});
|
||||
export const autocompletion = CM.autocompletion({
|
||||
icons: false,
|
||||
optionClass: (option: CM.Completion): string => option.type === 'secondary' ? 'cm-secondaryCompletion' : '',
|
||||
});
|
||||
|
||||
export const sourcesAutocompletion = DynamicSetting.bool('textEditorAutocompletion', autocompletion);
|
||||
|
||||
@@ -185,7 +188,6 @@ const baseKeymap = CM.keymap.of([
|
||||
{key: 'Mod-d', run: CM.selectNextOccurrence},
|
||||
{key: 'Alt-ArrowLeft', mac: 'Ctrl-ArrowLeft', run: CM.cursorSubwordBackward, shift: CM.selectSubwordBackward},
|
||||
{key: 'Alt-ArrowRight', mac: 'Ctrl-ArrowRight', run: CM.cursorSubwordForward, shift: CM.selectSubwordForward},
|
||||
...CM.closeBracketsKeymap,
|
||||
...CM.standardKeymap,
|
||||
...CM.historyKeymap,
|
||||
]);
|
||||
@@ -219,7 +221,6 @@ export function baseConfiguration(text: string): CM.Extension {
|
||||
CM.EditorState.allowMultipleSelections.of(true),
|
||||
CM.indentOnInput(),
|
||||
CodeHighlighter.CodeHighlighter.highlightStyle,
|
||||
CM.closeBrackets(),
|
||||
baseKeymap,
|
||||
tabMovesFocus.instance(),
|
||||
bracketMatching.instance(),
|
||||
@@ -234,6 +235,11 @@ export function baseConfiguration(text: string): CM.Extension {
|
||||
];
|
||||
}
|
||||
|
||||
export const closeBrackets: CM.Extension = [
|
||||
CM.closeBrackets(),
|
||||
CM.keymap.of(CM.closeBracketsKeymap),
|
||||
];
|
||||
|
||||
// Root editor tooltips at the top of the document, creating a special
|
||||
// element with the editor styles mounted in it for them. This is
|
||||
// annoying, but necessary because a scrollable parent node clips them
|
||||
@@ -299,12 +305,16 @@ export const showCompletionHint = CM.ViewPlugin.fromClass(class {
|
||||
}
|
||||
|
||||
topCompletion(state: CM.EditorState): string|null {
|
||||
const completions = CM.currentCompletions(state);
|
||||
if (!completions.length) {
|
||||
const completion = CM.selectedCompletion(state);
|
||||
if (!completion) {
|
||||
return null;
|
||||
}
|
||||
const {label} = completions[0];
|
||||
if (label.length > 100 || label.indexOf('\n') > -1) {
|
||||
let {label, apply} = completion;
|
||||
if (typeof apply === 'string') {
|
||||
label = apply;
|
||||
apply = undefined;
|
||||
}
|
||||
if (apply || label.length > 100 || label.indexOf('\n') > -1 || completion.type === 'secondary') {
|
||||
return null;
|
||||
}
|
||||
const pos = state.selection.main.head;
|
||||
@@ -312,13 +322,11 @@ export const showCompletionHint = CM.ViewPlugin.fromClass(class {
|
||||
if (pos !== lineBefore.to) {
|
||||
return null;
|
||||
}
|
||||
const textBefore = lineBefore.text.slice(0, pos - lineBefore.from);
|
||||
for (let i = label.length - 1; i > 0; i--) {
|
||||
if (textBefore.endsWith(label.slice(0, i)) && !/\w/.test(textBefore.charAt(textBefore.length - i - 1))) {
|
||||
return label.slice(i);
|
||||
}
|
||||
const wordBefore = /[\w$]+$/.exec(lineBefore.text);
|
||||
if (wordBefore && !label.startsWith(wordBefore[0])) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
return label.slice(wordBefore ? wordBefore[0].length : 0);
|
||||
}
|
||||
}, {decorations: p => p.decorations});
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const enum QueryType {
|
||||
PropertyExpression = 2,
|
||||
}
|
||||
|
||||
export function getQueryType(tree: CodeMirror.Tree, pos: number): {
|
||||
export function getQueryType(tree: CodeMirror.Tree, pos: number, doc: CodeMirror.Text): {
|
||||
type: QueryType,
|
||||
from?: number,
|
||||
relatedNode?: CodeMirror.SyntaxNode,
|
||||
@@ -101,13 +101,15 @@ export function getQueryType(tree: CodeMirror.Tree, pos: number): {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.name === 'VariableName') {
|
||||
return {type: QueryType.Expression, from: node.from};
|
||||
}
|
||||
if (node.name === 'PropertyName') {
|
||||
return parent?.name !== 'MemberExpression' ? null :
|
||||
{type: QueryType.PropertyName, from: node.from, relatedNode: parent};
|
||||
}
|
||||
if (node.name === 'VariableName' ||
|
||||
// Treat alphabetic keywords as variables
|
||||
!node.firstChild && node.to - node.from < 20 && !/[^a-z]/.test(doc.sliceString(node.from, node.to))) {
|
||||
return {type: QueryType.Expression, from: node.from};
|
||||
}
|
||||
if (node.name === 'String') {
|
||||
const parent = node.parent;
|
||||
return parent?.name === 'MemberExpression' && parent.childBefore(node.from)?.name === '[' ?
|
||||
@@ -134,8 +136,8 @@ export function getQueryType(tree: CodeMirror.Tree, pos: number): {
|
||||
|
||||
export async function javascriptCompletionSource(cx: CodeMirror.CompletionContext):
|
||||
Promise<CodeMirror.CompletionResult|null> {
|
||||
const query = getQueryType(CodeMirror.syntaxTree(cx.state), cx.pos);
|
||||
if (!query || query.from === undefined && !cx.explicit) {
|
||||
const query = getQueryType(CodeMirror.syntaxTree(cx.state), cx.pos, cx.state.doc);
|
||||
if (!query || query.from === undefined && !cx.explicit && query.type === QueryType.Expression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -162,7 +164,8 @@ export async function javascriptCompletionSource(cx: CodeMirror.CompletionContex
|
||||
if (!objectExpr) {
|
||||
return null;
|
||||
}
|
||||
result = await completeProperties(cx.state.sliceDoc(objectExpr.from, objectExpr.to), quote);
|
||||
result = await completeProperties(
|
||||
cx.state.sliceDoc(objectExpr.from, objectExpr.to), quote, cx.state.sliceDoc(cx.pos, cx.pos + 1) === ']');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -250,6 +253,7 @@ class PropertyCache {
|
||||
async function completeProperties(
|
||||
expression: string,
|
||||
quoted?: string,
|
||||
hasBracket: boolean = false,
|
||||
): Promise<CompletionSet> {
|
||||
const cache = PropertyCache.instance();
|
||||
if (!quoted) {
|
||||
@@ -262,7 +266,7 @@ async function completeProperties(
|
||||
if (!context) {
|
||||
return new CompletionSet();
|
||||
}
|
||||
const result = completePropertiesInner(expression, context, quoted);
|
||||
const result = completePropertiesInner(expression, context, quoted, hasBracket);
|
||||
if (!quoted) {
|
||||
cache.set(expression, result);
|
||||
}
|
||||
@@ -275,6 +279,7 @@ async function completePropertiesInner(
|
||||
expression: string,
|
||||
context: SDK.RuntimeModel.ExecutionContext,
|
||||
quoted?: string,
|
||||
hasBracket: boolean = false,
|
||||
): Promise<CompletionSet> {
|
||||
const result = new CompletionSet();
|
||||
if (!context) {
|
||||
@@ -313,7 +318,7 @@ async function completePropertiesInner(
|
||||
label,
|
||||
type: prop.value?.type === 'function' ? functionType : otherType,
|
||||
};
|
||||
if (quoted) {
|
||||
if (quoted && !hasBracket) {
|
||||
completion.apply = label + ']';
|
||||
}
|
||||
if (!prop.isOwn) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const editorTheme = CM.EditorView.theme({
|
||||
fontSize: 'var(--source-code-font-size)',
|
||||
},
|
||||
|
||||
'.cm-panels, .cm-tooltip': {
|
||||
'.cm-panels': {
|
||||
backgroundColor: 'var(--color-background-elevation-1)',
|
||||
},
|
||||
|
||||
@@ -110,25 +110,50 @@ export const editorTheme = CM.EditorView.theme({
|
||||
color: 'var(--color-text-secondary)',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
boxShadow: 'var(--drop-shadow)',
|
||||
backgroundColor: 'var(--color-background-elevation-1)',
|
||||
},
|
||||
|
||||
'.cm-argumentHints': {
|
||||
pointerEvents: 'none',
|
||||
padding: '0 4px',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '20px',
|
||||
marginBottom: '4px',
|
||||
boxShadow: 'var(--drop-shadow)',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
width: 'fit-content',
|
||||
},
|
||||
|
||||
'.cm-tooltip.cm-tooltip-autocomplete > ul': {
|
||||
backgroundColor: 'var(--color-background)',
|
||||
minWidth: '16em',
|
||||
'& > li.cm-secondaryCompletion': {
|
||||
display: 'flex',
|
||||
backgroundColor: 'var(--color-background-elevation-1)',
|
||||
justifyContent: 'space-between',
|
||||
'&::before': {
|
||||
content: '">"',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--color-primary-variant)',
|
||||
marginRight: '5px',
|
||||
},
|
||||
},
|
||||
'& > li:hover': {
|
||||
backgroundColor: 'var(--item-hover-color)',
|
||||
},
|
||||
'& > li[aria-selected]': {
|
||||
backgroundColor: 'var(--color-selected-option-background)',
|
||||
color: 'var(--color-selected-option)',
|
||||
'&, &.cm-secondaryCompletion::before': {
|
||||
color: 'var(--color-selected-option)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'.cm-completionMatchedText': {
|
||||
textDecoration: 'none',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
'.cm-highlightedLine': {
|
||||
animation: 'cm-fading-highlight 2s 0s',
|
||||
},
|
||||
|
||||
@@ -206,6 +206,7 @@ export class SourceFrameImpl extends Common.ObjectWrapper.eventMixin<EventTypes,
|
||||
return [
|
||||
CodeMirror.EditorView.updateListener.of(update => this.dispatchEventToListeners(Events.EditorUpdate, update)),
|
||||
TextEditor.Config.baseConfiguration(doc),
|
||||
TextEditor.Config.closeBrackets,
|
||||
TextEditor.Config.sourcesAutocompletion.instance(),
|
||||
TextEditor.Config.showWhitespace.instance(),
|
||||
TextEditor.Config.allowScrollPastEof.instance(),
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
--color-selected-option: #fff;
|
||||
--color-selected-option-background: #1a73e8;
|
||||
--color-highlighted-line: rgb(255 255 0 / 50%);
|
||||
--color-completion-hover: rgb(56 121 217 / 10%);
|
||||
--color-search-match-border: rgb(128 128 128);
|
||||
--color-selected-search-match: var(--color-text-primary);
|
||||
--color-selected-search-match-background: rgb(241 234 0);
|
||||
@@ -307,6 +308,7 @@
|
||||
--color-selected-option: #fff;
|
||||
--color-selected-option-background: #0e639c;
|
||||
--color-highlighted-line: hsl(133deg 100% 30% / 50%);
|
||||
--color-completion-hover: rgb(56 121 217 / 10%);
|
||||
--color-search-match-border: rgb(128 128 128);
|
||||
--color-selected-search-match: #eee;
|
||||
--color-selected-search-match-background: hsl(133deg 100% 30%);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import {click, getBrowserAndPages, typeText, waitFor, waitForNone} from '../../shared/helper.js';
|
||||
import {beforeEach, describe, it} from '../../shared/mocha-extensions.js';
|
||||
import {CONSOLE_TAB_SELECTOR, focusConsolePrompt} from '../helpers/console-helpers.js';
|
||||
import {CONSOLE_TAB_SELECTOR, CONSOLE_TOOLTIP_SELECTOR, focusConsolePrompt} from '../helpers/console-helpers.js';
|
||||
import {openSourcesPanel} from '../helpers/sources-helpers.js';
|
||||
|
||||
describe('The Console Tab', async () => {
|
||||
@@ -33,15 +33,15 @@ describe('The Console Tab', async () => {
|
||||
async function objectAutocompleteTest(textAfterObject: string) {
|
||||
const {frontend} = getBrowserAndPages();
|
||||
|
||||
const appearPromise = waitFor('.suggest-box');
|
||||
const appearPromise = waitFor(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await typeText('object');
|
||||
await appearPromise;
|
||||
|
||||
const disappearPromise = waitForNone('.suggest-box');
|
||||
const disappearPromise = waitForNone(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await frontend.keyboard.press('Escape');
|
||||
await disappearPromise;
|
||||
|
||||
const appearPromise2 = waitFor('.suggest-box');
|
||||
const appearPromise2 = waitFor(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await typeText(textAfterObject);
|
||||
await appearPromise2;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {assert} from 'chai';
|
||||
|
||||
import {click, getBrowserAndPages, typeText, waitFor, waitForNone} from '../../shared/helper.js';
|
||||
import {describe, it} from '../../shared/mocha-extensions.js';
|
||||
import {CONSOLE_TAB_SELECTOR, focusConsolePrompt} from '../helpers/console-helpers.js';
|
||||
import {CONSOLE_TAB_SELECTOR, CONSOLE_TOOLTIP_SELECTOR, focusConsolePrompt} from '../helpers/console-helpers.js';
|
||||
|
||||
describe('The Console Tab', async function() {
|
||||
// The tests in this suite are particularly slow, as they perform a lot of actions
|
||||
@@ -32,11 +32,11 @@ describe('The Console Tab', async function() {
|
||||
// 4. Hit enter
|
||||
// 5. Wait for the results to show up and verify them.
|
||||
|
||||
const appearPromise = waitFor('.suggest-box');
|
||||
const appearPromise = waitFor(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await typeText('let');
|
||||
await appearPromise;
|
||||
|
||||
const disappearPromise = waitForNone('.suggest-box');
|
||||
const disappearPromise = waitForNone(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await frontend.keyboard.press('Escape');
|
||||
await disappearPromise;
|
||||
|
||||
@@ -46,11 +46,11 @@ describe('The Console Tab', async function() {
|
||||
return document.querySelectorAll('.console-user-command-result').length === 1;
|
||||
});
|
||||
|
||||
const appearPromise2 = waitFor('.suggest-box');
|
||||
const appearPromise2 = waitFor(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await typeText('let');
|
||||
await appearPromise2;
|
||||
|
||||
const disappearPromise2 = waitForNone('.suggest-box');
|
||||
const disappearPromise2 = waitForNone(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await frontend.keyboard.press('Escape');
|
||||
await disappearPromise2;
|
||||
|
||||
@@ -60,11 +60,11 @@ describe('The Console Tab', async function() {
|
||||
return document.querySelectorAll('.console-user-command-result').length === 2;
|
||||
});
|
||||
|
||||
const appearPromise3 = waitFor('.suggest-box');
|
||||
const appearPromise3 = waitFor(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await typeText('x');
|
||||
await appearPromise3;
|
||||
|
||||
const disappearPromise3 = waitForNone('.suggest-box');
|
||||
const disappearPromise3 = waitForNone(CONSOLE_TOOLTIP_SELECTOR);
|
||||
await frontend.keyboard.press('Escape');
|
||||
await disappearPromise3;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const LOG_LEVELS_SELECTOR = '[aria-label^="Log level: "]';
|
||||
export const LOG_LEVELS_VERBOSE_OPTION_SELECTOR = '[aria-label^="Verbose"]';
|
||||
export const CONSOLE_PROMPT_SELECTOR = '.console-prompt-editor-container';
|
||||
export const CONSOLE_VIEW_SELECTOR = '.console-view';
|
||||
export const CONSOLE_TOOLTIP_SELECTOR = '.cm-tooltip';
|
||||
export const STACK_PREVIEW_CONTAINER = '.stack-preview-container';
|
||||
export const CONSOLE_MESSAGE_WRAPPER_SELECTOR = '.console-group-messages .console-message-wrapper';
|
||||
export const CONSOLE_SELECTOR = '.console-user-command-result';
|
||||
@@ -160,8 +161,8 @@ export async function typeIntoConsole(frontend: puppeteer.Page, message: string)
|
||||
const consoleElement = await waitFor(CONSOLE_PROMPT_SELECTOR, undefined, asyncScope);
|
||||
await consoleElement.type(message);
|
||||
// Wait for autocomplete text to catch up.
|
||||
const line = await waitFor('.CodeMirror-activeline', consoleElement, asyncScope);
|
||||
const autocomplete = await line.$('.auto-complete-text');
|
||||
const line = await waitFor('[aria-label="Console prompt"]', consoleElement, asyncScope);
|
||||
const autocomplete = await $(CONSOLE_TOOLTIP_SELECTOR);
|
||||
// The autocomplete element doesn't exist until the first autocomplete suggestion
|
||||
// is actually given.
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ describeWithEnvironment('TextEditor', () => {
|
||||
related?: string,
|
||||
): Promise<void> {
|
||||
const state = makeState(code, CodeMirror.javascript.javascriptLanguage);
|
||||
const query = TextEditor.JavaScript.getQueryType(CodeMirror.syntaxTree(state), pos);
|
||||
const query = TextEditor.JavaScript.getQueryType(CodeMirror.syntaxTree(state), pos, state.doc);
|
||||
if (type === undefined) {
|
||||
assert.isNull(query);
|
||||
} else {
|
||||
@@ -117,6 +117,7 @@ describeWithEnvironment('TextEditor', () => {
|
||||
it('recognizes expression queries', async () => {
|
||||
await testQueryType('foo', 3, TextEditor.JavaScript.QueryType.Expression, 'foo');
|
||||
await testQueryType('foo ', 4, TextEditor.JavaScript.QueryType.Expression, '');
|
||||
await testQueryType('let', 3, TextEditor.JavaScript.QueryType.Expression, 'let');
|
||||
});
|
||||
|
||||
it('recognizes propery name queries', async () => {
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
Tests accessibility in the console using the axe-core linter.
|
||||
aXe violations: []
|
||||
aXe violations: [
|
||||
{
|
||||
"ruleDescription": "Ensures ARIA attributes are allowed for an element's role",
|
||||
"helpUrl": "https://dequeuniversity.com/rules/axe/3.3/aria-allowed-attr?application=axeAPI",
|
||||
"ruleId": "aria-allowed-attr",
|
||||
"impact": "critical",
|
||||
"failedNodes": [
|
||||
{
|
||||
"target": [
|
||||
[
|
||||
"devtools-text-editor",
|
||||
".cm-content"
|
||||
]
|
||||
],
|
||||
"html": "<div spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" translate=\"no\" contenteditable=\"true\" class=\"cm-content cm-lineWrapping\" style=\"tab-size: 4\" role=\"textbox\" aria-multiline=\"true\" aria-label=\"Console prompt\" aria-autocomplete=\"list\" aria-expanded=\"false\">",
|
||||
"failureSummary": "Fix any of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user