diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/webconsole/webconsole.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/webconsole.js')
-rw-r--r-- | devtools/client/webconsole/webconsole.js | 469 |
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; |