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;
|