Files
react-native-devtools-frontend/front_end/sources/JavaScriptBreakpointsSidebarPane.js
T
Joel Einbinder 0745b105f2 DevTools: Show columns in the JavaScript breakpoint sidebar
Multiple active inline breakpoints now each get their own entry in
the breakpoints sidebar. The column is shown for every breakpoint after
the first that share a line.

https://i.imgur.com/BoLyzYv.png

Bug: 927961

Change-Id: If07ca26b177bf8fa82c7bf969397101485210c01
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1449152
Commit-Queue: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#647509}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 43005f59158e79ce01f4bfa77ea52b9732fc7a4d
2019-04-03 22:43:58 +00:00

250 lines
10 KiB
JavaScript

// Copyright (c) 2015 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.
/**
* @implements {UI.ContextFlavorListener}
* @unrestricted
*/
Sources.JavaScriptBreakpointsSidebarPane = class extends UI.ThrottledWidget {
constructor() {
super(true);
this.registerRequiredCSS('sources/javaScriptBreakpointsSidebarPane.css');
this._breakpointManager = Bindings.breakpointManager;
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointAdded, this.update, this);
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointRemoved, this.update, this);
Common.moduleSetting('breakpointsActive').addChangeListener(this.update, this);
/** @type {?Element} */
this._listElement = null;
this.update();
}
/**
* @override
* @return {!Promise<?>}
*/
doUpdate() {
const breakpointLocations = this._breakpointManager.allBreakpointLocations().filter(
breakpointLocation =>
breakpointLocation.uiLocation.uiSourceCode.project().type() !== Workspace.projectTypes.Debugger);
if (!breakpointLocations.length) {
this._listElement = null;
this.contentElement.removeChildren();
const emptyElement = this.contentElement.createChild('div', 'gray-info-message');
emptyElement.textContent = Common.UIString('No breakpoints');
this.contentElement.appendChild(emptyElement);
this._didUpdateForTest();
return Promise.resolve();
}
if (!this._listElement) {
this.contentElement.removeChildren();
this._listElement = this.contentElement.createChild('div');
this.contentElement.appendChild(this._listElement);
}
breakpointLocations.sort((item1, item2) => item1.uiLocation.compareTo(item2.uiLocation));
/** @type {!Multimap<string, string>} */
const breakpointEntriesForLine = new Multimap();
/** @type {!Multimap<string, !{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UILocation}>} */
const locationForEntry = new Multimap();
for (const breakpointLocation of breakpointLocations) {
const uiLocation = breakpointLocation.uiLocation;
const entryDescriptor = `${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}:${uiLocation.columnNumber}`;
locationForEntry.set(entryDescriptor, breakpointLocation);
const lineDescriptor = `${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}`;
breakpointEntriesForLine.set(lineDescriptor, entryDescriptor);
}
const details = UI.context.flavor(SDK.DebuggerPausedDetails);
const selectedUILocation = details && details.callFrames.length ?
Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(details.callFrames[0].location()) :
null;
let shouldShowView = false;
let entry = this._listElement.firstChild;
const promises = [];
for (const descriptor of locationForEntry.keysArray()) {
if (!entry) {
entry = this._listElement.createChild('div', 'breakpoint-entry');
entry.addEventListener('contextmenu', this._breakpointContextMenu.bind(this), true);
entry.addEventListener('click', this._revealLocation.bind(this), false);
const checkboxLabel = UI.CheckboxLabel.create('');
checkboxLabel.addEventListener('click', this._breakpointCheckboxClicked.bind(this), false);
entry.appendChild(checkboxLabel);
entry[Sources.JavaScriptBreakpointsSidebarPane._checkboxLabelSymbol] = checkboxLabel;
const snippetElement = entry.createChild('div', 'source-text monospace');
entry[Sources.JavaScriptBreakpointsSidebarPane._snippetElementSymbol] = snippetElement;
}
const locations = Array.from(locationForEntry.get(descriptor));
const uiLocation = locations[0].uiLocation;
const isSelected =
!!selectedUILocation && locations.some(location => location.uiLocation.id() === selectedUILocation.id());
const hasEnabled = locations.some(location => location.breakpoint.enabled());
const hasDisabled = locations.some(location => !location.breakpoint.enabled());
const showCoumn =
breakpointEntriesForLine.get(`${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}`).size > 1;
promises.push(
this._resetEntry(/** @type {!Element}*/ (entry), uiLocation, isSelected, hasEnabled, hasDisabled, showCoumn));
entry[Sources.JavaScriptBreakpointsSidebarPane._breakpointLocationsSymbol] = locations;
if (isSelected)
shouldShowView = true;
entry = entry.nextSibling;
}
while (entry) {
const next = entry.nextSibling;
entry.remove();
entry = next;
}
if (shouldShowView)
UI.viewManager.showView('sources.jsBreakpoints');
this._listElement.classList.toggle(
'breakpoints-list-deactivated', !Common.moduleSetting('breakpointsActive').get());
return Promise.all(promises).then(() => this._didUpdateForTest());
}
/**
* @param {!Element} element
* @param {!Workspace.UILocation} uiLocation
* @param {boolean} isSelected
* @param {boolean} hasEnabled
* @param {boolean} hasDisabled
* @param {boolean} showColumn
* @return {!Promise}
*/
async _resetEntry(element, uiLocation, isSelected, hasEnabled, hasDisabled, showColumn) {
element[Sources.JavaScriptBreakpointsSidebarPane._locationSymbol] = uiLocation;
element.classList.toggle('breakpoint-hit', isSelected);
const checkboxLabel = element[Sources.JavaScriptBreakpointsSidebarPane._checkboxLabelSymbol];
checkboxLabel.textElement.textContent =
uiLocation.linkText() + (showColumn ? ':' + (uiLocation.columnNumber + 1) : '');
checkboxLabel.checkboxElement.checked = hasEnabled;
checkboxLabel.checkboxElement.indeterminate = hasEnabled && hasDisabled;
const snippetElement = element[Sources.JavaScriptBreakpointsSidebarPane._snippetElementSymbol];
const content = await uiLocation.uiSourceCode.requestContent();
const lineNumber = uiLocation.lineNumber;
const text = new TextUtils.Text(content || '');
if (lineNumber < text.lineCount()) {
const lineText = text.lineAt(lineNumber);
const maxSnippetLength = 200;
snippetElement.textContent =
lineText.substring(showColumn ? uiLocation.columnNumber : 0).trimEnd(maxSnippetLength);
}
}
/**
* @param {!Event} event
* @return {!Array<!Bindings.BreakpointManager.BreakpointLocation>}
*/
_breakpointLocations(event) {
const node = event.target.enclosingNodeOrSelfWithClass('breakpoint-entry');
if (!node)
return [];
return node[Sources.JavaScriptBreakpointsSidebarPane._breakpointLocationsSymbol] || [];
}
/**
* @param {!Event} event
*/
_breakpointCheckboxClicked(event) {
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
const newState = event.target.checkboxElement.checked;
for (const breakpoint of breakpoints)
breakpoint.setEnabled(newState);
event.consume();
}
/**
* @param {!Event} event
*/
_revealLocation(event) {
const uiLocations = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.uiLocation);
let uiLocation = null;
for (const uiLocationCandidate of uiLocations) {
if (!uiLocation || uiLocationCandidate.columnNumber < uiLocation.columnNumber)
uiLocation = uiLocationCandidate;
}
if (uiLocation)
Common.Revealer.reveal(uiLocation);
}
/**
* @param {!Event} event
*/
_breakpointContextMenu(event) {
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
const contextMenu = new UI.ContextMenu(event);
const removeEntryTitle = breakpoints.length > 1 ? Common.UIString('Remove all breakpoints in line') :
Common.UIString('Remove breakpoint');
contextMenu.defaultSection().appendItem(
removeEntryTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false /* keepInStorage */)));
const breakpointActive = Common.moduleSetting('breakpointsActive').get();
const breakpointActiveTitle =
breakpointActive ? Common.UIString('Deactivate breakpoints') : Common.UIString('Activate breakpoints');
contextMenu.defaultSection().appendItem(
breakpointActiveTitle, () => Common.moduleSetting('breakpointsActive').set(!breakpointActive));
if (breakpoints.some(breakpoint => !breakpoint.enabled())) {
const enableTitle = Common.UIString('Enable all breakpoints');
contextMenu.defaultSection().appendItem(enableTitle, this._toggleAllBreakpoints.bind(this, true));
}
if (breakpoints.some(breakpoint => breakpoint.enabled())) {
const disableTitle = Common.UIString('Disable all breakpoints');
contextMenu.defaultSection().appendItem(disableTitle, this._toggleAllBreakpoints.bind(this, false));
}
const removeAllTitle = Common.UIString('Remove all breakpoints');
contextMenu.defaultSection().appendItem(removeAllTitle, this._removeAllBreakpoints.bind(this));
const removeOtherTitle = Common.UIString('Remove other breakpoints');
contextMenu.defaultSection().appendItem(
removeOtherTitle, this._removeOtherBreakpoints.bind(this, new Set(breakpoints)));
contextMenu.show();
}
/**
* @param {boolean} toggleState
*/
_toggleAllBreakpoints(toggleState) {
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations())
breakpointLocation.breakpoint.setEnabled(toggleState);
}
_removeAllBreakpoints() {
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations())
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
}
/**
* @param {!Set<!Bindings.BreakpointManager.Breakpoint>} selectedBreakpoints
*/
_removeOtherBreakpoints(selectedBreakpoints) {
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
if (!selectedBreakpoints.has(breakpointLocation.breakpoint))
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
}
}
/**
* @override
* @param {?Object} object
*/
flavorChanged(object) {
this.update();
}
_didUpdateForTest() {
}
};
Sources.JavaScriptBreakpointsSidebarPane._locationSymbol = Symbol('location');
Sources.JavaScriptBreakpointsSidebarPane._checkboxLabelSymbol = Symbol('checkbox-label');
Sources.JavaScriptBreakpointsSidebarPane._snippetElementSymbol = Symbol('snippet-element');
Sources.JavaScriptBreakpointsSidebarPane._breakpointLocationsSymbol = Symbol('locations');