[BreakpointsSidebarPane] Add listeners for interacting with sidebar

This adds listeners to the BreakpointsSidebarPane for jump to source
and enabling/disabling the checkbox, such that interacting with the
sidebar actually propagates the action to the SourcesPanel (jump)/
BreakpointManager (enable/disable).

This is still behind an experiment.

Screenshot: https://imgur.com/a/CT0JqLA
Doc: https://bit.ly/3pa4y92
Bug: 1324904, 1346231
Change-Id: Ic620d1c24cef465c81ffd77fb06a425752971555
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3899252
Reviewed-by: Jaroslav Sevcik <jarin@chromium.org>
Commit-Queue: Kim-Anh Tran <kimanh@chromium.org>
This commit is contained in:
Kim-Anh Tran
2022-09-16 13:39:18 +00:00
committed by Devtools-frontend LUCI CQ
parent 9a6f08ad08
commit fd2ae9a89d
2 changed files with 109 additions and 2 deletions
@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
@@ -16,6 +18,7 @@ let breakpointsViewControllerInstance: BreakpointsSidebarController;
export class BreakpointsSidebarPane extends UI.ThrottledWidget.ThrottledWidget {
readonly #breakpointsView: SourcesComponents.BreakpointsView.BreakpointsView;
readonly #controller: BreakpointsSidebarController;
static instance(): BreakpointsSidebarPane {
if (!breakpointsSidebarPaneInstance) {
@@ -26,22 +29,46 @@ export class BreakpointsSidebarPane extends UI.ThrottledWidget.ThrottledWidget {
constructor() {
super(true);
this.#controller = BreakpointsSidebarController.instance();
this.#breakpointsView = new SourcesComponents.BreakpointsView.BreakpointsView();
this.#breakpointsView.addEventListener(
SourcesComponents.BreakpointsView.CheckboxToggledEvent.eventName, (event: Event) => {
this.#onCheckBoxToggledEvent(event);
});
this.#breakpointsView.addEventListener(
SourcesComponents.BreakpointsView.BreakpointSelectedEvent.eventName, (event: Event) => {
this.#onBreakpointSelectedEvent(event);
});
this.contentElement.appendChild(this.#breakpointsView);
this.update();
}
async doUpdate(): Promise<void> {
await BreakpointsSidebarController.instance().update();
await this.#controller.update();
}
set data(data: SourcesComponents.BreakpointsView.BreakpointsViewData) {
this.#breakpointsView.data = data;
}
#onCheckBoxToggledEvent(event: Event): void {
const checkboxToggledEvent = event as SourcesComponents.BreakpointsView.CheckboxToggledEvent;
const {breakpointItem, checked} = checkboxToggledEvent.data;
this.#controller.breakpointStateChanged(breakpointItem, checked);
}
#onBreakpointSelectedEvent(event: Event): void {
const breakpointSelectedEvent = event as SourcesComponents.BreakpointsView.BreakpointSelectedEvent;
const breakpointItem = breakpointSelectedEvent.data.breakpointItem;
void this.#controller.jumpToSource(breakpointItem);
}
}
export class BreakpointsSidebarController implements UI.ContextFlavorListener.ContextFlavorListener {
readonly #breakpointManager: Bindings.BreakpointManager.BreakpointManager;
readonly #breakpointItemToLocationMap =
new WeakMap<SourcesComponents.BreakpointsView.BreakpointItem, Bindings.BreakpointManager.BreakpointLocation[]>();
constructor() {
this.#breakpointManager = Bindings.BreakpointManager.BreakpointManager.instance();
@@ -60,6 +87,27 @@ export class BreakpointsSidebarController implements UI.ContextFlavorListener.Co
void this.update();
}
breakpointStateChanged(breakpointItem: SourcesComponents.BreakpointsView.BreakpointItem, checked: boolean): void {
const locations = this.#getLocationsForBreakpointItem(breakpointItem);
locations.forEach((value: Bindings.BreakpointManager.BreakpointLocation) => {
const breakpoint = value.breakpoint;
breakpoint.setEnabled(checked);
});
}
async jumpToSource(breakpointItem: SourcesComponents.BreakpointsView.BreakpointItem): Promise<void> {
const uiLocations = this.#getLocationsForBreakpointItem(breakpointItem).map(location => location.uiLocation);
let uiLocation: Workspace.UISourceCode.UILocation|undefined;
for (const uiLocationCandidate of uiLocations) {
if (!uiLocation || uiLocationCandidate.compareTo(uiLocation) < 0) {
uiLocation = uiLocationCandidate;
}
}
if (uiLocation) {
await Common.Revealer.reveal(uiLocation);
}
}
async update(): Promise<void> {
const data = await this.getUpdatedBreakpointViewData();
BreakpointsSidebarPane.instance().data = data;
@@ -98,6 +146,7 @@ export class BreakpointsSidebarController implements UI.ContextFlavorListener.Co
const status: SourcesComponents.BreakpointsView.BreakpointStatus = this.#getBreakpointState(locations);
const item = {location: locationText, codeSnippet, isHit, status} as
SourcesComponents.BreakpointsView.BreakpointItem;
this.#breakpointItemToLocationMap.set(item, locations);
let group = urlToGroup.get(sourceURL);
if (group) {
@@ -112,6 +161,13 @@ export class BreakpointsSidebarController implements UI.ContextFlavorListener.Co
return {groups: Array.from(urlToGroup.values())};
}
#getLocationsForBreakpointItem(breakpointItem: SourcesComponents.BreakpointsView.BreakpointItem):
Bindings.BreakpointManager.BreakpointLocation[] {
const locations = this.#breakpointItemToLocationMap.get(breakpointItem);
assertNotNullOrUndefined(locations);
return locations;
}
async #getHitUILocation(): Promise<Workspace.UISourceCode.UILocation|null> {
const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails);
if (details && details.callFrames.length) {
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../../../../front_end/core/common/common.js';
import * as Bindings from '../../../../../front_end/models/bindings/bindings.js';
import * as Workspace from '../../../../../front_end/models/workspace/workspace.js';
import * as SourcesComponents from '../../../../../front_end/panels/sources/components/components.js';
@@ -48,6 +49,56 @@ function createBreakpointLocations(testData: LocationTestData[]): Bindings.Break
}
describeWithEnvironment('BreakpointsSidebarController', () => {
it('changes breakpoint state', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
];
initializeBreakpointManagerWithMockdata(testData);
const controller = Sources.BreakpointsSidebarPane.BreakpointsSidebarController.instance({forceNew: true});
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 1);
const breakpointItem = data.groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.status, SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED);
const locations = Bindings.BreakpointManager.BreakpointManager.instance().allBreakpointLocations();
assert.lengthOf(locations, 1);
const breakpoint = locations[0].breakpoint as sinon.SinonStubbedInstance<Bindings.BreakpointManager.Breakpoint>;
controller.breakpointStateChanged(breakpointItem, false);
assert.isTrue(breakpoint.setEnabled.calledWith(false));
});
it('triggers a jump to source', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
];
initializeBreakpointManagerWithMockdata(testData);
const controller = Sources.BreakpointsSidebarPane.BreakpointsSidebarController.instance({forceNew: true});
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 1);
const breakpointItem = data.groups[0].breakpointItems[0];
Common.Revealer.registerRevealer({
contextTypes() {
return [
Workspace.UISourceCode.UILocation,
];
},
destination: Common.Revealer.RevealerDestination.SOURCES_PANEL,
async loadRevealer() {
return Sources.SourcesPanel.UILocationRevealer.instance();
},
});
const revealStub = sinon.stub(Sources.SourcesPanel.UILocationRevealer.instance(), 'reveal');
await controller.jumpToSource(breakpointItem);
assert.isTrue(revealStub.called);
});
describe('getUpdatedBreakpointViewData', () => {
it('extracts breakpoint data', async () => {
const testData = [