From eb44342c2a8d76ba457d507966103102bb9dbd78 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 22 Aug 2023 17:01:11 +0200 Subject: [PATCH] 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 Reviewed-by: Danil Somsikov --- config/gni/devtools_grd_files.gni | 6 ++ front_end/core/host/InspectorFrontendHost.ts | 2 + .../core/host/InspectorFrontendHostAPI.ts | 3 + front_end/entrypoints/shell/BUILD.gn | 1 + front_end/entrypoints/shell/shell.ts | 1 + front_end/models/workspace/BUILD.gn | 1 + front_end/panels/explain/ActionDelegate.ts | 36 +++++++++++ front_end/panels/explain/BUILD.gn | 56 ++++++++++++++++ front_end/panels/explain/Bindings.ts | 9 +++ .../panels/explain/ContextMenuProvider.ts | 11 ++++ front_end/panels/explain/ExplainPopover.ts | 40 ++++++++++++ front_end/panels/explain/explain-meta.ts | 64 +++++++++++++++++++ front_end/panels/explain/explain.ts | 8 +++ front_end/panels/sources/BUILD.gn | 1 + front_end/third_party/marked/BUILD.gn | 1 + front_end/ui/legacy/ActionRegistration.ts | 1 + front_end/ui/legacy/PopoverHelper.ts | 12 ++-- .../legacy/components/source_frame/BUILD.gn | 1 + test/unittests/front_end/BUILD.gn | 1 + .../front_end/panels/explain/BUILD.gn | 15 +++++ .../front_end/panels/explain/explain_test.ts | 22 +++++++ 21 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 front_end/panels/explain/ActionDelegate.ts create mode 100644 front_end/panels/explain/BUILD.gn create mode 100644 front_end/panels/explain/Bindings.ts create mode 100644 front_end/panels/explain/ContextMenuProvider.ts create mode 100644 front_end/panels/explain/ExplainPopover.ts create mode 100644 front_end/panels/explain/explain-meta.ts create mode 100644 front_end/panels/explain/explain.ts create mode 100644 test/unittests/front_end/panels/explain/BUILD.gn create mode 100644 test/unittests/front_end/panels/explain/explain_test.ts diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 31ebf20d5c..ec22c95bb9 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -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", diff --git a/front_end/core/host/InspectorFrontendHost.ts b/front_end/core/host/InspectorFrontendHost.ts index f477627aba..03904d8932 100644 --- a/front_end/core/host/InspectorFrontendHost.ts +++ b/front_end/core/host/InspectorFrontendHost.ts @@ -449,6 +449,8 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI { async initialTargetId(): Promise { return null; } + + explainCode: unknown; } // @ts-ignore Global injected by devtools-compatibility.js diff --git a/front_end/core/host/InspectorFrontendHostAPI.ts b/front_end/core/host/InspectorFrontendHostAPI.ts index 71b71e1d13..7f3222d5e4 100644 --- a/front_end/core/host/InspectorFrontendHostAPI.ts +++ b/front_end/core/host/InspectorFrontendHostAPI.ts @@ -300,6 +300,9 @@ export interface InspectorFrontendHostAPI { setAddExtensionCallback(callback: (arg0: ExtensionDescriptor) => void): void; initialTargetId(): Promise; + + // TODO: update the binding name. + explainCode?: unknown; } export interface ContextMenuDescriptor { diff --git a/front_end/entrypoints/shell/BUILD.gn b/front_end/entrypoints/shell/BUILD.gn index 72617d7036..eb06389797 100644 --- a/front_end/entrypoints/shell/BUILD.gn +++ b/front_end/entrypoints/shell/BUILD.gn @@ -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", diff --git a/front_end/entrypoints/shell/shell.ts b/front_end/entrypoints/shell/shell.ts index b22591ee60..10580db71c 100644 --- a/front_end/entrypoints/shell/shell.ts +++ b/front_end/entrypoints/shell/shell.ts @@ -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'; diff --git a/front_end/models/workspace/BUILD.gn b/front_end/models/workspace/BUILD.gn index e6d95fb30c..53f2d1f143 100644 --- a/front_end/models/workspace/BUILD.gn +++ b/front_end/models/workspace/BUILD.gn @@ -42,6 +42,7 @@ devtools_entrypoint("bundle") { "../../panels/changes/*", "../../panels/coverage/*", "../../panels/emulation/*", + "../../panels/explain/*", "../../panels/lighthouse/*", "../../panels/network/*", "../../panels/profiler/*", diff --git a/front_end/panels/explain/ActionDelegate.ts b/front_end/panels/explain/ActionDelegate.ts new file mode 100644 index 0000000000..ba2d073406 --- /dev/null +++ b/front_end/panels/explain/ActionDelegate.ts @@ -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; + } +} diff --git a/front_end/panels/explain/BUILD.gn b/front_end/panels/explain/BUILD.gn new file mode 100644 index 0000000000..68a999d0b8 --- /dev/null +++ b/front_end/panels/explain/BUILD.gn @@ -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/*" ] +} diff --git a/front_end/panels/explain/Bindings.ts b/front_end/panels/explain/Bindings.ts new file mode 100644 index 0000000000..20e2187039 --- /dev/null +++ b/front_end/panels/explain/Bindings.ts @@ -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); +} diff --git a/front_end/panels/explain/ContextMenuProvider.ts b/front_end/panels/explain/ContextMenuProvider.ts new file mode 100644 index 0000000000..51f23d1cce --- /dev/null +++ b/front_end/panels/explain/ContextMenuProvider.ts @@ -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'); + } +} diff --git a/front_end/panels/explain/ExplainPopover.ts b/front_end/panels/explain/ExplainPopover.ts new file mode 100644 index 0000000000..053c5780d4 --- /dev/null +++ b/front_end/panels/explain/ExplainPopover.ts @@ -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); + } +} diff --git a/front_end/panels/explain/explain-meta.ts b/front_end/panels/explain/explain-meta.ts new file mode 100644 index 0000000000..efc5a55f7d --- /dev/null +++ b/front_end/panels/explain/explain-meta.ts @@ -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 { + if (!loadedExplainModule) { + loadedExplainModule = await import('./explain.js'); + } + return loadedExplainModule; +} + +let loadedSourcesModule: (typeof Sources|undefined); +function maybeRetrieveContextTypes(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(); + }, + }); +} diff --git a/front_end/panels/explain/explain.ts b/front_end/panels/explain/explain.ts new file mode 100644 index 0000000000..8963177199 --- /dev/null +++ b/front_end/panels/explain/explain.ts @@ -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'; diff --git a/front_end/panels/sources/BUILD.gn b/front_end/panels/sources/BUILD.gn index c6417b512b..dc2eae5d3f 100644 --- a/front_end/panels/sources/BUILD.gn +++ b/front_end/panels/sources/BUILD.gn @@ -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 diff --git a/front_end/third_party/marked/BUILD.gn b/front_end/third_party/marked/BUILD.gn index d1dc458021..7587de9198 100644 --- a/front_end/third_party/marked/BUILD.gn +++ b/front_end/third_party/marked/BUILD.gn @@ -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/*", ] diff --git a/front_end/ui/legacy/ActionRegistration.ts b/front_end/ui/legacy/ActionRegistration.ts index 926829e0e9..dab5050e76 100644 --- a/front_end/ui/legacy/ActionRegistration.ts +++ b/front_end/ui/legacy/ActionRegistration.ts @@ -299,6 +299,7 @@ export enum ActionCategory { DEBUGGER = 'DEBUGGER', SOURCES = 'SOURCES', RENDERING = 'RENDERING', + EXPLAIN = 'EXPLAIN', } export function getLocalizedActionCategory(category: ActionCategory): Platform.UIString.LocalizedString { diff --git a/front_end/ui/legacy/PopoverHelper.ts b/front_end/ui/legacy/PopoverHelper.ts index b0a28c8699..6a226d9fa6 100644 --- a/front_end/ui/legacy/PopoverHelper.ts +++ b/front_end/ui/legacy/PopoverHelper.ts @@ -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; diff --git a/front_end/ui/legacy/components/source_frame/BUILD.gn b/front_end/ui/legacy/components/source_frame/BUILD.gn index 21469fd04c..d0e88c505b 100644 --- a/front_end/ui/legacy/components/source_frame/BUILD.gn +++ b/front_end/ui/legacy/components/source_frame/BUILD.gn @@ -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/*", diff --git a/test/unittests/front_end/BUILD.gn b/test/unittests/front_end/BUILD.gn index 5ea15eb3e6..eed3de22ac 100644 --- a/test/unittests/front_end/BUILD.gn +++ b/test/unittests/front_end/BUILD.gn @@ -48,6 +48,7 @@ group("front_end") { "panels/css_overview", "panels/elements", "panels/emulation", + "panels/explain", "panels/issues", "panels/layers", "panels/lighthouse", diff --git a/test/unittests/front_end/panels/explain/BUILD.gn b/test/unittests/front_end/panels/explain/BUILD.gn new file mode 100644 index 0000000000..1bbecb1362 --- /dev/null +++ b/test/unittests/front_end/panels/explain/BUILD.gn @@ -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", + ] +} diff --git a/test/unittests/front_end/panels/explain/explain_test.ts b/test/unittests/front_end/panels/explain/explain_test.ts new file mode 100644 index 0000000000..eceeddf8b3 --- /dev/null +++ b/test/unittests/front_end/panels/explain/explain_test.ts @@ -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; + } + }); +});