| // 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'); |