forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			469 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			469 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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";
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "Utils",
 | |
|   "resource://devtools/client/webconsole/utils.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "WebConsoleUI",
 | |
|   "resource://devtools/client/webconsole/webconsole-ui.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "gDevTools",
 | |
|   "resource://devtools/client/framework/devtools.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "openDocLink",
 | |
|   "resource://devtools/client/shared/link.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "DevToolsUtils",
 | |
|   "resource://devtools/shared/DevToolsUtils.js"
 | |
| );
 | |
| const EventEmitter = require("resource://devtools/shared/event-emitter.js");
 | |
| const Telemetry = require("resource://devtools/client/shared/telemetry.js");
 | |
| 
 | |
| var gHudId = 0;
 | |
| const isMacOS = Services.appinfo.OS === "Darwin";
 | |
| 
 | |
| /**
 | |
|  * A WebConsole instance is an interactive console initialized *per target*
 | |
|  * that displays console log data as well as provides an interactive terminal to
 | |
|  * manipulate the target's document content.
 | |
|  *
 | |
|  * This object only wraps the iframe that holds the Web Console UI. This is
 | |
|  * meant to be an integration point between the Firefox UI and the Web Console
 | |
|  * UI and features.
 | |
|  */
 | |
| class WebConsole {
 | |
|   /*
 | |
|    * @constructor
 | |
|    * @param object toolbox
 | |
|    *        The toolbox where the web console is displayed.
 | |
|    * @param object commands
 | |
|    *        The commands object with all interfaces defined from devtools/shared/commands/
 | |
|    * @param nsIDOMWindow iframeWindow
 | |
|    *        The window where the web console UI is already loaded.
 | |
|    * @param nsIDOMWindow chromeWindow
 | |
|    *        The window of the web console owner.
 | |
|    * @param bool isBrowserConsole
 | |
|    */
 | |
|   constructor(
 | |
|     toolbox,
 | |
|     commands,
 | |
|     iframeWindow,
 | |
|     chromeWindow,
 | |
|     isBrowserConsole = false
 | |
|   ) {
 | |
|     this.toolbox = toolbox;
 | |
|     this.commands = commands;
 | |
|     this.iframeWindow = iframeWindow;
 | |
|     this.chromeWindow = chromeWindow;
 | |
|     this.hudId = "hud_" + ++gHudId;
 | |
|     this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow);
 | |
|     this.isBrowserConsole = isBrowserConsole;
 | |
| 
 | |
|     // On the browser console, where we don't have a toolbox, we instantiate a dedicated Telemetry instance.
 | |
|     this.telemetry = toolbox?.telemetry || new Telemetry();
 | |
| 
 | |
|     const element = this.browserWindow.document.documentElement;
 | |
|     if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
 | |
|       this.browserWindow = Services.wm.getMostRecentWindow(
 | |
|         gDevTools.chromeWindowType
 | |
|       );
 | |
|     }
 | |
|     this.ui = new WebConsoleUI(this);
 | |
|     this._destroyer = null;
 | |
| 
 | |
|     EventEmitter.decorate(this);
 | |
|   }
 | |
| 
 | |
|   recordEvent(event, extra = {}) {
 | |
|     this.telemetry.recordEvent(event, "webconsole", null, extra);
 | |
|   }
 | |
| 
 | |
|   get currentTarget() {
 | |
|     return this.commands.targetCommand.targetFront;
 | |
|   }
 | |
| 
 | |
|   get resourceCommand() {
 | |
|     return this.commands.resourceCommand;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Getter for the window that can provide various utilities that the web
 | |
|    * console makes use of, like opening links, managing popups, etc.  In
 | |
|    * most cases, this will be |this.browserWindow|, but in some uses (such as
 | |
|    * the Browser Toolbox), there is no browser window, so an alternative window
 | |
|    * hosts the utilities there.
 | |
|    * @type nsIDOMWindow
 | |
|    */
 | |
|   get chromeUtilsWindow() {
 | |
|     if (this.browserWindow) {
 | |
|       return this.browserWindow;
 | |
|     }
 | |
|     return DevToolsUtils.getTopWindow(this.chromeWindow);
 | |
|   }
 | |
| 
 | |
|   get gViewSourceUtils() {
 | |
|     return this.chromeUtilsWindow.gViewSourceUtils;
 | |
|   }
 | |
| 
 | |
|   getFrontByID(id) {
 | |
|     return this.commands.client.getFrontByID(id);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Initialize the Web Console instance.
 | |
|    *
 | |
|    * @param {Boolean} emitCreatedEvent: Defaults to true. If false is passed,
 | |
|    *        We won't be sending the 'web-console-created' event.
 | |
|    *
 | |
|    * @return object
 | |
|    *         A promise for the initialization.
 | |
|    */
 | |
|   async init(emitCreatedEvent = true) {
 | |
|     await this.ui.init();
 | |
| 
 | |
|     // This event needs to be fired later in the case of the BrowserConsole
 | |
|     if (emitCreatedEvent) {
 | |
|       const id = Utils.supportsString(this.hudId);
 | |
|       Services.obs.notifyObservers(id, "web-console-created");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The JSTerm object that manages the console's input.
 | |
|    * @see webconsole.js::JSTerm
 | |
|    * @type object
 | |
|    */
 | |
|   get jsterm() {
 | |
|     return this.ui ? this.ui.jsterm : null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the value from the input field.
 | |
|    * @returns {String|null} returns null if there's no input.
 | |
|    */
 | |
|   getInputValue() {
 | |
|     if (!this.jsterm) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     return this.jsterm._getValue();
 | |
|   }
 | |
| 
 | |
|   inputHasSelection() {
 | |
|     const { editor } = this.jsterm || {};
 | |
|     return editor && !!editor.getSelection();
 | |
|   }
 | |
| 
 | |
|   getInputSelection() {
 | |
|     if (!this.jsterm || !this.jsterm.editor) {
 | |
|       return null;
 | |
|     }
 | |
|     return this.jsterm.editor.getSelection();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets the value of the input field (command line)
 | |
|    *
 | |
|    * @param {String} newValue: The new value to set.
 | |
|    */
 | |
|   setInputValue(newValue) {
 | |
|     if (!this.jsterm) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.jsterm._setValue(newValue);
 | |
|   }
 | |
| 
 | |
|   focusInput() {
 | |
|     return this.jsterm && this.jsterm.focus();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Open a link in a new tab.
 | |
|    *
 | |
|    * @param string link
 | |
|    *        The URL you want to open in a new tab.
 | |
|    */
 | |
|   openLink(link, e = {}) {
 | |
|     openDocLink(link, {
 | |
|       relatedToCurrent: true,
 | |
|       inBackground: isMacOS ? e.metaKey : e.ctrlKey,
 | |
|     });
 | |
|     if (e && typeof e.stopPropagation === "function") {
 | |
|       e.stopPropagation();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Open a link in Firefox's view source.
 | |
|    *
 | |
|    * @param string sourceURL
 | |
|    *        The URL of the file.
 | |
|    * @param integer sourceLine
 | |
|    *        The line number which should be highlighted.
 | |
|    */
 | |
|   viewSource(sourceURL, sourceLine) {
 | |
|     this.gViewSourceUtils.viewSource({
 | |
|       URL: sourceURL,
 | |
|       lineNumber: sourceLine || -1,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Tries to open a JavaScript file related to the web page for the web console
 | |
|    * instance in the Script Debugger. If the file is not found, it is opened in
 | |
|    * source view instead.
 | |
|    *
 | |
|    * Manually handle the case where toolbox does not exist (Browser Console).
 | |
|    *
 | |
|    * @param string sourceURL
 | |
|    *        The URL of the file.
 | |
|    * @param integer sourceLine
 | |
|    *        The line number which you want to place the caret.
 | |
|    * @param integer sourceColumn
 | |
|    *        The column number which you want to place the caret.
 | |
|    */
 | |
|   async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
 | |
|     const { toolbox } = this;
 | |
|     if (!toolbox) {
 | |
|       this.viewSource(sourceURL, sourceLine, sourceColumn);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
 | |
|     this.ui.emitForTests("source-in-debugger-opened");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Retrieve information about the JavaScript debugger's currently selected stackframe.
 | |
|    * is used to allow the Web Console to evaluate code in the selected stackframe.
 | |
|    *
 | |
|    * @return {String}
 | |
|    *         The Frame Actor ID.
 | |
|    *         If the debugger is not open or if it's not paused, then |null| is
 | |
|    *         returned.
 | |
|    */
 | |
|   getSelectedFrameActorID() {
 | |
|     const { toolbox } = this;
 | |
|     if (!toolbox) {
 | |
|       return null;
 | |
|     }
 | |
|     const panel = toolbox.getPanel("jsdebugger");
 | |
| 
 | |
|     if (!panel) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     return panel.getSelectedFrameActorID();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Given an expression, returns an object containing a new expression, mapped by the
 | |
|    * parser worker to provide additional feature for the user (top-level await,
 | |
|    * original languages mapping, …).
 | |
|    *
 | |
|    * @param {String} expression: The input to maybe map.
 | |
|    * @returns {Object|null}
 | |
|    *          Returns null if the input can't be mapped.
 | |
|    *          If it can, returns an object containing the following:
 | |
|    *            - {String} expression: The mapped expression
 | |
|    *            - {Object} mapped: An object containing the different mapping that could
 | |
|    *                               be done and if they were applied on the input.
 | |
|    *                               At the moment, contains `await`, `bindings` and
 | |
|    *                               `originalExpression`.
 | |
|    */
 | |
|   getMappedExpression(expression) {
 | |
|     const { toolbox } = this;
 | |
| 
 | |
|     // We need to check if the debugger is open, since it may perform a variable name
 | |
|     // substitution for sourcemapped script (i.e. evaluated `myVar.trim()` might need to
 | |
|     // be transformed into `a.trim()`).
 | |
|     const panel = toolbox && toolbox.getPanel("jsdebugger");
 | |
|     if (panel) {
 | |
|       return panel.getMappedExpression(expression);
 | |
|     }
 | |
| 
 | |
|     if (expression.includes("await ")) {
 | |
|       const shouldMapBindings = false;
 | |
|       const shouldMapAwait = true;
 | |
|       const res = this.parserWorker.mapExpression(
 | |
|         expression,
 | |
|         null,
 | |
|         null,
 | |
|         shouldMapBindings,
 | |
|         shouldMapAwait
 | |
|       );
 | |
|       return res;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   getMappedVariables() {
 | |
|     const { toolbox } = this;
 | |
|     return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
 | |
|   }
 | |
| 
 | |
|   get parserWorker() {
 | |
|     // If we have a toolbox, we could reuse the parser already instantiated for the debugger.
 | |
|     // Note that we won't have a toolbox when running the Browser Console...
 | |
|     if (this.toolbox) {
 | |
|       return this.toolbox.parserWorker;
 | |
|     }
 | |
| 
 | |
|     if (this._parserWorker) {
 | |
|       return this._parserWorker;
 | |
|     }
 | |
| 
 | |
|     const {
 | |
|       ParserDispatcher,
 | |
|     } = require("resource://devtools/client/debugger/src/workers/parser/index.js");
 | |
| 
 | |
|     this._parserWorker = new ParserDispatcher();
 | |
|     return this._parserWorker;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Retrieves the current selection from the Inspector, if such a selection
 | |
|    * exists. This is used to pass the ID of the selected actor to the Web
 | |
|    * Console server for the $0 helper.
 | |
|    *
 | |
|    * @return object|null
 | |
|    *         A Selection referring to the currently selected node in the
 | |
|    *         Inspector.
 | |
|    *         If the inspector was never opened, or no node was ever selected,
 | |
|    *         then |null| is returned.
 | |
|    */
 | |
|   getInspectorSelection() {
 | |
|     const { toolbox } = this;
 | |
|     if (!toolbox) {
 | |
|       return null;
 | |
|     }
 | |
|     const panel = toolbox.getPanel("inspector");
 | |
|     if (!panel || !panel.selection) {
 | |
|       return null;
 | |
|     }
 | |
|     return panel.selection;
 | |
|   }
 | |
| 
 | |
|   async onViewSourceInDebugger({ id, url, line, column }) {
 | |
|     if (this.toolbox) {
 | |
|       await this.toolbox.viewSourceInDebugger(url, line, column, id);
 | |
| 
 | |
|       this.recordEvent("jump_to_source");
 | |
|       this.emitForTests("source-in-debugger-opened");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async onViewSourceInStyleEditor({ url, line, column }) {
 | |
|     if (!this.toolbox) {
 | |
|       return;
 | |
|     }
 | |
|     await this.toolbox.viewSourceInStyleEditorByURL(url, line, column);
 | |
|     this.recordEvent("jump_to_source");
 | |
|   }
 | |
| 
 | |
|   async openNetworkPanel(requestId) {
 | |
|     if (!this.toolbox) {
 | |
|       return;
 | |
|     }
 | |
|     const netmonitor = await this.toolbox.selectTool("netmonitor");
 | |
|     await netmonitor.panelWin.Netmonitor.inspectRequest(requestId);
 | |
|   }
 | |
| 
 | |
|   getHighlighter() {
 | |
|     if (!this.toolbox) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     if (this._highlighter) {
 | |
|       return this._highlighter;
 | |
|     }
 | |
| 
 | |
|     this._highlighter = this.toolbox.getHighlighter();
 | |
|     return this._highlighter;
 | |
|   }
 | |
| 
 | |
|   async resendNetworkRequest(requestId) {
 | |
|     if (!this.toolbox) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const api = await this.toolbox.getNetMonitorAPI();
 | |
|     await api.resendRequest(requestId);
 | |
|   }
 | |
| 
 | |
|   async openNodeInInspector(grip) {
 | |
|     if (!this.toolbox) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const onSelectInspector = this.toolbox.selectTool(
 | |
|       "inspector",
 | |
|       "inspect_dom"
 | |
|     );
 | |
| 
 | |
|     const onNodeFront = this.toolbox.target
 | |
|       .getFront("inspector")
 | |
|       .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip));
 | |
| 
 | |
|     const [nodeFront, inspectorPanel] = await Promise.all([
 | |
|       onNodeFront,
 | |
|       onSelectInspector,
 | |
|     ]);
 | |
| 
 | |
|     const onInspectorUpdated = inspectorPanel.once("inspector-updated");
 | |
|     const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, {
 | |
|       reason: "console",
 | |
|     });
 | |
| 
 | |
|     await Promise.all([onNodeFrontSet, onInspectorUpdated]);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Destroy the object. Call this method to avoid memory leaks when the Web
 | |
|    * Console is closed.
 | |
|    *
 | |
|    * @return object
 | |
|    *         A promise object that is resolved once the Web Console is closed.
 | |
|    */
 | |
|   destroy() {
 | |
|     if (!this.hudId) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this.ui) {
 | |
|       this.ui.destroy();
 | |
|     }
 | |
| 
 | |
|     if (this._parserWorker) {
 | |
|       this._parserWorker.stop();
 | |
|       this._parserWorker = null;
 | |
|     }
 | |
| 
 | |
|     const id = Utils.supportsString(this.hudId);
 | |
|     Services.obs.notifyObservers(id, "web-console-destroyed");
 | |
|     this.hudId = null;
 | |
| 
 | |
|     this.emit("destroyed");
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = WebConsole;
 | 
