1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 loader.lazyRequireGetter(
10 "resource://devtools/client/webconsole/utils.js",
13 loader.lazyRequireGetter(
16 "resource://devtools/client/webconsole/webconsole-ui.js",
19 loader.lazyRequireGetter(
22 "resource://devtools/client/framework/devtools.js",
25 loader.lazyRequireGetter(
28 "resource://devtools/client/shared/link.js",
31 loader.lazyRequireGetter(
34 "resource://devtools/shared/DevToolsUtils.js"
36 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
37 const Telemetry = require("resource://devtools/client/shared/telemetry.js");
40 const isMacOS = Services.appinfo.OS === "Darwin";
43 * A WebConsole instance is an interactive console initialized *per target*
44 * that displays console log data as well as provides an interactive terminal to
45 * manipulate the target's document content.
47 * This object only wraps the iframe that holds the Web Console UI. This is
48 * meant to be an integration point between the Firefox UI and the Web Console
54 * @param object toolbox
55 * The toolbox where the web console is displayed.
56 * @param object commands
57 * The commands object with all interfaces defined from devtools/shared/commands/
58 * @param nsIDOMWindow iframeWindow
59 * The window where the web console UI is already loaded.
60 * @param nsIDOMWindow chromeWindow
61 * The window of the web console owner.
62 * @param bool isBrowserConsole
69 isBrowserConsole = false
71 this.toolbox = toolbox;
72 this.commands = commands;
73 this.iframeWindow = iframeWindow;
74 this.chromeWindow = chromeWindow;
75 this.hudId = "hud_" + ++gHudId;
76 this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow);
77 this.isBrowserConsole = isBrowserConsole;
79 // On the browser console, where we don't have a toolbox, we instantiate a dedicated Telemetry instance.
80 this.telemetry = toolbox?.telemetry || new Telemetry();
82 const element = this.browserWindow.document.documentElement;
83 if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
84 this.browserWindow = Services.wm.getMostRecentWindow(
85 gDevTools.chromeWindowType
88 this.ui = new WebConsoleUI(this);
89 this._destroyer = null;
91 EventEmitter.decorate(this);
94 recordEvent(event, extra = {}) {
95 this.telemetry.recordEvent(event, "webconsole", null, extra);
99 return this.commands.targetCommand.targetFront;
102 get resourceCommand() {
103 return this.commands.resourceCommand;
107 * Getter for the window that can provide various utilities that the web
108 * console makes use of, like opening links, managing popups, etc. In
109 * most cases, this will be |this.browserWindow|, but in some uses (such as
110 * the Browser Toolbox), there is no browser window, so an alternative window
111 * hosts the utilities there.
114 get chromeUtilsWindow() {
115 if (this.browserWindow) {
116 return this.browserWindow;
118 return DevToolsUtils.getTopWindow(this.chromeWindow);
121 get gViewSourceUtils() {
122 return this.chromeUtilsWindow.gViewSourceUtils;
126 return this.commands.client.getFrontByID(id);
130 * Initialize the Web Console instance.
132 * @param {Boolean} emitCreatedEvent: Defaults to true. If false is passed,
133 * We won't be sending the 'web-console-created' event.
136 * A promise for the initialization.
138 async init(emitCreatedEvent = true) {
139 await this.ui.init();
141 // This event needs to be fired later in the case of the BrowserConsole
142 if (emitCreatedEvent) {
143 const id = Utils.supportsString(this.hudId);
144 Services.obs.notifyObservers(id, "web-console-created");
149 * The JSTerm object that manages the console's input.
150 * @see webconsole.js::JSTerm
154 return this.ui ? this.ui.jsterm : null;
158 * Get the value from the input field.
159 * @returns {String|null} returns null if there's no input.
166 return this.jsterm._getValue();
169 inputHasSelection() {
170 const { editor } = this.jsterm || {};
171 return editor && !!editor.getSelection();
174 getInputSelection() {
175 if (!this.jsterm || !this.jsterm.editor) {
178 return this.jsterm.editor.getSelection();
182 * Sets the value of the input field (command line)
184 * @param {String} newValue: The new value to set.
186 setInputValue(newValue) {
191 this.jsterm._setValue(newValue);
195 return this.jsterm && this.jsterm.focus();
199 * Open a link in a new tab.
202 * The URL you want to open in a new tab.
204 openLink(link, e = {}) {
206 relatedToCurrent: true,
207 inBackground: isMacOS ? e.metaKey : e.ctrlKey,
209 if (e && typeof e.stopPropagation === "function") {
215 * Open a link in Firefox's view source.
217 * @param string sourceURL
218 * The URL of the file.
219 * @param integer sourceLine
220 * The line number which should be highlighted.
222 viewSource(sourceURL, sourceLine) {
223 this.gViewSourceUtils.viewSource({
225 lineNumber: sourceLine || -1,
230 * Tries to open a JavaScript file related to the web page for the web console
231 * instance in the Script Debugger. If the file is not found, it is opened in
232 * source view instead.
234 * Manually handle the case where toolbox does not exist (Browser Console).
236 * @param string sourceURL
237 * The URL of the file.
238 * @param integer sourceLine
239 * The line number which you want to place the caret.
240 * @param integer sourceColumn
241 * The column number which you want to place the caret.
243 async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
244 const { toolbox } = this;
246 this.viewSource(sourceURL, sourceLine, sourceColumn);
250 await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
251 this.ui.emitForTests("source-in-debugger-opened");
255 * Retrieve information about the JavaScript debugger's currently selected stackframe.
256 * is used to allow the Web Console to evaluate code in the selected stackframe.
259 * The Frame Actor ID.
260 * If the debugger is not open or if it's not paused, then |null| is
263 getSelectedFrameActorID() {
264 const { toolbox } = this;
268 const panel = toolbox.getPanel("jsdebugger");
274 return panel.getSelectedFrameActorID();
278 * Given an expression, returns an object containing a new expression, mapped by the
279 * parser worker to provide additional feature for the user (top-level await,
280 * original languages mapping, …).
282 * @param {String} expression: The input to maybe map.
283 * @returns {Object|null}
284 * Returns null if the input can't be mapped.
285 * If it can, returns an object containing the following:
286 * - {String} expression: The mapped expression
287 * - {Object} mapped: An object containing the different mapping that could
288 * be done and if they were applied on the input.
289 * At the moment, contains `await`, `bindings` and
290 * `originalExpression`.
292 getMappedExpression(expression) {
293 const { toolbox } = this;
295 // We need to check if the debugger is open, since it may perform a variable name
296 // substitution for sourcemapped script (i.e. evaluated `myVar.trim()` might need to
297 // be transformed into `a.trim()`).
298 const panel = toolbox && toolbox.getPanel("jsdebugger");
300 return panel.getMappedExpression(expression);
303 if (expression.includes("await ")) {
304 const shouldMapBindings = false;
305 const shouldMapAwait = true;
306 const res = this.parserWorker.mapExpression(
319 getMappedVariables() {
320 const { toolbox } = this;
321 return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
325 // If we have a toolbox, we could reuse the parser already instantiated for the debugger.
326 // Note that we won't have a toolbox when running the Browser Console...
328 return this.toolbox.parserWorker;
331 if (this._parserWorker) {
332 return this._parserWorker;
337 } = require("resource://devtools/client/debugger/src/workers/parser/index.js");
339 this._parserWorker = new ParserDispatcher();
340 return this._parserWorker;
344 * Retrieves the current selection from the Inspector, if such a selection
345 * exists. This is used to pass the ID of the selected actor to the Web
346 * Console server for the $0 helper.
348 * @return object|null
349 * A Selection referring to the currently selected node in the
351 * If the inspector was never opened, or no node was ever selected,
352 * then |null| is returned.
354 getInspectorSelection() {
355 const { toolbox } = this;
359 const panel = toolbox.getPanel("inspector");
360 if (!panel || !panel.selection) {
363 return panel.selection;
366 async onViewSourceInDebugger({ id, url, line, column }) {
368 await this.toolbox.viewSourceInDebugger(url, line, column, id);
370 this.recordEvent("jump_to_source");
371 this.emitForTests("source-in-debugger-opened");
375 async onViewSourceInStyleEditor({ url, line, column }) {
379 await this.toolbox.viewSourceInStyleEditorByURL(url, line, column);
380 this.recordEvent("jump_to_source");
383 async openNetworkPanel(requestId) {
387 const netmonitor = await this.toolbox.selectTool("netmonitor");
388 await netmonitor.panelWin.Netmonitor.inspectRequest(requestId);
396 if (this._highlighter) {
397 return this._highlighter;
400 this._highlighter = this.toolbox.getHighlighter();
401 return this._highlighter;
404 async resendNetworkRequest(requestId) {
409 const api = await this.toolbox.getNetMonitorAPI();
410 await api.resendRequest(requestId);
413 async openNodeInInspector(grip) {
418 const onSelectInspector = this.toolbox.selectTool(
423 const onNodeFront = this.toolbox.target
424 .getFront("inspector")
425 .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip));
427 const [nodeFront, inspectorPanel] = await Promise.all([
432 const onInspectorUpdated = inspectorPanel.once("inspector-updated");
433 const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, {
437 await Promise.all([onNodeFrontSet, onInspectorUpdated]);
441 * Destroy the object. Call this method to avoid memory leaks when the Web
445 * A promise object that is resolved once the Web Console is closed.
456 if (this._parserWorker) {
457 this._parserWorker.stop();
458 this._parserWorker = null;
461 const id = Utils.supportsString(this.hudId);
462 Services.obs.notifyObservers(id, "web-console-destroyed");
465 this.emit("destroyed");
469 module.exports = WebConsole;