diff --git a/front_end/panels/sources/BreakpointsSidebarPane.ts b/front_end/panels/sources/BreakpointsSidebarPane.ts index b44b100550..2e319dd0da 100644 --- a/front_end/panels/sources/BreakpointsSidebarPane.ts +++ b/front_end/panels/sources/BreakpointsSidebarPane.ts @@ -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 { - 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(); 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 { + 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 { 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 { const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails); if (details && details.callFrames.length) { diff --git a/test/unittests/front_end/panels/sources/BreakpointsSidebarPane_test.ts b/test/unittests/front_end/panels/sources/BreakpointsSidebarPane_test.ts index e40f12e3b9..38c546407b 100644 --- a/test/unittests/front_end/panels/sources/BreakpointsSidebarPane_test.ts +++ b/test/unittests/front_end/panels/sources/BreakpointsSidebarPane_test.ts @@ -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; + 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 = [