gecko-dev/devtools/client/webconsole/webconsole.js

506 строки
13 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";
var Services = require("Services");
loader.lazyRequireGetter(
this,
"Utils",
"devtools/client/webconsole/utils",
true
);
loader.lazyRequireGetter(
this,
"WebConsoleUI",
"devtools/client/webconsole/webconsole-ui",
true
);
loader.lazyRequireGetter(
this,
"gDevTools",
"devtools/client/framework/devtools",
true
);
loader.lazyRequireGetter(
this,
"openDocLink",
"devtools/client/shared/link",
true
);
loader.lazyRequireGetter(
this,
"DevToolsUtils",
"devtools/shared/DevToolsUtils"
);
const EventEmitter = require("devtools/shared/event-emitter");
const Telemetry = require("devtools/client/shared/telemetry");
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 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, iframeWindow, chromeWindow, isBrowserConsole = false) {
this.toolbox = toolbox;
this.iframeWindow = iframeWindow;
this.chromeWindow = chromeWindow;
this.hudId = "hud_" + ++gHudId;
this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow);
this.isBrowserConsole = isBrowserConsole;
this.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, {
session_id: (this.toolbox && this.toolbox.sessionId) || -1,
...extra,
});
}
get currentTarget() {
return this.toolbox.target;
}
get targetList() {
return this.toolbox.targetList;
}
/**
* 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.currentTarget.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;
}
canRewind() {
const traits = this.currentTarget && this.currentTarget.traits;
return traits && traits.canRewind;
}
/**
* 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 || 0,
});
}
/**
* Tries to open a Stylesheet file related to the web page for the web console
* instance in the Style Editor. 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.
*/
viewSourceInStyleEditor(sourceURL, sourceLine) {
const toolbox = this.toolbox;
if (!toolbox) {
this.viewSource(sourceURL, sourceLine);
return;
}
toolbox.viewSourceInStyleEditor(sourceURL, sourceLine);
}
/**
* 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.toolbox;
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 stackframes list. This
* is used to allow the Web Console to evaluate code in the selected
* stackframe.
*
* @return object|null
* An object which holds:
* - frames: the active ThreadFront.cachedFrames array.
* - selected: depth/index of the selected stackframe in the debugger
* UI.
* If the debugger is not open or if it's not paused, then |null| is
* returned.
*/
getDebuggerFrames() {
const toolbox = this.toolbox;
if (!toolbox) {
return null;
}
const panel = toolbox.getPanel("jsdebugger");
if (!panel) {
return null;
}
return panel.getFrames();
}
/**
* 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.toolbox;
// 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 (this.parserService && expression.includes("await ")) {
const shouldMapBindings = false;
const shouldMapAwait = true;
const res = this.parserService.mapExpression(
expression,
null,
null,
shouldMapBindings,
shouldMapAwait
);
return res;
}
return null;
}
get parserService() {
if (this._parserService) {
return this._parserService;
}
const {
ParserDispatcher,
} = require("devtools/client/debugger/src/workers/parser/index");
this._parserService = new ParserDispatcher();
this._parserService.start(
"resource://devtools/client/debugger/dist/parser-worker.js",
this.chromeUtilsWindow
);
return this._parserService;
}
/**
* 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.toolbox;
if (!toolbox) {
return null;
}
const panel = toolbox.getPanel("inspector");
if (!panel || !panel.selection) {
return null;
}
return panel.selection;
}
async onViewSourceInDebugger(frame) {
if (this.toolbox) {
await this.toolbox.viewSourceInDebugger(
frame.url,
frame.line,
frame.column,
frame.sourceId
);
this.recordEvent("jump_to_source");
this.emitForTests("source-in-debugger-opened");
}
}
async onViewSourceInStyleEditor(frame) {
if (!this.toolbox) {
return;
}
await this.toolbox.viewSourceInStyleEditor(
frame.url,
frame.line,
frame.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]);
}
/**
* Evaluate a JavaScript expression asynchronously.
*
* @param {String} string: The code you want to evaluate.
* @param {Object} options: Options for evaluation. See evaluateJSAsync method on
* devtools/shared/fronts/webconsole.js
*/
evaluateJSAsync(expression, options = {}) {
return this.ui._commands.evaluateJSAsync(expression, options);
}
/**
* 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._parserService) {
this._parserService.stop();
this._parserService = null;
}
const id = Utils.supportsString(this.hudId);
Services.obs.notifyObservers(id, "web-console-destroyed");
this.hudId = null;
this.emit("destroyed");
}
}
module.exports = WebConsole;