summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/panel.js')
-rw-r--r--devtools/client/debugger/panel.js376
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;