Bug 1854550 - pt 12. Allow inlining between mozjemalloc and PHC r=glandium
[gecko.git] / devtools / client / webconsole / webconsole.js
blob7280bf7810c30b230ed8205d5cd2aedb6d664348
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/. */
5 "use strict";
7 loader.lazyRequireGetter(
8   this,
9   "Utils",
10   "resource://devtools/client/webconsole/utils.js",
11   true
13 loader.lazyRequireGetter(
14   this,
15   "WebConsoleUI",
16   "resource://devtools/client/webconsole/webconsole-ui.js",
17   true
19 loader.lazyRequireGetter(
20   this,
21   "gDevTools",
22   "resource://devtools/client/framework/devtools.js",
23   true
25 loader.lazyRequireGetter(
26   this,
27   "openDocLink",
28   "resource://devtools/client/shared/link.js",
29   true
31 loader.lazyRequireGetter(
32   this,
33   "DevToolsUtils",
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");
39 var gHudId = 0;
40 const isMacOS = Services.appinfo.OS === "Darwin";
42 /**
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.
46  *
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
49  * UI and features.
50  */
51 class WebConsole {
52   /*
53    * @constructor
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
63    */
64   constructor(
65     toolbox,
66     commands,
67     iframeWindow,
68     chromeWindow,
69     isBrowserConsole = false
70   ) {
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
86       );
87     }
88     this.ui = new WebConsoleUI(this);
89     this._destroyer = null;
91     EventEmitter.decorate(this);
92   }
94   recordEvent(event, extra = {}) {
95     this.telemetry.recordEvent(event, "webconsole", null, extra);
96   }
98   get currentTarget() {
99     return this.commands.targetCommand.targetFront;
100   }
102   get resourceCommand() {
103     return this.commands.resourceCommand;
104   }
106   /**
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.
112    * @type nsIDOMWindow
113    */
114   get chromeUtilsWindow() {
115     if (this.browserWindow) {
116       return this.browserWindow;
117     }
118     return DevToolsUtils.getTopWindow(this.chromeWindow);
119   }
121   get gViewSourceUtils() {
122     return this.chromeUtilsWindow.gViewSourceUtils;
123   }
125   getFrontByID(id) {
126     return this.commands.client.getFrontByID(id);
127   }
129   /**
130    * Initialize the Web Console instance.
131    *
132    * @param {Boolean} emitCreatedEvent: Defaults to true. If false is passed,
133    *        We won't be sending the 'web-console-created' event.
134    *
135    * @return object
136    *         A promise for the initialization.
137    */
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");
145     }
146   }
148   /**
149    * The JSTerm object that manages the console's input.
150    * @see webconsole.js::JSTerm
151    * @type object
152    */
153   get jsterm() {
154     return this.ui ? this.ui.jsterm : null;
155   }
157   /**
158    * Get the value from the input field.
159    * @returns {String|null} returns null if there's no input.
160    */
161   getInputValue() {
162     if (!this.jsterm) {
163       return null;
164     }
166     return this.jsterm._getValue();
167   }
169   inputHasSelection() {
170     const { editor } = this.jsterm || {};
171     return editor && !!editor.getSelection();
172   }
174   getInputSelection() {
175     if (!this.jsterm || !this.jsterm.editor) {
176       return null;
177     }
178     return this.jsterm.editor.getSelection();
179   }
181   /**
182    * Sets the value of the input field (command line)
183    *
184    * @param {String} newValue: The new value to set.
185    */
186   setInputValue(newValue) {
187     if (!this.jsterm) {
188       return;
189     }
191     this.jsterm._setValue(newValue);
192   }
194   focusInput() {
195     return this.jsterm && this.jsterm.focus();
196   }
198   /**
199    * Open a link in a new tab.
200    *
201    * @param string link
202    *        The URL you want to open in a new tab.
203    */
204   openLink(link, e = {}) {
205     openDocLink(link, {
206       relatedToCurrent: true,
207       inBackground: isMacOS ? e.metaKey : e.ctrlKey,
208     });
209     if (e && typeof e.stopPropagation === "function") {
210       e.stopPropagation();
211     }
212   }
214   /**
215    * Open a link in Firefox's view source.
216    *
217    * @param string sourceURL
218    *        The URL of the file.
219    * @param integer sourceLine
220    *        The line number which should be highlighted.
221    */
222   viewSource(sourceURL, sourceLine) {
223     this.gViewSourceUtils.viewSource({
224       URL: sourceURL,
225       lineNumber: sourceLine || -1,
226     });
227   }
229   /**
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.
233    *
234    * Manually handle the case where toolbox does not exist (Browser Console).
235    *
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.
242    */
243   async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
244     const { toolbox } = this;
245     if (!toolbox) {
246       this.viewSource(sourceURL, sourceLine, sourceColumn);
247       return;
248     }
250     await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
251     this.ui.emitForTests("source-in-debugger-opened");
252   }
254   /**
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.
257    *
258    * @return {String}
259    *         The Frame Actor ID.
260    *         If the debugger is not open or if it's not paused, then |null| is
261    *         returned.
262    */
263   getSelectedFrameActorID() {
264     const { toolbox } = this;
265     if (!toolbox) {
266       return null;
267     }
268     const panel = toolbox.getPanel("jsdebugger");
270     if (!panel) {
271       return null;
272     }
274     return panel.getSelectedFrameActorID();
275   }
277   /**
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, …).
281    *
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`.
291    */
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");
299     if (panel) {
300       return panel.getMappedExpression(expression);
301     }
303     if (expression.includes("await ")) {
304       const shouldMapBindings = false;
305       const shouldMapAwait = true;
306       const res = this.parserWorker.mapExpression(
307         expression,
308         null,
309         null,
310         shouldMapBindings,
311         shouldMapAwait
312       );
313       return res;
314     }
316     return null;
317   }
319   getMappedVariables() {
320     const { toolbox } = this;
321     return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
322   }
324   get parserWorker() {
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...
327     if (this.toolbox) {
328       return this.toolbox.parserWorker;
329     }
331     if (this._parserWorker) {
332       return this._parserWorker;
333     }
335     const {
336       ParserDispatcher,
337     } = require("resource://devtools/client/debugger/src/workers/parser/index.js");
339     this._parserWorker = new ParserDispatcher();
340     return this._parserWorker;
341   }
343   /**
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.
347    *
348    * @return object|null
349    *         A Selection referring to the currently selected node in the
350    *         Inspector.
351    *         If the inspector was never opened, or no node was ever selected,
352    *         then |null| is returned.
353    */
354   getInspectorSelection() {
355     const { toolbox } = this;
356     if (!toolbox) {
357       return null;
358     }
359     const panel = toolbox.getPanel("inspector");
360     if (!panel || !panel.selection) {
361       return null;
362     }
363     return panel.selection;
364   }
366   async onViewSourceInDebugger({ id, url, line, column }) {
367     if (this.toolbox) {
368       await this.toolbox.viewSourceInDebugger(url, line, column, id);
370       this.recordEvent("jump_to_source");
371       this.emitForTests("source-in-debugger-opened");
372     }
373   }
375   async onViewSourceInStyleEditor({ url, line, column }) {
376     if (!this.toolbox) {
377       return;
378     }
379     await this.toolbox.viewSourceInStyleEditorByURL(url, line, column);
380     this.recordEvent("jump_to_source");
381   }
383   async openNetworkPanel(requestId) {
384     if (!this.toolbox) {
385       return;
386     }
387     const netmonitor = await this.toolbox.selectTool("netmonitor");
388     await netmonitor.panelWin.Netmonitor.inspectRequest(requestId);
389   }
391   getHighlighter() {
392     if (!this.toolbox) {
393       return null;
394     }
396     if (this._highlighter) {
397       return this._highlighter;
398     }
400     this._highlighter = this.toolbox.getHighlighter();
401     return this._highlighter;
402   }
404   async resendNetworkRequest(requestId) {
405     if (!this.toolbox) {
406       return;
407     }
409     const api = await this.toolbox.getNetMonitorAPI();
410     await api.resendRequest(requestId);
411   }
413   async openNodeInInspector(grip) {
414     if (!this.toolbox) {
415       return;
416     }
418     const onSelectInspector = this.toolbox.selectTool(
419       "inspector",
420       "inspect_dom"
421     );
423     const onNodeFront = this.toolbox.target
424       .getFront("inspector")
425       .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip));
427     const [nodeFront, inspectorPanel] = await Promise.all([
428       onNodeFront,
429       onSelectInspector,
430     ]);
432     const onInspectorUpdated = inspectorPanel.once("inspector-updated");
433     const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, {
434       reason: "console",
435     });
437     await Promise.all([onNodeFrontSet, onInspectorUpdated]);
438   }
440   /**
441    * Destroy the object. Call this method to avoid memory leaks when the Web
442    * Console is closed.
443    *
444    * @return object
445    *         A promise object that is resolved once the Web Console is closed.
446    */
447   destroy() {
448     if (!this.hudId) {
449       return;
450     }
452     if (this.ui) {
453       this.ui.destroy();
454     }
456     if (this._parserWorker) {
457       this._parserWorker.stop();
458       this._parserWorker = null;
459     }
461     const id = Utils.supportsString(this.hudId);
462     Services.obs.notifyObservers(id, "web-console-destroyed");
463     this.hudId = null;
465     this.emit("destroyed");
466   }
469 module.exports = WebConsole;