Reland "Port the console prompt to CodeMirror 6"

This reverts commit d635360712.

Reason for revert: tests should be fixed now

Original change's description:
> Revert "Port the console prompt to CodeMirror 6"
>
> This reverts commit 71cd14a337.
>
> 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:
Marijn Haverbeke
2021-11-22 16:10:18 +00:00
committed by Devtools-frontend LUCI CQ
parent e53f51a1b0
commit f034f0b144
16 changed files with 234 additions and 220 deletions
-1
View File
@@ -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,
],
}));
+127 -177
View File
@@ -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 {
+1 -1
View File
@@ -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,
+21 -13
View File
@@ -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) {
+29 -4
View File
@@ -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(),
+2
View File
@@ -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;
+7 -7
View File
@@ -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;
+3 -2
View File
@@ -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\""
}
]
}
]