panel.js (13032B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 "use strict"; 6 7 const { 8 MultiLocalizationHelper, 9 } = require("resource://devtools/shared/l10n.js"); 10 const { 11 FluentL10n, 12 } = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js"); 13 14 loader.lazyRequireGetter( 15 this, 16 "openContentLink", 17 "resource://devtools/client/shared/link.js", 18 true 19 ); 20 21 loader.lazyRequireGetter( 22 this, 23 "registerStoreObserver", 24 "resource://devtools/client/shared/redux/subscriber.js", 25 true 26 ); 27 28 const DBG_STRINGS_URI = [ 29 "devtools/client/locales/debugger.properties", 30 // Used by App.js to map the sourceeditor goto line shortcut 31 "devtools/client/locales/sourceeditor.properties", 32 // These are used in the AppErrorBoundary component 33 "devtools/client/locales/startup.properties", 34 "devtools/client/locales/components.properties", 35 // Used by SourceMapLoader 36 "devtools/client/locales/toolbox.properties", 37 ]; 38 const L10N = new MultiLocalizationHelper(...DBG_STRINGS_URI); 39 40 async function getNodeFront(gripOrFront, toolbox) { 41 // Given a NodeFront 42 if ("actorID" in gripOrFront) { 43 return new Promise(resolve => resolve(gripOrFront)); 44 } 45 46 const inspectorFront = await toolbox.target.getFront("inspector"); 47 return inspectorFront.getNodeFrontFromNodeGrip(gripOrFront); 48 } 49 50 class DebuggerPanel { 51 constructor(iframeWindow, toolbox, commands) { 52 this.panelWin = iframeWindow; 53 this.panelWin.L10N = L10N; 54 this.panelWin.sourceMapURLService = toolbox.sourceMapURLService; 55 56 this.toolbox = toolbox; 57 this.commands = commands; 58 59 // Somewhat equivalent of global `lazy` object used for ChromeUtils.defineESModuleGetter, 60 // but instantiated once per Debugger document as the modules will be loaded in the 61 // document scope. 62 // It is also important to release references to them in destroy in order to prevent leaks. 63 this.lazyModules = {}; 64 } 65 66 async open() { 67 // whypaused-* strings are in devtools/shared as they're used in the PausedDebuggerOverlay as well 68 const fluentL10n = new FluentL10n(); 69 await fluentL10n.init(["devtools/shared/debugger-paused-reasons.ftl"]); 70 71 // Ensure loading all debugger modules via the Browser Loader 72 // in order to prevent loading duplicated instances of them, 73 // and also ensure loading them with debugger document as global. 74 const { browserLoader } = this.panelWin; 75 browserLoader.lazyRequireGetter( 76 this.lazyModules, 77 "features", 78 "resource://devtools/client/debugger/src/utils/prefs.js", 79 true 80 ); 81 browserLoader.lazyRequireGetter( 82 this.lazyModules, 83 "getOriginalLocation", 84 "resource://devtools/client/debugger/src/utils/source-maps.js", 85 true 86 ); 87 browserLoader.lazyRequireGetter( 88 this.lazyModules, 89 "createLocation", 90 "resource://devtools/client/debugger/src/utils/location.js", 91 true 92 ); 93 browserLoader.lazyRequireGetter( 94 this.lazyModules, 95 "getMappedExpression", 96 "resource://devtools/client/debugger/src/actions/expressions.js", 97 true 98 ); 99 100 const { actions, store, selectors, client } = 101 await this.panelWin.Debugger.bootstrap({ 102 commands: this.commands, 103 fluentBundles: fluentL10n.getBundles(), 104 resourceCommand: this.commands.resourceCommand, 105 workers: { 106 sourceMapLoader: this.toolbox.sourceMapLoader, 107 parserWorker: this.toolbox.parserWorker, 108 }, 109 panel: this, 110 }); 111 112 this._actions = actions; 113 this._store = store; 114 this._selectors = selectors; 115 this._client = client; 116 117 registerStoreObserver(this._store, this._onDebuggerStateChange.bind(this)); 118 119 return this; 120 } 121 122 async _onDebuggerStateChange(state, oldState) { 123 const { getCurrentThread } = this._selectors; 124 const currentThreadActorID = getCurrentThread(state); 125 126 if ( 127 currentThreadActorID && 128 currentThreadActorID !== getCurrentThread(oldState) 129 ) { 130 const threadFront = 131 this.commands.client.getFrontByID(currentThreadActorID); 132 this.toolbox.selectTarget(threadFront?.targetFront.actorID); 133 } 134 135 this.toolbox.emit( 136 "show-original-variable-mapping-warnings", 137 this.shouldShowOriginalVariableMappingWarnings() 138 ); 139 } 140 141 shouldShowOriginalVariableMappingWarnings() { 142 const { getSelectedSource, isMapScopesEnabled } = this._selectors; 143 if (!this.isPaused() || isMapScopesEnabled(this._getState())) { 144 return false; 145 } 146 const selectedSource = getSelectedSource(this._getState()); 147 return selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted; 148 } 149 150 getVarsForTests() { 151 return { 152 store: this._store, 153 selectors: this._selectors, 154 actions: this._actions, 155 client: this._client, 156 }; 157 } 158 159 _getState() { 160 return this._store.getState(); 161 } 162 163 getToolboxStore() { 164 return this.toolbox.store; 165 } 166 167 openLink(url) { 168 openContentLink(url); 169 } 170 171 async openConsoleAndEvaluate(input) { 172 const { hud } = await this.toolbox.selectTool("webconsole"); 173 hud.ui.wrapper.dispatchEvaluateExpression(input); 174 } 175 176 async openInspector() { 177 this.toolbox.selectTool("inspector"); 178 } 179 180 async openElementInInspector(gripOrFront) { 181 const onSelectInspector = this.toolbox.selectTool("inspector"); 182 const onGripNodeToFront = getNodeFront(gripOrFront, this.toolbox); 183 184 const [front, inspector] = await Promise.all([ 185 onGripNodeToFront, 186 onSelectInspector, 187 ]); 188 189 const onInspectorUpdated = inspector.once("inspector-updated"); 190 const onNodeFrontSet = this.toolbox.selection.setNodeFront(front, { 191 reason: "debugger", 192 }); 193 194 return Promise.all([onNodeFrontSet, onInspectorUpdated]); 195 } 196 197 highlightDomElement(gripOrFront) { 198 if (!this._highlight) { 199 const { highlight, unhighlight } = this.toolbox.getHighlighter(); 200 this._highlight = highlight; 201 this._unhighlight = unhighlight; 202 } 203 204 return this._highlight(gripOrFront); 205 } 206 207 unHighlightDomElement() { 208 if (!this._unhighlight) { 209 return Promise.resolve(); 210 } 211 212 return this._unhighlight(); 213 } 214 215 /** 216 * Return the Frame Actor ID of the currently selected frame, 217 * or null if the debugger isn't paused. 218 */ 219 getSelectedFrameActorID() { 220 const selectedFrame = this._selectors.getSelectedFrame(this._getState()); 221 if (selectedFrame) { 222 return selectedFrame.id; 223 } 224 return null; 225 } 226 227 getMappedExpression(expression) { 228 const thread = this._selectors.getCurrentThread(this._getState()); 229 return this.lazyModules.getMappedExpression(expression, thread, { 230 getState: this._store.getState, 231 parserWorker: this.toolbox.parserWorker, 232 }); 233 } 234 235 /** 236 * Return the source-mapped variables for the current scope. 237 * 238 * @returns {{[string]: string} | null} A dictionary mapping original variable names to generated 239 * variable names if map scopes is enabled, otherwise null. 240 */ 241 getMappedVariables() { 242 if (!this._selectors.isMapScopesEnabled(this._getState())) { 243 return null; 244 } 245 const thread = this._selectors.getCurrentThread(this._getState()); 246 return this._selectors.getSelectedScopeMappings(this._getState(), thread); 247 } 248 249 isPaused() { 250 const thread = this._selectors.getCurrentThread(this._getState()); 251 return this._selectors.getIsPaused(this._getState(), thread); 252 } 253 254 selectSourceURL(url, line, column) { 255 return this._actions.selectSourceURL(url, { line, column }); 256 } 257 258 /** 259 * This is called when some other panels wants to open a given source 260 * in the debugger at a precise line/column. 261 * 262 * @param {string} generatedURL 263 * @param {number} generatedLine 264 * @param {number} generatedColumn 265 * @param {string} sourceActorId (optional) 266 * If the callsite knows about a particular sourceActorId, 267 * or if the source doesn't have a URL, you have to pass a sourceActorId. 268 * @param {string} reason 269 * A telemetry identifier to record when opening the debugger. 270 * This help differentiate why we opened the debugger. 271 * 272 * @return {boolean} 273 * Returns true if the location is known by the debugger 274 * and the debugger opens it. 275 */ 276 async openSourceInDebugger({ 277 generatedURL, 278 generatedLine, 279 generatedColumn, 280 sourceActorId, 281 reason, 282 }) { 283 // Resolve the URL in case this is an URL like http://example.org/./test.js 284 // as the frontend only supports final resolved URLs. 285 generatedURL = URL.parse(generatedURL)?.href || generatedURL; 286 287 const generatedSource = sourceActorId 288 ? this._selectors.getSourceByActorId(this._getState(), sourceActorId) 289 : this._selectors.getSourceByURL(this._getState(), generatedURL); 290 // We won't try opening source in the debugger when we can't find the related source actor in the reducer, 291 // or, when it doesn't have any related source actor registered. 292 if ( 293 !generatedSource || 294 // Note: We're not entirely sure when this can happen, 295 // so we may want to revisit that at some point. 296 !this._selectors.getSourceActorsForSource( 297 this._getState(), 298 generatedSource.id 299 ).length 300 ) { 301 return false; 302 } 303 304 const generatedLocation = this.lazyModules.createLocation({ 305 source: generatedSource, 306 line: generatedLine, 307 column: generatedColumn, 308 }); 309 310 // Note that getOriginalLocation can easily return generatedLocation 311 // if the location can't be mapped to any original source. 312 // So that we may open either regular source or original sources here. 313 const originalLocation = await this.lazyModules.getOriginalLocation( 314 generatedLocation, 315 { 316 // Reproduce a minimal thunkArgs for getOriginalLocation. 317 sourceMapLoader: this.toolbox.sourceMapLoader, 318 getState: this._store.getState, 319 } 320 ); 321 322 // view-source module only forced the load of debugger in the background. 323 // Now that we know we want to show a source, force displaying it in foreground. 324 // 325 // Note that browser_markup_view-source.js doesn't wait for the debugger 326 // to be fully loaded with the source and requires the debugger to be loaded late. 327 // But we might try to load display it early to improve user perception. 328 await this.toolbox.selectTool("jsdebugger", reason); 329 330 const hasLogpoint = this._selectors.hasLogpoint( 331 this._getState(), 332 originalLocation 333 ); 334 await this._actions.selectLocation(originalLocation, { 335 // We want to select the precise given location and do not try to map to original/bundle 336 // depending on the currently selected source type. 337 keepContext: false, 338 // We don't want to highlight/focus CodeMirror if there is a log point as we will open the log point panel right after and focus it 339 highlight: !hasLogpoint, 340 }); 341 342 // XXX: should this be moved to selectSpecificLocation?? 343 if (hasLogpoint) { 344 this._actions.openConditionalPanel(originalLocation, true); 345 } 346 347 return true; 348 } 349 350 async selectServiceWorker(workerDescriptorFront) { 351 // The descriptor used by the application panel isn't fetching the worker target, 352 // but the debugger will fetch it via the watcher actor and TargetCommand. 353 // So try to match the descriptor with its related target. 354 const targets = this.commands.targetCommand.getAllTargets([ 355 this.commands.targetCommand.TYPES.SERVICE_WORKER, 356 ]); 357 const workerTarget = targets.find( 358 target => target.id == workerDescriptorFront.id 359 ); 360 361 const threadFront = await workerTarget.getFront("thread"); 362 const threadActorID = threadFront?.actorID; 363 const isThreadAvailable = this._selectors 364 .getThreads(this._getState()) 365 .find(x => x.actor === threadActorID); 366 367 if (!this.lazyModules.features.windowlessServiceWorkers) { 368 console.error( 369 "Selecting a worker needs the pref debugger.features.windowless-service-workers set to true" 370 ); 371 return; 372 } 373 374 if (!isThreadAvailable) { 375 console.error(`Worker ${threadActorID} is not available for debugging`); 376 return; 377 } 378 379 // select worker's thread 380 this.selectThread(threadActorID); 381 382 // select worker's source 383 const source = this._selectors.getSourceByURL( 384 this._getState(), 385 workerDescriptorFront._url 386 ); 387 const sourceActor = this._selectors.getFirstSourceActorForGeneratedSource( 388 this._getState(), 389 source.id, 390 threadActorID 391 ); 392 await this._actions.selectSource(source, sourceActor); 393 } 394 395 selectThread(threadActorID) { 396 this._actions.selectThread(threadActorID); 397 } 398 399 showTracerSidebar() { 400 this._actions.setPrimaryPaneTab("tracer"); 401 } 402 403 destroy() { 404 this.panelWin.Debugger.destroy(); 405 this.lazyModules = {}; 406 this.emit("destroyed"); 407 } 408 } 409 410 exports.DebuggerPanel = DebuggerPanel;