mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
The source editor, when it cannot load a particular file, will simply display a blank file instead. At the bottom, this is because the APIs used to load the files do not have a way to propagate an error up to the call site. Rather, the callback either is never called, or is called with just an empty string. This refactor changes the way the project system loads file contents by replacing the callback model with a Promise-based model. However, in this version, rather than propagating an error (handled via catch), the error is exposed as a property on the object passed via the the load functions. Although it might be preferable to use async throw/catch, because there are ~4-5 layers of redirection through the project system, the added complexity seems to not really justify that work. I'm open to reconsidering this design, though. Attempting to load a file via file:// which does not exist previously produced no error because the DevToolsUIBindings handler would just always resolve with no content and HTTP status 200. I had previously addressed that bug in this changeset, but I've split it out to https://chromium-review.googlesource.com/c/chromium/src/+/1847833 . Sample "after" screenshot: https://imgur.com/a/tlm90sg Bug: 961940 Bug: 941035 Change-Id: If121611090e9c35eeb1de162b59f8a9f72f696d9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1817677 Reviewed-by: Lorne Mitchell <lomitch@microsoft.com> Reviewed-by: Jeff Fisher <jeffish@microsoft.com> Commit-Queue: Lorne Mitchell <lomitch@microsoft.com> Cr-Original-Commit-Position: refs/heads/master@{#705438} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 61ec2a233d9a7bf4bb4b3e610de6fc233ae0d46e
259 lines
11 KiB
JavaScript
259 lines
11 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 {!Platform.Multimap<string, string>} */
|
|
const breakpointEntriesForLine = new Platform.Multimap();
|
|
|
|
/** @type {!Platform.Multimap<string, !{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UILocation}>} */
|
|
const locationForEntry = new Platform.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).trimEndWithMaxLength(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');
|