Bug 1946184 - Fix computing the CSD margin right after calling HideWindowChrome(...
[gecko.git] / devtools / client / webconsole / actions / input.js
blob258f7844e151dba62b554610ad01b5d2fa67a3f0
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 const {
8 Utils: WebConsoleUtils,
9 } = require("resource://devtools/client/webconsole/utils.js");
10 const {
11 EVALUATE_EXPRESSION,
12 SET_TERMINAL_INPUT,
13 SET_TERMINAL_EAGER_RESULT,
14 EDITOR_PRETTY_PRINT,
15 HELP_URL,
16 } = require("resource://devtools/client/webconsole/constants.js");
17 const {
18 getAllPrefs,
19 } = require("resource://devtools/client/webconsole/selectors/prefs.js");
20 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
21 const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
23 loader.lazyServiceGetter(
24 this,
25 "clipboardHelper",
26 "@mozilla.org/widget/clipboardhelper;1",
27 "nsIClipboardHelper"
29 loader.lazyRequireGetter(
30 this,
31 "messagesActions",
32 "resource://devtools/client/webconsole/actions/messages.js"
34 loader.lazyRequireGetter(
35 this,
36 "historyActions",
37 "resource://devtools/client/webconsole/actions/history.js"
39 loader.lazyRequireGetter(
40 this,
41 "ConsoleCommand",
42 "resource://devtools/client/webconsole/types.js",
43 true
45 loader.lazyRequireGetter(
46 this,
47 "netmonitorBlockingActions",
48 "resource://devtools/client/netmonitor/src/actions/request-blocking.js"
51 loader.lazyRequireGetter(
52 this,
53 ["saveScreenshot", "captureAndSaveScreenshot"],
54 "resource://devtools/client/shared/screenshot.js",
55 true
57 loader.lazyRequireGetter(
58 this,
59 "createSimpleTableMessage",
60 "resource://devtools/client/webconsole/utils/messages.js",
61 true
63 loader.lazyRequireGetter(
64 this,
65 "getSelectedTarget",
66 "resource://devtools/shared/commands/target/selectors/targets.js",
67 true
70 async function getMappedExpression(hud, expression) {
71 let mapResult;
72 try {
73 mapResult = await hud.getMappedExpression(expression);
74 } catch (e) {
75 console.warn("Error when calling getMappedExpression", e);
78 let mapped = null;
79 if (mapResult) {
80 ({ expression, mapped } = mapResult);
82 return { expression, mapped };
85 function evaluateExpression(expression, from = "input") {
86 return async ({ dispatch, webConsoleUI, hud, commands }) => {
87 if (!expression) {
88 expression = hud.getInputSelection() || hud.getInputValue();
90 if (!expression) {
91 return null;
94 // We use the messages action as it's doing additional transformation on the message.
95 const { messages } = dispatch(
96 messagesActions.messagesAdd([
97 new ConsoleCommand({
98 messageText: expression,
99 timeStamp: Date.now(),
103 const [consoleCommandMessage] = messages;
105 dispatch({
106 type: EVALUATE_EXPRESSION,
107 expression,
108 from,
111 WebConsoleUtils.usageCount++;
113 let mapped;
114 ({ expression, mapped } = await getMappedExpression(hud, expression));
116 // Even if the evaluation fails,
117 // we still need to pass the error response to onExpressionEvaluated.
118 const onSettled = res => res;
120 const response = await commands.scriptCommand
121 .execute(expression, {
122 frameActor: hud.getSelectedFrameActorID(),
123 selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
124 selectedTargetFront: getSelectedTarget(
125 webConsoleUI.hud.commands.targetCommand.store.getState()
127 mapped,
128 // Allow breakpoints to be triggerred and the evaluated source to be shown in debugger UI
129 disableBreaks: false,
131 .then(onSettled, onSettled);
133 const serverConsoleCommandTimestamp = response.startTime;
135 // In case of remote debugging, it might happen that the debuggee page does not have
136 // the exact same clock time as the client. This could cause some ordering issues
137 // where the result message is displayed *before* the expression that lead to it.
138 if (
139 serverConsoleCommandTimestamp &&
140 consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp
142 // If we're in such case, we remove the original command message, and add it again,
143 // with the timestamp coming from the server.
144 dispatch(messagesActions.messageRemove(consoleCommandMessage.id));
145 dispatch(
146 messagesActions.messagesAdd([
147 new ConsoleCommand({
148 messageText: expression,
149 timeStamp: serverConsoleCommandTimestamp,
155 return dispatch(onExpressionEvaluated(response));
160 * The JavaScript evaluation response handler.
162 * @private
163 * @param {Object} response
164 * The message received from the server.
166 function onExpressionEvaluated(response) {
167 return async ({ dispatch, webConsoleUI }) => {
168 if (response.error) {
169 console.error(`Evaluation error`, response.error, ": ", response.message);
170 return;
173 // If the evaluation was a top-level await expression that was rejected, there will
174 // be an uncaught exception reported, so we don't need to do anything.
175 if (response.topLevelAwaitRejected === true) {
176 return;
179 if (!response.helperResult) {
180 webConsoleUI.wrapper.dispatchMessageAdd(response);
181 return;
184 await dispatch(handleHelperResult(response));
188 function handleHelperResult(response) {
189 // eslint-disable-next-line complexity
190 return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => {
191 const { result, helperResult } = response;
192 const helperHasRawOutput = !!helperResult?.rawOutput;
194 if (helperResult?.type) {
195 switch (helperResult.type) {
196 case "exception":
197 dispatch(
198 messagesActions.messagesAdd([
200 level: "error",
201 arguments: [helperResult.message],
202 chromeContext: true,
203 resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
207 break;
208 case "clearOutput":
209 dispatch(messagesActions.messagesClear());
210 break;
211 case "clearHistory":
212 dispatch(historyActions.clearHistory());
213 break;
214 case "historyOutput":
215 const history = getState().history.entries || [];
216 const columns = new Map([
217 ["_index", "(index)"],
218 ["expression", "Expressions"],
220 dispatch(
221 messagesActions.messagesAdd([
223 ...createSimpleTableMessage(
224 columns,
225 history.map((expression, index) => {
226 return { _index: index, expression };
232 break;
233 case "inspectObject": {
234 const objectActor = helperResult.object;
235 if (hud.toolbox && !helperResult.forceExpandInConsole) {
236 hud.toolbox.inspectObjectActor(objectActor);
237 } else {
238 webConsoleUI.inspectObjectActor(objectActor);
240 break;
242 case "help":
243 hud.openLink(HELP_URL);
244 break;
245 case "copyValueToClipboard":
246 clipboardHelper.copyString(helperResult.value);
247 dispatch(
248 messagesActions.messagesAdd([
250 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
251 message: l10n.getStr(
252 "webconsole.message.commands.copyValueToClipboard"
257 break;
258 case "screenshotOutput":
259 const { args, value } = helperResult;
260 const targetFront =
261 getSelectedTarget(hud.commands.targetCommand.store.getState()) ||
262 hud.currentTarget;
263 let screenshotMessages;
265 // @backward-compat { version 87 } The screenshot-content actor isn't available
266 // in older server.
267 // With an old server, the console actor captures the screenshot when handling
268 // the command, and send it to the client which only needs to save it to a file.
269 // With a new server, the server simply acknowledges the command,
270 // and the client will drive the whole screenshot process (capture and save).
271 if (targetFront.hasActor("screenshotContent")) {
272 screenshotMessages = await captureAndSaveScreenshot(
273 targetFront,
274 webConsoleUI.getPanelWindow(),
275 args
277 } else {
278 screenshotMessages = await saveScreenshot(
279 webConsoleUI.getPanelWindow(),
280 args,
281 value
285 if (screenshotMessages && screenshotMessages.length) {
286 dispatch(
287 messagesActions.messagesAdd(
288 screenshotMessages.map(message => ({
289 level: message.level || "log",
290 arguments: [message.text],
291 chromeContext: true,
292 resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
297 break;
298 case "blockURL":
299 const blockURL = helperResult.args.url;
300 // The console actor isn't able to block the request as the console actor runs in the content
301 // process, while the request has to be blocked from the parent process.
302 // Then, calling the Netmonitor action will only update the visual state of the Netmonitor,
303 // but we also have to block the request via the NetworkParentActor.
304 await hud.commands.networkCommand.blockRequestForUrl(blockURL);
305 toolbox
306 .getPanel("netmonitor")
307 ?.panelWin.store.dispatch(
308 netmonitorBlockingActions.addBlockedUrl(blockURL)
311 dispatch(
312 messagesActions.messagesAdd([
314 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
315 message: l10n.getFormatStr(
316 "webconsole.message.commands.blockedURL",
317 [blockURL]
322 break;
323 case "unblockURL":
324 const unblockURL = helperResult.args.url;
325 await hud.commands.networkCommand.unblockRequestForUrl(unblockURL);
326 toolbox
327 .getPanel("netmonitor")
328 ?.panelWin.store.dispatch(
329 netmonitorBlockingActions.removeBlockedUrl(unblockURL)
332 dispatch(
333 messagesActions.messagesAdd([
335 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
336 message: l10n.getFormatStr(
337 "webconsole.message.commands.unblockedURL",
338 [unblockURL]
343 // early return as we already dispatched necessary messages.
344 return;
346 // Sent when using ":command --help or :command --usage"
347 // to help discover command arguments.
349 // The remote runtime will tell us about the usage as it may
350 // be different from the client one.
351 case "usage":
352 dispatch(
353 messagesActions.messagesAdd([
355 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
356 message: helperResult.message,
360 break;
362 case "traceOutput":
363 // Nothing in particular to do.
364 // The JSTRACER_STATE resource will report the start/stop of the profiler.
365 break;
369 const hasErrorMessage =
370 response.exceptionMessage ||
371 (helperResult && helperResult.type === "error");
373 // Hide undefined results coming from helper functions.
374 const hasUndefinedResult =
375 result && typeof result == "object" && result.type == "undefined";
377 if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) {
378 dispatch(messagesActions.messagesAdd([response]));
383 function focusInput() {
384 return ({ hud }) => {
385 return hud.focusInput();
389 function setInputValue(value) {
390 return ({ hud }) => {
391 return hud.setInputValue(value);
396 * Request an eager evaluation from the server.
398 * @param {String} expression: The expression to evaluate.
399 * @param {Boolean} force: When true, will request an eager evaluation again, even if
400 * the expression is the same one than the one that was used in
401 * the previous evaluation.
403 function terminalInputChanged(expression, force = false) {
404 return async ({ dispatch, webConsoleUI, hud, commands, getState }) => {
405 const prefs = getAllPrefs(getState());
406 if (!prefs.eagerEvaluation) {
407 return null;
410 const { terminalInput = "" } = getState().history;
412 // Only re-evaluate if the expression did change.
413 if (
414 (!terminalInput && !expression) ||
415 (typeof terminalInput === "string" &&
416 typeof expression === "string" &&
417 expression.trim() === terminalInput.trim() &&
418 !force)
420 return null;
423 dispatch({
424 type: SET_TERMINAL_INPUT,
425 expression: expression.trim(),
428 // There's no need to evaluate an empty string.
429 if (!expression || !expression.trim()) {
430 return dispatch({
431 type: SET_TERMINAL_EAGER_RESULT,
432 expression,
433 result: null,
437 let mapped;
438 ({ expression, mapped } = await getMappedExpression(hud, expression));
440 // We don't want to evaluate top-level await expressions (see Bug 1786805)
441 if (mapped?.await) {
442 return dispatch({
443 type: SET_TERMINAL_EAGER_RESULT,
444 expression,
445 result: null,
449 const response = await commands.scriptCommand.execute(expression, {
450 frameActor: hud.getSelectedFrameActorID(),
451 selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
452 selectedTargetFront: getSelectedTarget(
453 hud.commands.targetCommand.store.getState()
455 mapped,
456 eager: true,
459 return dispatch({
460 type: SET_TERMINAL_EAGER_RESULT,
461 result: getEagerEvaluationResult(response),
467 * Refresh the current eager evaluation by requesting a new eager evaluation.
469 function updateInstantEvaluationResultForCurrentExpression() {
470 return ({ getState, dispatch }) =>
471 dispatch(terminalInputChanged(getState().history.terminalInput, true));
474 function getEagerEvaluationResult(response) {
475 const result = response.exception || response.result;
476 // Don't show syntax errors results to the user.
477 if (result?.isSyntaxError || (result && result.type == "undefined")) {
478 return null;
481 return result;
484 function prettyPrintEditor() {
485 return {
486 type: EDITOR_PRETTY_PRINT,
490 module.exports = {
491 evaluateExpression,
492 focusInput,
493 setInputValue,
494 terminalInputChanged,
495 updateInstantEvaluationResultForCurrentExpression,
496 prettyPrintEditor,