webconsole.js (12860B)
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 loader.lazyRequireGetter( 8 this, 9 "Utils", 10 "resource://devtools/client/webconsole/utils.js", 11 true 12 ); 13 loader.lazyRequireGetter( 14 this, 15 "WebConsoleUI", 16 "resource://devtools/client/webconsole/webconsole-ui.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 "gDevTools", 22 "resource://devtools/client/framework/devtools.js", 23 true 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "openDocLink", 28 "resource://devtools/client/shared/link.js", 29 true 30 ); 31 loader.lazyRequireGetter( 32 this, 33 "DevToolsUtils", 34 "resource://devtools/shared/DevToolsUtils.js" 35 ); 36 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 37 const Telemetry = require("resource://devtools/client/shared/telemetry.js"); 38 39 var gHudId = 0; 40 const isMacOS = Services.appinfo.OS === "Darwin"; 41 42 /** 43 * A WebConsole instance is an interactive console initialized *per target* 44 * that displays console log data as well as provides an interactive terminal to 45 * manipulate the target's document content. 46 * 47 * This object only wraps the iframe that holds the Web Console UI. This is 48 * meant to be an integration point between the Firefox UI and the Web Console 49 * UI and features. 50 */ 51 class WebConsole { 52 /** 53 * @class 54 * @param object toolbox 55 * The toolbox where the web console is displayed. 56 * @param object commands 57 * The commands object with all interfaces defined from devtools/shared/commands/ 58 * @param nsIDOMWindow iframeWindow 59 * The window where the web console UI is already loaded. 60 * @param nsIDOMWindow chromeWindow 61 * The window of the web console owner. 62 * @param bool isBrowserConsole 63 */ 64 constructor( 65 toolbox, 66 commands, 67 iframeWindow, 68 chromeWindow, 69 isBrowserConsole = false 70 ) { 71 this.toolbox = toolbox; 72 this.commands = commands; 73 this.iframeWindow = iframeWindow; 74 this.chromeWindow = chromeWindow; 75 this.hudId = "hud_" + ++gHudId; 76 this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow); 77 this.isBrowserConsole = isBrowserConsole; 78 79 // On the browser console, where we don't have a toolbox, we instantiate a dedicated Telemetry instance. 80 this.telemetry = toolbox?.telemetry || new Telemetry(); 81 82 const element = this.browserWindow.document.documentElement; 83 if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) { 84 this.browserWindow = Services.wm.getMostRecentWindow( 85 gDevTools.chromeWindowType 86 ); 87 } 88 this.ui = new WebConsoleUI(this); 89 this._destroyer = null; 90 91 EventEmitter.decorate(this); 92 } 93 94 recordEvent(event, extra = {}) { 95 this.telemetry.recordEvent(event, "webconsole", null, extra); 96 } 97 98 get currentTarget() { 99 return this.commands.targetCommand.targetFront; 100 } 101 102 get resourceCommand() { 103 return this.commands.resourceCommand; 104 } 105 106 /** 107 * Getter for the window that can provide various utilities that the web 108 * console makes use of, like opening links, managing popups, etc. In 109 * most cases, this will be |this.browserWindow|, but in some uses (such as 110 * the Browser Toolbox), there is no browser window, so an alternative window 111 * hosts the utilities there. 112 * 113 * @type nsIDOMWindow 114 */ 115 get chromeUtilsWindow() { 116 if (this.browserWindow) { 117 return this.browserWindow; 118 } 119 return DevToolsUtils.getTopWindow(this.chromeWindow); 120 } 121 122 get gViewSourceUtils() { 123 return this.chromeUtilsWindow.gViewSourceUtils; 124 } 125 126 getFrontByID(id) { 127 return this.commands.client.getFrontByID(id); 128 } 129 130 /** 131 * Initialize the Web Console instance. 132 * 133 * @param {boolean} emitCreatedEvent: Defaults to true. If false is passed, 134 * We won't be sending the 'web-console-created' event. 135 * 136 * @return object 137 * A promise for the initialization. 138 */ 139 async init(emitCreatedEvent = true) { 140 await this.ui.init(); 141 142 // This event needs to be fired later in the case of the BrowserConsole 143 if (emitCreatedEvent) { 144 const id = Utils.supportsString(this.hudId); 145 Services.obs.notifyObservers(id, "web-console-created"); 146 } 147 } 148 149 /** 150 * The JSTerm object that manages the console's input. 151 * 152 * @see webconsole.js::JSTerm 153 * @type object 154 */ 155 get jsterm() { 156 return this.ui ? this.ui.jsterm : null; 157 } 158 159 /** 160 * Get the value from the input field. 161 * 162 * @returns {string | null} returns null if there's no input. 163 */ 164 getInputValue() { 165 if (!this.jsterm) { 166 return null; 167 } 168 169 return this.jsterm._getValue(); 170 } 171 172 inputHasSelection() { 173 const { editor } = this.jsterm || {}; 174 return editor && !!editor.getSelection(); 175 } 176 177 getInputSelection() { 178 if (!this.jsterm || !this.jsterm.editor) { 179 return null; 180 } 181 return this.jsterm.editor.getSelection(); 182 } 183 184 /** 185 * Sets the value of the input field (command line) 186 * 187 * @param {string} newValue: The new value to set. 188 */ 189 setInputValue(newValue) { 190 if (!this.jsterm) { 191 return; 192 } 193 194 this.jsterm._setValue(newValue); 195 } 196 197 focusInput() { 198 return this.jsterm && this.jsterm.focus(); 199 } 200 201 /** 202 * Open a link in a new tab. 203 * 204 * @param string link 205 * The URL you want to open in a new tab. 206 */ 207 openLink(link, e = {}) { 208 openDocLink(link, { 209 relatedToCurrent: true, 210 inBackground: isMacOS ? e.metaKey : e.ctrlKey, 211 }); 212 if (e && typeof e.stopPropagation === "function") { 213 e.stopPropagation(); 214 } 215 } 216 217 /** 218 * Open a link in Firefox's view source. 219 * 220 * @param string sourceURL 221 * The URL of the file. 222 * @param integer sourceLine 223 * The line number which should be highlighted. 224 */ 225 viewSource(sourceURL, sourceLine) { 226 this.gViewSourceUtils.viewSource({ 227 URL: sourceURL, 228 lineNumber: sourceLine || -1, 229 }); 230 } 231 232 /** 233 * Tries to open a JavaScript file related to the web page for the web console 234 * instance in the Script Debugger. If the file is not found, it is opened in 235 * source view instead. 236 * 237 * Manually handle the case where toolbox does not exist (Browser Console). 238 * 239 * @param string sourceURL 240 * The URL of the file. 241 * @param integer sourceLine 242 * The line number which you want to place the caret. 243 * @param integer sourceColumn 244 * The column number which you want to place the caret. 245 */ 246 async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) { 247 const { toolbox } = this; 248 if (!toolbox) { 249 this.viewSource(sourceURL, sourceLine, sourceColumn); 250 return; 251 } 252 253 await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn); 254 this.ui.emitForTests("source-in-debugger-opened"); 255 } 256 257 /** 258 * Retrieve information about the JavaScript debugger's currently selected stackframe. 259 * is used to allow the Web Console to evaluate code in the selected stackframe. 260 * 261 * @return {string} 262 * The Frame Actor ID. 263 * If the debugger is not open or if it's not paused, then |null| is 264 * returned. 265 */ 266 getSelectedFrameActorID() { 267 const { toolbox } = this; 268 if (!toolbox) { 269 return null; 270 } 271 const panel = toolbox.getPanel("jsdebugger"); 272 273 if (!panel) { 274 return null; 275 } 276 277 return panel.getSelectedFrameActorID(); 278 } 279 280 /** 281 * Given an expression, returns an object containing a new expression, mapped by the 282 * parser worker to provide additional feature for the user (top-level await, 283 * original languages mapping, …). 284 * 285 * @param {string} expression: The input to maybe map. 286 * @returns {object | null} 287 * Returns null if the input can't be mapped. 288 * If it can, returns an object containing the following: 289 * - {String} expression: The mapped expression 290 * - {Object} mapped: An object containing the different mapping that could 291 * be done and if they were applied on the input. 292 * At the moment, contains `await`, `bindings` and 293 * `originalExpression`. 294 */ 295 getMappedExpression(expression) { 296 const { toolbox } = this; 297 298 // We need to check if the debugger is open, since it may perform a variable name 299 // substitution for sourcemapped script (i.e. evaluated `myVar.trim()` might need to 300 // be transformed into `a.trim()`). 301 const panel = toolbox && toolbox.getPanel("jsdebugger"); 302 if (panel) { 303 return panel.getMappedExpression(expression); 304 } 305 306 if (expression.includes("await ")) { 307 const shouldMapBindings = false; 308 const shouldMapAwait = true; 309 const res = this.parserWorker.mapExpression( 310 expression, 311 null, 312 null, 313 shouldMapBindings, 314 shouldMapAwait 315 ); 316 return res; 317 } 318 319 return null; 320 } 321 322 getMappedVariables() { 323 const { toolbox } = this; 324 return toolbox?.getPanel("jsdebugger")?.getMappedVariables(); 325 } 326 327 get parserWorker() { 328 // If we have a toolbox, we could reuse the parser already instantiated for the debugger. 329 // Note that we won't have a toolbox when running the Browser Console... 330 if (this.toolbox) { 331 return this.toolbox.parserWorker; 332 } 333 334 if (this._parserWorker) { 335 return this._parserWorker; 336 } 337 338 const { 339 ParserDispatcher, 340 } = require("resource://devtools/client/debugger/src/workers/parser/index.js"); 341 342 this._parserWorker = new ParserDispatcher(); 343 return this._parserWorker; 344 } 345 346 /** 347 * Retrieves the current selection from the Inspector, if such a selection 348 * exists. This is used to pass the ID of the selected actor to the Web 349 * Console server for the $0 helper. 350 * 351 * @return object|null 352 * A Selection referring to the currently selected node in the 353 * Inspector. 354 * If the inspector was never opened, or no node was ever selected, 355 * then |null| is returned. 356 */ 357 getInspectorSelection() { 358 const { toolbox } = this; 359 if (!toolbox) { 360 return null; 361 } 362 const panel = toolbox.getPanel("inspector"); 363 if (!panel || !panel.selection) { 364 return null; 365 } 366 return panel.selection; 367 } 368 369 async onViewSourceInDebugger({ id, url, line, column }) { 370 if (this.toolbox) { 371 await this.toolbox.viewSourceInDebugger(url, line, column, id); 372 373 this.recordEvent("jump_to_source"); 374 this.emitForTests("source-in-debugger-opened"); 375 } 376 } 377 378 async onViewSourceInStyleEditor({ url, line, column }) { 379 if (!this.toolbox) { 380 return; 381 } 382 await this.toolbox.viewSourceInStyleEditorByURL(url, line, column); 383 this.recordEvent("jump_to_source"); 384 } 385 386 async openNetworkPanel(requestId) { 387 if (!this.toolbox) { 388 return; 389 } 390 const netmonitor = await this.toolbox.selectTool("netmonitor"); 391 await netmonitor.panelWin.Netmonitor.inspectRequest(requestId); 392 } 393 394 getHighlighter() { 395 if (!this.toolbox) { 396 return null; 397 } 398 399 if (this._highlighter) { 400 return this._highlighter; 401 } 402 403 this._highlighter = this.toolbox.getHighlighter(); 404 return this._highlighter; 405 } 406 407 async resendNetworkRequest(requestId) { 408 if (!this.toolbox) { 409 return; 410 } 411 412 const api = await this.toolbox.getNetMonitorAPI(); 413 await api.resendRequest(requestId); 414 } 415 416 async openNodeInInspector(grip) { 417 if (!this.toolbox) { 418 return; 419 } 420 421 const onSelectInspector = this.toolbox.selectTool( 422 "inspector", 423 "inspect_dom" 424 ); 425 426 const onNodeFront = this.toolbox.target 427 .getFront("inspector") 428 .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip)); 429 430 const [nodeFront, inspectorPanel] = await Promise.all([ 431 onNodeFront, 432 onSelectInspector, 433 ]); 434 435 const onInspectorUpdated = inspectorPanel.once("inspector-updated"); 436 const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, { 437 reason: "console", 438 }); 439 440 await Promise.all([onNodeFrontSet, onInspectorUpdated]); 441 } 442 443 /** 444 * Destroy the object. Call this method to avoid memory leaks when the Web 445 * Console is closed. 446 * 447 * @return object 448 * A promise object that is resolved once the Web Console is closed. 449 */ 450 destroy() { 451 if (!this.hudId) { 452 return; 453 } 454 455 if (this.ui) { 456 this.ui.destroy(); 457 } 458 459 if (this._parserWorker) { 460 this._parserWorker.stop(); 461 this._parserWorker = null; 462 } 463 464 const id = Utils.supportsString(this.hudId); 465 Services.obs.notifyObservers(id, "web-console-destroyed"); 466 this.hudId = null; 467 468 this.emit("destroyed"); 469 } 470 } 471 472 module.exports = WebConsole;