/* 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 . */ import { isConsole } from "../utils/preview"; import { getGrip, getFront } from "../utils/evaluation-result"; import { isLineInScope, isSelectedFrameVisible, getSelectedSource, getSelectedLocation, getSelectedFrame, getCurrentThread, getSelectedException, getSelectedTraceIndex, getAllTraces, } from "../selectors/index"; import { getMappedExpression } from "./expressions"; const { TRACER_FIELDS_INDEXES, } = require("resource://devtools/server/actors/tracer.js"); async function findExpressionMatches(state, editor, tokenPos) { const location = getSelectedLocation(state); if (!location) { return []; } return editor.findBestMatchExpressions(tokenPos); } /** * Get a preview object for the currently selected frame in the JS Tracer. * * @param {object} target * The hovered DOM Element within CodeMirror rendering. * @param {object} tokenPos * The CodeMirror position object for the hovered token. * @param {object} editor * The CodeMirror editor object. */ export function getTracerPreview(target, tokenPos, editor) { return async thunkArgs => { const { getState } = thunkArgs; const selectedTraceIndex = getSelectedTraceIndex(getState()); if (selectedTraceIndex == null) { return null; } const trace = getAllTraces(getState())[selectedTraceIndex]; // We may be selecting a mutation trace, which doesn't expose any value, // so only consider method calls. if (trace[TRACER_FIELDS_INDEXES.TYPE] != "enter") { return null; } const matches = await findExpressionMatches(getState(), editor, tokenPos); if (!matches.length) { return null; } let { expression, location } = matches[0]; const source = getSelectedSource(getState()); if (location && source.isOriginal) { const thread = getCurrentThread(getState()); const mapResult = await getMappedExpression( expression, thread, thunkArgs ); if (mapResult) { expression = mapResult.expression; } } const argumentValues = trace[TRACER_FIELDS_INDEXES.ENTER_ARGS]; const argumentNames = trace[TRACER_FIELDS_INDEXES.ENTER_ARG_NAMES]; if (!argumentNames || !argumentValues) { return null; } const argumentIndex = argumentNames.indexOf(expression); if (argumentIndex == -1) { return null; } const result = argumentValues[argumentIndex]; // Values are either primitives, or an Object Front const resultGrip = result?.getGrip ? result?.getGrip() : result; const root = { // Force updating the ObjectInspector when hovering same-name variable on another trace. // See ObjectInspector.getNodeKey. path: `${selectedTraceIndex}-${expression}`, contents: { value: resultGrip, front: getFront(result), }, }; return { previewType: "tracer", target, tokenPos, cursorPos: target.getBoundingClientRect(), expression, root, resultGrip, }; }; } /** * Get a preview object for the currently paused frame, if paused. * * @param {object} target * The hovered DOM Element within CodeMirror rendering. * @param {object} tokenPos * The CodeMirror position object for the hovered token. * @param {object} editor * The CodeMirror editor object. */ export function getPausedPreview(target, tokenPos, editor) { return async thunkArgs => { const { getState, client } = thunkArgs; if ( !isSelectedFrameVisible(getState()) || !isLineInScope(getState(), tokenPos.line) ) { return null; } const source = getSelectedSource(getState()); if (!source) { return null; } const thread = getCurrentThread(getState()); const selectedFrame = getSelectedFrame(getState()); if (!selectedFrame) { return null; } const matches = await findExpressionMatches(getState(), editor, tokenPos); if (!matches.length) { return null; } let { expression, location } = matches[0]; if (isConsole(expression)) { return null; } if (location && source.isOriginal) { const mapResult = await getMappedExpression( expression, thread, thunkArgs ); if (mapResult) { expression = mapResult.expression; } } const { result, hasException, exception } = await client.evaluate( expression, { frameId: selectedFrame.id, } ); // The evaluation shouldn't return an exception. if (hasException) { const errorClass = exception?.getGrip()?.class || "Error"; throw new Error( `Debugger internal exception: Preview for <${expression}> threw a ${errorClass}` ); } const resultGrip = getGrip(result); // Error case occurs for a token that follows an errored evaluation // https://github.com/firefox-devtools/debugger/pull/8056 // Accommodating for null allows us to show preview for falsy values // line "", false, null, Nan, and more if (resultGrip === null) { return null; } // Handle cases where the result is invisible to the debugger // and not possible to preview. Bug 1548256 if ( resultGrip && resultGrip.class && typeof resultGrip.class === "string" && resultGrip.class.includes("InvisibleToDebugger") ) { return null; } const root = { path: expression, contents: { value: resultGrip, front: getFront(result), }, }; return { previewType: "pause", target, tokenPos, cursorPos: target.getBoundingClientRect(), expression, root, resultGrip, }; }; } export function getExceptionPreview(target, tokenPos, editor) { return async ({ getState }) => { const matches = await findExpressionMatches(getState(), editor, tokenPos); if (!matches.length) { return null; } let exception; // Lezer might return multiple matches in certain scenarios. // Example: For this expression `[].inlineException()` is likely to throw an exception, // but if the user hovers over `inlineException` lezer finds 2 matches : // 1) `inlineException` for `PropertyName`, // 2) `[].inlineException()` for `MemberExpression` // Babel seems to only include the `inlineException`. for (const match of matches) { const tokenColumnStart = match.location.start.column + 1; exception = getSelectedException( getState(), tokenPos.line, tokenColumnStart ); if (exception) { break; } } if (!exception) { return null; } return { target, tokenPos, cursorPos: target.getBoundingClientRect(), exception, }; }; }