blob: 60b042f69808b76304bec808a68469907a80ceb1 [file] [log] [blame]
// 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');