| /* | |
| * Copyright (C) 2008 Apple Inc. All Rights Reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions | |
| * are met: | |
| * 1. Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * 2. Redistributions in binary form must reproduce the above copyright | |
| * notice, this list of conditions and the following disclaimer in the | |
| * documentation and/or other materials provided with the distribution. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY | |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR | |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| */ | |
| /** | |
| * @implements {UI.ContextFlavorListener} | |
| * @implements {UI.ListDelegate<!Sources.CallStackSidebarPane.Item>} | |
| * @unrestricted | |
| */ | |
| Sources.CallStackSidebarPane = class extends UI.SimpleView { | |
| constructor() { | |
| super(Common.UIString('Call Stack'), true); | |
| this.registerRequiredCSS('sources/callStackSidebarPane.css'); | |
| this._blackboxedMessageElement = this._createBlackboxedMessageElement(); | |
| this.contentElement.appendChild(this._blackboxedMessageElement); | |
| this._notPausedMessageElement = this.contentElement.createChild('div', 'gray-info-message'); | |
| this._notPausedMessageElement.textContent = Common.UIString('Not paused'); | |
| /** @type {!UI.ListModel<!Sources.CallStackSidebarPane.Item>} */ | |
| this._items = new UI.ListModel(); | |
| /** @type {!UI.ListControl<!Sources.CallStackSidebarPane.Item>} */ | |
| this._list = new UI.ListControl(this._items, this, UI.ListMode.NonViewport); | |
| this.contentElement.appendChild(this._list.element); | |
| this._list.element.addEventListener('contextmenu', this._onContextMenu.bind(this), false); | |
| this._list.element.addEventListener('click', this._onClick.bind(this), false); | |
| this._showMoreMessageElement = this._createShowMoreMessageElement(); | |
| this._showMoreMessageElement.classList.add('hidden'); | |
| this.contentElement.appendChild(this._showMoreMessageElement); | |
| this._showBlackboxed = false; | |
| this._locationPool = new Bindings.LiveLocationPool(); | |
| this._updateThrottler = new Common.Throttler(100); | |
| this._maxAsyncStackChainDepth = Sources.CallStackSidebarPane._defaultMaxAsyncStackChainDepth; | |
| this._update(); | |
| this._updateItemThrottler = new Common.Throttler(100); | |
| this._scheduledForUpdateItems = new Set(); | |
| } | |
| /** | |
| * @override | |
| * @param {?Object} object | |
| */ | |
| flavorChanged(object) { | |
| this._showBlackboxed = false; | |
| this._maxAsyncStackChainDepth = Sources.CallStackSidebarPane._defaultMaxAsyncStackChainDepth; | |
| this._update(); | |
| } | |
| _update() { | |
| this._updateThrottler.schedule(() => this._doUpdate()); | |
| } | |
| /** | |
| * @return {!Promise<undefined>} | |
| */ | |
| async _doUpdate() { | |
| this._locationPool.disposeAll(); | |
| const details = UI.context.flavor(SDK.DebuggerPausedDetails); | |
| if (!details) { | |
| this._notPausedMessageElement.classList.remove('hidden'); | |
| this._blackboxedMessageElement.classList.add('hidden'); | |
| this._showMoreMessageElement.classList.add('hidden'); | |
| this._items.replaceAll([]); | |
| UI.context.setFlavor(SDK.DebuggerModel.CallFrame, null); | |
| return; | |
| } | |
| let debuggerModel = details.debuggerModel; | |
| this._notPausedMessageElement.classList.add('hidden'); | |
| const items = details.callFrames.map(frame => { | |
| const item = Sources.CallStackSidebarPane.Item.createForDebuggerCallFrame( | |
| frame, this._locationPool, this._refreshItem.bind(this)); | |
| item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol] = frame; | |
| return item; | |
| }); | |
| let asyncStackTrace = details.asyncStackTrace; | |
| if (!asyncStackTrace && details.asyncStackTraceId) { | |
| if (details.asyncStackTraceId.debuggerId) { | |
| debuggerModel = SDK.DebuggerModel.modelForDebuggerId(details.asyncStackTraceId.debuggerId); | |
| } | |
| asyncStackTrace = debuggerModel ? await debuggerModel.fetchAsyncStackTrace(details.asyncStackTraceId) : null; | |
| } | |
| let peviousStackTrace = details.callFrames; | |
| let maxAsyncStackChainDepth = this._maxAsyncStackChainDepth; | |
| while (asyncStackTrace && maxAsyncStackChainDepth > 0) { | |
| let title = ''; | |
| const isAwait = asyncStackTrace.description === 'async function'; | |
| if (isAwait && peviousStackTrace.length && asyncStackTrace.callFrames.length) { | |
| const lastPreviousFrame = peviousStackTrace[peviousStackTrace.length - 1]; | |
| const lastPreviousFrameName = UI.beautifyFunctionName(lastPreviousFrame.functionName); | |
| title = UI.asyncStackTraceLabel('await in ' + lastPreviousFrameName); | |
| } else { | |
| title = UI.asyncStackTraceLabel(asyncStackTrace.description); | |
| } | |
| items.push(...Sources.CallStackSidebarPane.Item.createItemsForAsyncStack( | |
| title, debuggerModel, asyncStackTrace.callFrames, this._locationPool, this._refreshItem.bind(this))); | |
| --maxAsyncStackChainDepth; | |
| peviousStackTrace = asyncStackTrace.callFrames; | |
| if (asyncStackTrace.parent) { | |
| asyncStackTrace = asyncStackTrace.parent; | |
| } else if (asyncStackTrace.parentId) { | |
| if (asyncStackTrace.parentId.debuggerId) { | |
| debuggerModel = SDK.DebuggerModel.modelForDebuggerId(asyncStackTrace.parentId.debuggerId); | |
| } | |
| asyncStackTrace = debuggerModel ? await debuggerModel.fetchAsyncStackTrace(asyncStackTrace.parentId) : null; | |
| } else { | |
| asyncStackTrace = null; | |
| } | |
| } | |
| this._showMoreMessageElement.classList.toggle('hidden', !asyncStackTrace); | |
| this._items.replaceAll(items); | |
| if (this._maxAsyncStackChainDepth === Sources.CallStackSidebarPane._defaultMaxAsyncStackChainDepth) { | |
| this._list.selectNextItem(true /* canWrap */, false /* center */); | |
| } | |
| this._updatedForTest(); | |
| } | |
| _updatedForTest() { | |
| } | |
| /** | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| */ | |
| _refreshItem(item) { | |
| this._scheduledForUpdateItems.add(item); | |
| this._updateItemThrottler.schedule(innerUpdate.bind(this)); | |
| /** | |
| * @this {!Sources.CallStackSidebarPane} | |
| * @return {!Promise<undefined>} | |
| */ | |
| function innerUpdate() { | |
| const items = Array.from(this._scheduledForUpdateItems); | |
| this._scheduledForUpdateItems.clear(); | |
| this._muteActivateItem = true; | |
| if (!this._showBlackboxed && this._items.every(item => item.isBlackboxed)) { | |
| this._showBlackboxed = true; | |
| for (let i = 0; i < this._items.length; ++i) { | |
| this._list.refreshItemByIndex(i); | |
| } | |
| this._blackboxedMessageElement.classList.toggle('hidden', true); | |
| } else { | |
| const itemsSet = new Set(items); | |
| let hasBlackboxed = false; | |
| for (let i = 0; i < this._items.length; ++i) { | |
| const item = this._items.at(i); | |
| if (itemsSet.has(item)) { | |
| this._list.refreshItemByIndex(i); | |
| } | |
| hasBlackboxed = hasBlackboxed || item.isBlackboxed; | |
| } | |
| this._blackboxedMessageElement.classList.toggle('hidden', this._showBlackboxed || !hasBlackboxed); | |
| } | |
| delete this._muteActivateItem; | |
| return Promise.resolve(); | |
| } | |
| } | |
| /** | |
| * @override | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| * @return {!Element} | |
| */ | |
| createElementForItem(item) { | |
| const element = createElementWithClass('div', 'call-frame-item'); | |
| const title = element.createChild('div', 'call-frame-item-title'); | |
| title.createChild('div', 'call-frame-title-text').textContent = item.title; | |
| if (item.isAsyncHeader) { | |
| element.classList.add('async-header'); | |
| } else { | |
| const linkElement = element.createChild('div', 'call-frame-location'); | |
| linkElement.textContent = item.linkText.trimMiddle(30); | |
| linkElement.title = item.linkText; | |
| element.classList.toggle('blackboxed-call-frame', item.isBlackboxed); | |
| } | |
| element.classList.toggle('hidden', !this._showBlackboxed && item.isBlackboxed); | |
| element.appendChild(UI.Icon.create('smallicon-thick-right-arrow', 'selected-call-frame-icon')); | |
| return element; | |
| } | |
| /** | |
| * @override | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| * @return {number} | |
| */ | |
| heightForItem(item) { | |
| console.assert(false); // Should not be called. | |
| return 0; | |
| } | |
| /** | |
| * @override | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| * @return {boolean} | |
| */ | |
| isItemSelectable(item) { | |
| return !!item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]; | |
| } | |
| /** | |
| * @override | |
| * @param {?Sources.CallStackSidebarPane.Item} from | |
| * @param {?Sources.CallStackSidebarPane.Item} to | |
| * @param {?Element} fromElement | |
| * @param {?Element} toElement | |
| */ | |
| selectedItemChanged(from, to, fromElement, toElement) { | |
| if (fromElement) { | |
| fromElement.classList.remove('selected'); | |
| } | |
| if (toElement) { | |
| toElement.classList.add('selected'); | |
| } | |
| if (to) { | |
| this._activateItem(to); | |
| } | |
| } | |
| /** | |
| * @override | |
| * @param {?Element} fromElement | |
| * @param {?Element} toElement | |
| * @return {boolean} | |
| */ | |
| updateSelectedItemARIA(fromElement, toElement) { | |
| return false; | |
| } | |
| /** | |
| * @return {!Element} | |
| */ | |
| _createBlackboxedMessageElement() { | |
| const element = createElementWithClass('div', 'blackboxed-message'); | |
| element.createChild('span'); | |
| const showAllLink = element.createChild('span', 'link'); | |
| showAllLink.textContent = Common.UIString('Show blackboxed frames'); | |
| showAllLink.addEventListener('click', () => { | |
| this._showBlackboxed = true; | |
| for (const item of this._items) { | |
| this._refreshItem(item); | |
| } | |
| this._blackboxedMessageElement.classList.toggle('hidden', true); | |
| }); | |
| return element; | |
| } | |
| /** | |
| * @return {!Element} | |
| */ | |
| _createShowMoreMessageElement() { | |
| const element = createElementWithClass('div', 'show-more-message'); | |
| element.createChild('span'); | |
| const showAllLink = element.createChild('span', 'link'); | |
| showAllLink.textContent = Common.UIString('Show more'); | |
| showAllLink.addEventListener('click', () => { | |
| this._maxAsyncStackChainDepth += Sources.CallStackSidebarPane._defaultMaxAsyncStackChainDepth; | |
| this._update(); | |
| }, false); | |
| return element; | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _onContextMenu(event) { | |
| const item = this._list.itemForNode(/** @type {?Node} */ (event.target)); | |
| if (!item) { | |
| return; | |
| } | |
| const contextMenu = new UI.ContextMenu(event); | |
| const debuggerCallFrame = item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]; | |
| if (debuggerCallFrame) { | |
| contextMenu.defaultSection().appendItem(Common.UIString('Restart frame'), () => debuggerCallFrame.restart()); | |
| } | |
| contextMenu.defaultSection().appendItem(Common.UIString('Copy stack trace'), this._copyStackTrace.bind(this)); | |
| if (item.uiLocation) { | |
| this.appendBlackboxURLContextMenuItems(contextMenu, item.uiLocation.uiSourceCode); | |
| } | |
| contextMenu.show(); | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _onClick(event) { | |
| const item = this._list.itemForNode(/** @type {?Node} */ (event.target)); | |
| if (item) { | |
| this._activateItem(item); | |
| } | |
| } | |
| /** | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| */ | |
| _activateItem(item) { | |
| const uiLocation = item.uiLocation; | |
| if (this._muteActivateItem || !uiLocation) { | |
| return; | |
| } | |
| const debuggerCallFrame = item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]; | |
| if (debuggerCallFrame && UI.context.flavor(SDK.DebuggerModel.CallFrame) !== debuggerCallFrame) { | |
| debuggerCallFrame.debuggerModel.setSelectedCallFrame(debuggerCallFrame); | |
| UI.context.setFlavor(SDK.DebuggerModel.CallFrame, debuggerCallFrame); | |
| } else { | |
| Common.Revealer.reveal(uiLocation); | |
| } | |
| } | |
| /** | |
| * @param {!UI.ContextMenu} contextMenu | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| */ | |
| appendBlackboxURLContextMenuItems(contextMenu, uiSourceCode) { | |
| const binding = Persistence.persistence.binding(uiSourceCode); | |
| if (binding) { | |
| uiSourceCode = binding.network; | |
| } | |
| if (uiSourceCode.project().type() === Workspace.projectTypes.FileSystem) { | |
| return; | |
| } | |
| const canBlackbox = Bindings.blackboxManager.canBlackboxUISourceCode(uiSourceCode); | |
| const isBlackboxed = Bindings.blackboxManager.isBlackboxedUISourceCode(uiSourceCode); | |
| const isContentScript = uiSourceCode.project().type() === Workspace.projectTypes.ContentScripts; | |
| const manager = Bindings.blackboxManager; | |
| if (canBlackbox) { | |
| if (isBlackboxed) { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('Stop blackboxing'), manager.unblackboxUISourceCode.bind(manager, uiSourceCode)); | |
| } else { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('Blackbox script'), manager.blackboxUISourceCode.bind(manager, uiSourceCode)); | |
| } | |
| } | |
| if (isContentScript) { | |
| if (isBlackboxed) { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('Stop blackboxing all content scripts'), manager.blackboxContentScripts.bind(manager)); | |
| } else { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('Blackbox all content scripts'), manager.unblackboxContentScripts.bind(manager)); | |
| } | |
| } | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| _selectNextCallFrameOnStack() { | |
| return this._list.selectNextItem(false /* canWrap */, false /* center */); | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| _selectPreviousCallFrameOnStack() { | |
| return this._list.selectPreviousItem(false /* canWrap */, false /* center */); | |
| } | |
| _copyStackTrace() { | |
| const text = []; | |
| for (const item of this._items) { | |
| let itemText = item.title; | |
| if (item.uiLocation) { | |
| itemText += ' (' + item.uiLocation.linkText(true /* skipTrim */) + ')'; | |
| } | |
| text.push(itemText); | |
| } | |
| Host.InspectorFrontendHost.copyText(text.join('\n')); | |
| } | |
| }; | |
| Sources.CallStackSidebarPane._debuggerCallFrameSymbol = Symbol('debuggerCallFrame'); | |
| Sources.CallStackSidebarPane._elementSymbol = Symbol('element'); | |
| Sources.CallStackSidebarPane._defaultMaxAsyncStackChainDepth = 32; | |
| /** | |
| * @implements {UI.ActionDelegate} | |
| */ | |
| Sources.CallStackSidebarPane.ActionDelegate = class { | |
| /** | |
| * @override | |
| * @param {!UI.Context} context | |
| * @param {string} actionId | |
| * @return {boolean} | |
| */ | |
| handleAction(context, actionId) { | |
| const callStackSidebarPane = self.runtime.sharedInstance(Sources.CallStackSidebarPane); | |
| switch (actionId) { | |
| case 'debugger.next-call-frame': | |
| callStackSidebarPane._selectNextCallFrameOnStack(); | |
| return true; | |
| case 'debugger.previous-call-frame': | |
| callStackSidebarPane._selectPreviousCallFrameOnStack(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| }; | |
| Sources.CallStackSidebarPane.Item = class { | |
| /** | |
| * @param {!SDK.DebuggerModel.CallFrame} frame | |
| * @param {!Bindings.LiveLocationPool} locationPool | |
| * @param {function(!Sources.CallStackSidebarPane.Item)} updateDelegate | |
| * @return {!Sources.CallStackSidebarPane.Item} | |
| */ | |
| static createForDebuggerCallFrame(frame, locationPool, updateDelegate) { | |
| const item = new Sources.CallStackSidebarPane.Item(UI.beautifyFunctionName(frame.functionName), updateDelegate); | |
| Bindings.debuggerWorkspaceBinding.createCallFrameLiveLocation( | |
| frame.location(), item._update.bind(item), locationPool); | |
| return item; | |
| } | |
| /** | |
| * @param {string} title | |
| * @param {?SDK.DebuggerModel} debuggerModel | |
| * @param {!Array<!Protocol.Runtime.CallFrame>} frames | |
| * @param {!Bindings.LiveLocationPool} locationPool | |
| * @param {function(!Sources.CallStackSidebarPane.Item)} updateDelegate | |
| * @return {!Array<!Sources.CallStackSidebarPane.Item>} | |
| */ | |
| static createItemsForAsyncStack(title, debuggerModel, frames, locationPool, updateDelegate) { | |
| const whiteboxedItemsSymbol = Symbol('whiteboxedItems'); | |
| const asyncHeaderItem = new Sources.CallStackSidebarPane.Item(title, updateDelegate); | |
| asyncHeaderItem[whiteboxedItemsSymbol] = new Set(); | |
| asyncHeaderItem.isAsyncHeader = true; | |
| const asyncFrameItems = frames.map(frame => { | |
| const item = new Sources.CallStackSidebarPane.Item(UI.beautifyFunctionName(frame.functionName), update); | |
| const rawLocation = debuggerModel ? | |
| debuggerModel.createRawLocationByScriptId(frame.scriptId, frame.lineNumber, frame.columnNumber) : | |
| null; | |
| if (!rawLocation) { | |
| item.linkText = (frame.url || '<unknown>') + ':' + (frame.lineNumber + 1); | |
| item.updateDelegate(item); | |
| } else { | |
| Bindings.debuggerWorkspaceBinding.createCallFrameLiveLocation( | |
| rawLocation, item._update.bind(item), locationPool); | |
| } | |
| return item; | |
| }); | |
| updateDelegate(asyncHeaderItem); | |
| return [asyncHeaderItem, ...asyncFrameItems]; | |
| /** | |
| * @param {!Sources.CallStackSidebarPane.Item} item | |
| */ | |
| function update(item) { | |
| updateDelegate(item); | |
| let shouldUpdate = false; | |
| const items = asyncHeaderItem[whiteboxedItemsSymbol]; | |
| if (item.isBlackboxed) { | |
| items.delete(item); | |
| shouldUpdate = items.size === 0; | |
| } else { | |
| shouldUpdate = items.size === 0; | |
| items.add(item); | |
| } | |
| asyncHeaderItem.isBlackboxed = asyncHeaderItem[whiteboxedItemsSymbol].size === 0; | |
| if (shouldUpdate) { | |
| updateDelegate(asyncHeaderItem); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {string} title | |
| * @param {function(!Sources.CallStackSidebarPane.Item)} updateDelegate | |
| */ | |
| constructor(title, updateDelegate) { | |
| this.isBlackboxed = false; | |
| this.title = title; | |
| this.linkText = ''; | |
| this.uiLocation = null; | |
| this.isAsyncHeader = false; | |
| this.updateDelegate = updateDelegate; | |
| } | |
| /** | |
| * @param {!Bindings.LiveLocation} liveLocation | |
| */ | |
| _update(liveLocation) { | |
| const uiLocation = liveLocation.uiLocation(); | |
| this.isBlackboxed = uiLocation ? Bindings.blackboxManager.isBlackboxedUISourceCode(uiLocation.uiSourceCode) : false; | |
| this.linkText = uiLocation ? uiLocation.linkText() : ''; | |
| this.uiLocation = uiLocation; | |
| this.updateDelegate(this); | |
| } | |
| }; |