summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/webconsole.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/webconsole.js')
-rw-r--r--devtools/client/webconsole/webconsole.js469
1 files changed, 469 insertions, 0 deletions
diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js
new file mode 100644
index 0000000000..7280bf7810
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.js
@@ -0,0 +1,469 @@
+/* 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;