diff options
Diffstat (limited to 'devtools/client/debugger/panel.js')
-rw-r--r-- | devtools/client/debugger/panel.js | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/devtools/client/debugger/panel.js b/devtools/client/debugger/panel.js new file mode 100644 index 0000000000..6368fbe8ac --- /dev/null +++ b/devtools/client/debugger/panel.js @@ -0,0 +1,376 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const { + MultiLocalizationHelper, +} = require("resource://devtools/shared/l10n.js"); +const { + FluentL10n, +} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js"); + +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); +loader.lazyRequireGetter( + this, + "features", + "resource://devtools/client/debugger/src/utils/prefs.js", + true +); +loader.lazyRequireGetter( + this, + "getOriginalLocation", + "resource://devtools/client/debugger/src/utils/source-maps.js", + true +); +loader.lazyRequireGetter( + this, + "createLocation", + "resource://devtools/client/debugger/src/utils/location.js", + true +); +loader.lazyRequireGetter( + this, + "registerStoreObserver", + "resource://devtools/client/shared/redux/subscriber.js", + true +); +loader.lazyRequireGetter( + this, + "getMappedExpression", + "resource://devtools/client/debugger/src/actions/expressions.js", + true +); + +const DBG_STRINGS_URI = [ + "devtools/client/locales/debugger.properties", + // These are used in the AppErrorBoundary component + "devtools/client/locales/startup.properties", + "devtools/client/locales/components.properties", + // Used by SourceMapLoader + "devtools/client/locales/toolbox.properties", +]; +const L10N = new MultiLocalizationHelper(...DBG_STRINGS_URI); + +async function getNodeFront(gripOrFront, toolbox) { + // Given a NodeFront + if ("actorID" in gripOrFront) { + return new Promise(resolve => resolve(gripOrFront)); + } + + const inspectorFront = await toolbox.target.getFront("inspector"); + return inspectorFront.getNodeFrontFromNodeGrip(gripOrFront); +} + +class DebuggerPanel { + constructor(iframeWindow, toolbox, commands) { + this.panelWin = iframeWindow; + this.panelWin.L10N = L10N; + + this.toolbox = toolbox; + this.commands = commands; + } + + async open() { + // whypaused-* strings are in devtools/shared as they're used in the PausedDebuggerOverlay as well + const fluentL10n = new FluentL10n(); + await fluentL10n.init(["devtools/shared/debugger-paused-reasons.ftl"]); + + const { actions, store, selectors, client } = + await this.panelWin.Debugger.bootstrap({ + commands: this.commands, + fluentBundles: fluentL10n.getBundles(), + resourceCommand: this.toolbox.resourceCommand, + workers: { + sourceMapLoader: this.toolbox.sourceMapLoader, + parserWorker: this.toolbox.parserWorker, + }, + panel: this, + }); + + this._actions = actions; + this._store = store; + this._selectors = selectors; + this._client = client; + + registerStoreObserver(this._store, this._onDebuggerStateChange.bind(this)); + + return this; + } + + async _onDebuggerStateChange(state, oldState) { + const { getCurrentThread } = this._selectors; + const currentThreadActorID = getCurrentThread(state); + + if ( + currentThreadActorID && + currentThreadActorID !== getCurrentThread(oldState) + ) { + const threadFront = + this.commands.client.getFrontByID(currentThreadActorID); + this.toolbox.selectTarget(threadFront?.targetFront.actorID); + } + + this.toolbox.emit( + "show-original-variable-mapping-warnings", + this.shouldShowOriginalVariableMappingWarnings() + ); + } + + shouldShowOriginalVariableMappingWarnings() { + const { getSelectedSource, isMapScopesEnabled } = this._selectors; + if (!this.isPaused() || isMapScopesEnabled(this._getState())) { + return false; + } + const selectedSource = getSelectedSource(this._getState()); + return selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted; + } + + getVarsForTests() { + return { + store: this._store, + selectors: this._selectors, + actions: this._actions, + client: this._client, + }; + } + + _getState() { + return this._store.getState(); + } + + getToolboxStore() { + return this.toolbox.store; + } + + openLink(url) { + openContentLink(url); + } + + async openConsoleAndEvaluate(input) { + const { hud } = await this.toolbox.selectTool("webconsole"); + hud.ui.wrapper.dispatchEvaluateExpression(input); + } + + async openInspector() { + this.toolbox.selectTool("inspector"); + } + + async openElementInInspector(gripOrFront) { + const onSelectInspector = this.toolbox.selectTool("inspector"); + const onGripNodeToFront = getNodeFront(gripOrFront, this.toolbox); + + const [front, inspector] = await Promise.all([ + onGripNodeToFront, + onSelectInspector, + ]); + + const onInspectorUpdated = inspector.once("inspector-updated"); + const onNodeFrontSet = this.toolbox.selection.setNodeFront(front, { + reason: "debugger", + }); + + return Promise.all([onNodeFrontSet, onInspectorUpdated]); + } + + highlightDomElement(gripOrFront) { + if (!this._highlight) { + const { highlight, unhighlight } = this.toolbox.getHighlighter(); + this._highlight = highlight; + this._unhighlight = unhighlight; + } + + return this._highlight(gripOrFront); + } + + unHighlightDomElement() { + if (!this._unhighlight) { + return Promise.resolve(); + } + + return this._unhighlight(); + } + + /** + * Return the Frame Actor ID of the currently selected frame, + * or null if the debugger isn't paused. + */ + getSelectedFrameActorID() { + const thread = this._selectors.getCurrentThread(this._getState()); + const selectedFrame = this._selectors.getSelectedFrame( + this._getState(), + thread + ); + if (selectedFrame) { + return selectedFrame.id; + } + return null; + } + + getMappedExpression(expression) { + const thread = this._selectors.getCurrentThread(this._getState()); + return getMappedExpression(expression, thread, { + getState: this._store.getState, + parserWorker: this.toolbox.parserWorker, + }); + } + + /** + * Return the source-mapped variables for the current scope. + * @returns {{[String]: String} | null} A dictionary mapping original variable names to generated + * variable names if map scopes is enabled, otherwise null. + */ + getMappedVariables() { + if (!this._selectors.isMapScopesEnabled(this._getState())) { + return null; + } + const thread = this._selectors.getCurrentThread(this._getState()); + return this._selectors.getSelectedScopeMappings(this._getState(), thread); + } + + isPaused() { + const thread = this._selectors.getCurrentThread(this._getState()); + return this._selectors.getIsPaused(this._getState(), thread); + } + + selectSourceURL(url, line, column) { + return this._actions.selectSourceURL(url, { line, column }); + } + + /** + * This is called when some other panels wants to open a given source + * in the debugger at a precise line/column. + * + * @param {String} generatedURL + * @param {Number} generatedLine + * @param {Number} generatedColumn + * @param {String} sourceActorId (optional) + * If the callsite knows about a particular sourceActorId, + * or if the source doesn't have a URL, you have to pass a sourceActorId. + * @param {String} reason + * A telemetry identifier to record when opening the debugger. + * This help differentiate why we opened the debugger. + * + * @return {Boolean} + * Returns true if the location is known by the debugger + * and the debugger opens it. + */ + async openSourceInDebugger({ + generatedURL, + generatedLine, + generatedColumn, + sourceActorId, + reason, + }) { + const generatedSource = sourceActorId + ? this._selectors.getSourceByActorId(this._getState(), sourceActorId) + : this._selectors.getSourceByURL(this._getState(), generatedURL); + // We won't try opening source in the debugger when we can't find the related source actor in the reducer, + // or, when it doesn't have any related source actor registered. + if ( + !generatedSource || + // Note: We're not entirely sure when this can happen, + // so we may want to revisit that at some point. + !this._selectors.getSourceActorsForSource( + this._getState(), + generatedSource.id + ).length + ) { + return false; + } + + const generatedLocation = createLocation({ + source: generatedSource, + line: generatedLine, + column: generatedColumn, + }); + + // Note that getOriginalLocation can easily return generatedLocation + // if the location can't be mapped to any original source. + // So that we may open either regular source or original sources here. + const originalLocation = await getOriginalLocation(generatedLocation, { + // Reproduce a minimal thunkArgs for getOriginalLocation. + sourceMapLoader: this.toolbox.sourceMapLoader, + getState: this._store.getState, + }); + + // view-source module only forced the load of debugger in the background. + // Now that we know we want to show a source, force displaying it in foreground. + // + // Note that browser_markup_view-source.js doesn't wait for the debugger + // to be fully loaded with the source and requires the debugger to be loaded late. + // But we might try to load display it early to improve user perception. + await this.toolbox.selectTool("jsdebugger", reason); + + await this._actions.selectSpecificLocation(originalLocation); + + // XXX: should this be moved to selectSpecificLocation?? + if (this._selectors.hasLogpoint(this._getState(), originalLocation)) { + this._actions.openConditionalPanel(originalLocation, true); + } + + return true; + } + + async selectServiceWorker(workerDescriptorFront) { + // The descriptor used by the application panel isn't fetching the worker target, + // but the debugger will fetch it via the watcher actor and TargetCommand. + // So try to match the descriptor with its related target. + const targets = this.commands.targetCommand.getAllTargets([ + this.commands.targetCommand.TYPES.SERVICE_WORKER, + ]); + const workerTarget = targets.find( + target => target.id == workerDescriptorFront.id + ); + + const threadFront = await workerTarget.getFront("thread"); + const threadActorID = threadFront?.actorID; + const isThreadAvailable = this._selectors + .getThreads(this._getState()) + .find(x => x.actor === threadActorID); + + if (!features.windowlessServiceWorkers) { + console.error( + "Selecting a worker needs the pref debugger.features.windowless-service-workers set to true" + ); + return; + } + + if (!isThreadAvailable) { + console.error(`Worker ${threadActorID} is not available for debugging`); + return; + } + + // select worker's thread + this.selectThread(threadActorID); + + // select worker's source + const source = this._selectors.getSourceByURL( + this._getState(), + workerDescriptorFront._url + ); + const sourceActor = this._selectors.getFirstSourceActorForGeneratedSource( + this._getState(), + source.id, + threadActorID + ); + await this._actions.selectSource(source, sourceActor); + } + + selectThread(threadActorID) { + this._actions.selectThread(threadActorID); + } + + destroy() { + this.panelWin.Debugger.destroy(); + this.emit("destroyed"); + } +} + +exports.DebuggerPanel = DebuggerPanel; |