Add explain experiment

Bug: b/295119680
Change-Id: I046a9516e00f63cd143ded08312abe229b4f8c15
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4802144
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
This commit is contained in:
Alex Rudenko
2023-08-23 08:11:20 +00:00
committed by Devtools-frontend LUCI CQ
parent 108da102fe
commit eb44342c2a
21 changed files with 288 additions and 4 deletions
+6
View File
@@ -499,6 +499,8 @@ grd_files_release_sources = [
"front_end/panels/emulation/emulation-meta.js",
"front_end/panels/emulation/emulation.js",
"front_end/panels/event_listeners/event_listeners.js",
"front_end/panels/explain/explain-meta.js",
"front_end/panels/explain/explain.js",
"front_end/panels/issues/components/components.js",
"front_end/panels/issues/issues-meta.js",
"front_end/panels/issues/issues.js",
@@ -1241,6 +1243,10 @@ grd_files_debug_sources = [
"front_end/panels/event_listeners/EventListenersUtils.js",
"front_end/panels/event_listeners/EventListenersView.js",
"front_end/panels/event_listeners/eventListenersView.css.js",
"front_end/panels/explain/ActionDelegate.js",
"front_end/panels/explain/Bindings.js",
"front_end/panels/explain/ContextMenuProvider.js",
"front_end/panels/explain/ExplainPopover.js",
"front_end/panels/issues/AffectedBlockedByResponseView.js",
"front_end/panels/issues/AffectedCookiesView.js",
"front_end/panels/issues/AffectedDirectivesView.js",
@@ -449,6 +449,8 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
async initialTargetId(): Promise<string|null> {
return null;
}
explainCode: unknown;
}
// @ts-ignore Global injected by devtools-compatibility.js
@@ -300,6 +300,9 @@ export interface InspectorFrontendHostAPI {
setAddExtensionCallback(callback: (arg0: ExtensionDescriptor) => void): void;
initialTargetId(): Promise<string|null>;
// TODO: update the binding name.
explainCode?: unknown;
}
export interface ContextMenuDescriptor {
+1
View File
@@ -21,6 +21,7 @@ devtools_entrypoint("shell") {
"../../panels/console:meta",
"../../panels/console_counters:meta",
"../../panels/coverage:meta",
"../../panels/explain:meta",
"../../panels/profiler:meta",
"../../panels/protocol_monitor:meta",
"../../panels/settings:meta",
+1
View File
@@ -6,6 +6,7 @@ import '../../Images/Images.js';
import '../../core/root/root-legacy.js';
import '../../core/dom_extension/dom_extension.js';
import '../../panels/sources/sources-meta.js';
import '../../panels/explain/explain-meta.js';
import '../../panels/profiler/profiler-meta.js';
import '../../panels/console/console-meta.js';
import '../../panels/coverage/coverage-meta.js';
+1
View File
@@ -42,6 +42,7 @@ devtools_entrypoint("bundle") {
"../../panels/changes/*",
"../../panels/coverage/*",
"../../panels/emulation/*",
"../../panels/explain/*",
"../../panels/lighthouse/*",
"../../panels/network/*",
"../../panels/profiler/*",
@@ -0,0 +1,36 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as UI from '../../ui/legacy/legacy.js';
import * as Sources from '../sources/sources.js';
import { ExplainPopover } from './ExplainPopover.js';
let actionDelegateInstance: ActionDelegate;
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): ActionDelegate {
const {forceNew} = opts;
if (!actionDelegateInstance || forceNew) {
actionDelegateInstance = new ActionDelegate();
}
return actionDelegateInstance;
}
handleAction(_context: UI.Context.Context, actionId: string): boolean {
switch (actionId) {
case 'explain.code': {
const frame = UI.Context.Context.instance().flavor(Sources.UISourceCodeFrame.UISourceCodeFrame);
if (frame) {
new ExplainPopover(frame);
return true;
}
}
}
return false;
}
}
+56
View File
@@ -0,0 +1,56 @@
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("../../../scripts/build/ninja/devtools_entrypoint.gni")
import("../../../scripts/build/ninja/devtools_module.gni")
import("../../../scripts/build/ninja/generate_css.gni")
import("../visibility.gni")
devtools_module("explain") {
sources = [
"ActionDelegate.ts",
"Bindings.ts",
"ContextMenuProvider.ts",
"ExplainPopover.ts",
]
deps = [
"../../core/host:bundle",
"../../third_party/marked:bundle",
"../../ui/components/markdown_view:bundle",
"../../ui/legacy:bundle",
"../../ui/legacy/components/source_frame:bundle",
"../sources:bundle",
]
}
devtools_entrypoint("bundle") {
entrypoint = "explain.ts"
deps = [ ":explain" ]
visibility = [
":*",
"../../../test/unittests/*",
"../../../test/unittests/front_end/panels/emulation/*",
"../../entrypoints/*",
"../../legacy_test_runner/*",
]
visibility += devtools_panels_visibility
}
devtools_entrypoint("meta") {
entrypoint = "explain-meta.ts"
deps = [
":bundle",
"../../core/i18n:bundle",
"../../models/workspace:bundle",
"../../ui/legacy:bundle",
"../sources:bundle",
]
visibility = [ "../../entrypoints/*" ]
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Host from '../../core/host/host.js';
export function isAvailable(): boolean {
return Boolean(Host.InspectorFrontendHost.InspectorFrontendHostInstance.explainCode);
}
@@ -0,0 +1,11 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as UI from '../../ui/legacy/legacy.js';
export class ContextMenuProvider implements UI.ContextMenu.Provider {
appendApplicableItems(_event: Event, contextMenu: UI.ContextMenu.ContextMenu, _target: Object): void {
contextMenu.debugSection().appendAction('explain.code');
}
}
@@ -0,0 +1,40 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as UI from '../../ui/legacy/legacy.js';
import type * as Sources from '../sources/sources.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as MarkdownView from '../../ui/components/markdown_view/markdown_view.js';
import * as Marked from '../../third_party/marked/marked.js';
export class ExplainPopover {
constructor(frame: Sources.UISourceCodeFrame.UISourceCodeFrame) {
const {state: editorState} = frame.textEditor;
const selectionRange = editorState.selection.main;
const leftCorner = frame.textEditor.editor.coordsAtPos(selectionRange.from);
const rightCorner = frame.textEditor.editor.coordsAtPos(selectionRange.to);
if (!leftCorner || !rightCorner) {
return;
}
const box = new AnchorBox(leftCorner.left, leftCorner.top - 2, rightCorner.right - leftCorner.left, rightCorner.bottom - leftCorner.top);
const popover = UI.PopoverHelper.PopoverHelper.createPopover();
popover.contentElement.classList.toggle('has-padding', false);
popover.contentElement.addEventListener('mousemove', () => {}, true);
popover.contentElement.addEventListener('mouseout', () => {}, true);
const div = document.createElement('div');
const ouput = 'This code creates a class called `PageHandlerPendingReceiver`. The constructor takes a handle as input and stores it in the `handle` property. The `bindInBrowser` method takes a scope as input and uses it to bind the `handle` to the interface `ntp.history_clusters.mojom.PageHandler`.';
const component = new MarkdownView.MarkdownView.MarkdownView();
component.data = {tokens: Marked.Marked.lexer(ouput)};
div.append(component);
div.style.margin = '16px';
div.style.maxWidth = ' 400px';
popover.contentElement.classList.toggle('has-padding', false);
popover.contentElement.appendChild(div);
popover.setContentAnchorBox(box);
popover.show(document);
frame.addEventListener(SourceFrame.SourceFrame.Events.EditorScroll, () => {
popover.hide();
}, this);
}
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Workspace from '../../models/workspace/workspace.js';
import type * as Explain from './explain.js';
import type * as Sources from '../sources/sources.js';
import {isAvailable} from './Bindings.js';
const UIStrings = {
/**
*@description Title of an action in the debugger tool to explain selection.
*/
explainCode: '✨ Explain selected code',
};
const str_ = i18n.i18n.registerUIStrings('panels/explain/explain-meta.ts', UIStrings);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
let loadedExplainModule: (typeof Explain|undefined);
async function loadExplainModule(): Promise<typeof Explain> {
if (!loadedExplainModule) {
loadedExplainModule = await import('./explain.js');
}
return loadedExplainModule;
}
let loadedSourcesModule: (typeof Sources|undefined);
function maybeRetrieveContextTypes<T = unknown>(getClassCallBack: (sourcesModule: typeof Sources) => T[]): T[] {
if (loadedSourcesModule === undefined) {
return [];
}
return getClassCallBack(loadedSourcesModule);
}
if (isAvailable()) {
UI.ActionRegistration.registerActionExtension({
actionId: 'explain.code',
category: UI.ActionRegistration.ActionCategory.EXPLAIN,
async loadActionDelegate() {
const Explain = await loadExplainModule();
return Explain.ActionDelegate.instance();
},
title: i18nLazyString(UIStrings.explainCode),
contextTypes() {
return maybeRetrieveContextTypes(Sources => [Sources.UISourceCodeFrame.UISourceCodeFrame]);
},
});
UI.ContextMenu.registerProvider({
contextTypes() {
return [
Workspace.UISourceCode.UISourceCode,
];
},
async loadProvider() {
const Explain = await loadExplainModule();
return new Explain.ContextMenuProvider();
},
});
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export * from './ExplainPopover.js';
export * from './ActionDelegate.js';
export * from './Bindings.js';
export * from './ContextMenuProvider.js';
+1
View File
@@ -107,6 +107,7 @@ devtools_entrypoint("bundle") {
"../../../test/unittests/front_end/entrypoints/missing_entrypoints/*",
"../../../test/unittests/front_end/panels/sources/*",
"../../entrypoints/*",
"../../panels/explain/*",
"../../panels/network/*",
# TODO(crbug.com/1202788): Remove invalid dependents
+1
View File
@@ -25,6 +25,7 @@ devtools_entrypoint("bundle") {
visibility = [
"../../../test/unittests/front_end/ui/components/*",
"../../models/issues_manager/*",
"../../panels/explain/*",
"../../ui/components/docs/markdown_view/*",
"../../ui/components/markdown_view/*",
]
@@ -299,6 +299,7 @@ export enum ActionCategory {
DEBUGGER = 'DEBUGGER',
SOURCES = 'SOURCES',
RENDERING = 'RENDERING',
EXPLAIN = 'EXPLAIN',
}
export function getLocalizedActionCategory(category: ActionCategory): Platform.UIString.LocalizedString {
+8 -4
View File
@@ -32,6 +32,13 @@ import {GlassPane, MarginBehavior, SizeBehavior} from './GlassPane.js';
import popoverStyles from './popover.css.legacy.js';
export class PopoverHelper {
static createPopover = (): GlassPane => {
const popover = new GlassPane();
popover.registerRequiredCSS(popoverStyles);
popover.setSizeBehavior(SizeBehavior.MeasureContent);
popover.setMarginBehavior(MarginBehavior.Arrow);
return popover;
};
private disableOnClick: boolean;
private hasPadding: boolean;
private getRequest: (arg0: MouseEvent) => PopoverRequest | null;
@@ -193,10 +200,7 @@ export class PopoverHelper {
}
private showPopover(document: Document): void {
const popover = new GlassPane();
popover.registerRequiredCSS(popoverStyles);
popover.setSizeBehavior(SizeBehavior.MeasureContent);
popover.setMarginBehavior(MarginBehavior.Arrow);
const popover = PopoverHelper.createPopover();
const request = this.scheduledRequest;
if (!request) {
return;
@@ -62,6 +62,7 @@ devtools_entrypoint("bundle") {
"../../../../../test/unittests/front_end/entrypoints/meta/*",
"../../../../panels/application/*",
"../../../../panels/coverage/*",
"../../../../panels/explain/*",
"../../../../panels/network/*",
"../../../../panels/protocol_monitor/*",
"../../../../panels/sources/*",
+1
View File
@@ -48,6 +48,7 @@ group("front_end") {
"panels/css_overview",
"panels/elements",
"panels/emulation",
"panels/explain",
"panels/issues",
"panels/layers",
"panels/lighthouse",
@@ -0,0 +1,15 @@
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("../../../../../third_party/typescript/typescript.gni")
ts_library("explain") {
testonly = true
sources = [ "explain_test.ts" ]
deps = [
"../../../../../front_end/panels/explain:bundle",
"../../helpers",
]
}
@@ -0,0 +1,22 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Explain from '../../../../../front_end/panels/explain/explain.js';
import * as Host from '../../../../../front_end/core/host/host.js';
const {assert} = chai;
describe('Explain', () => {
it('should be available if bindings are defined', async () => {
const original = Host.InspectorFrontendHost.InspectorFrontendHostInstance.explainCode;
try {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.explainCode = undefined;
assert(!Explain.isAvailable());
Host.InspectorFrontendHost.InspectorFrontendHostInstance.explainCode = () => {};
assert(Explain.isAvailable());
} finally {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.explainCode = original;
}
});
});