Bug 1602489 - Basic eager evaluation support, r=nchevobbe.

Differential Revision: https://phabricator.services.mozilla.com/D56393

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Brian Hackett 2019-12-12 21:48:03 +00:00
Родитель d3103de230
Коммит 662a021b39
20 изменённых файлов: 363 добавлений и 14 удалений

Просмотреть файл

@ -2157,6 +2157,10 @@ pref("devtools.webconsole.filter.netxhr", false);
// Webconsole autocomplete preference
pref("devtools.webconsole.input.autocomplete",true);
// Set to true to eagerly show the results of webconsole terminal evaluations
// when they don't have side effects.
pref("devtools.webconsole.input.eagerEvaluation", false);
// Browser console filters
pref("devtools.browserconsole.filter.error", true);
pref("devtools.browserconsole.filter.warn", true);

Просмотреть файл

@ -499,6 +499,10 @@ html #webconsole-notificationbox {
fill: var(--theme-icon-checked-color);
}
.eager-evaluation-result * {
color: var(--theme-comment) !important;
}
.webconsole-app .cm-auto-complete-shadow-text::after {
content: attr(title);
color: var(--theme-comment);

Просмотреть файл

@ -5,7 +5,12 @@
"use strict";
const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
const { EVALUATE_EXPRESSION } = require("devtools/client/webconsole/constants");
const {
EVALUATE_EXPRESSION,
SET_TERMINAL_INPUT,
SET_TERMINAL_EAGER_RESULT,
} = require("devtools/client/webconsole/constants");
const { getAllPrefs } = require("devtools/client/webconsole/selectors/prefs");
loader.lazyServiceGetter(
this,
@ -36,6 +41,21 @@ loader.lazyRequireGetter(
);
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
async function getMappedExpression(hud, expression) {
let mapResult;
try {
mapResult = await hud.getMappedExpression(expression);
} catch (e) {
console.warn("Error when calling getMappedExpression", e);
}
let mapped = null;
if (mapResult) {
({ expression, mapped } = mapResult);
}
return { expression, mapped };
}
function evaluateExpression(expression) {
return async ({ dispatch, webConsoleUI, hud, client }) => {
if (!expression) {
@ -61,16 +81,8 @@ function evaluateExpression(expression) {
WebConsoleUtils.usageCount++;
let mappedExpressionRes;
try {
mappedExpressionRes = await hud.getMappedExpression(expression);
} catch (e) {
console.warn("Error when calling getMappedExpression", e);
}
expression = mappedExpressionRes
? mappedExpressionRes.expression
: expression;
let mapped;
({ expression, mapped } = await getMappedExpression(hud, expression));
const { frameActor, webConsoleFront } = webConsoleUI.getFrameActor();
@ -83,7 +95,7 @@ function evaluateExpression(expression) {
frameActor,
selectedNodeFront: webConsoleUI.getSelectedNodeFront(),
webConsoleFront,
mapped: mappedExpressionRes ? mappedExpressionRes.mapped : null,
mapped,
})
.then(onSettled, onSettled);
@ -195,8 +207,56 @@ function setInputValue(value) {
};
}
function terminalInputChanged(expression) {
return async ({ dispatch, webConsoleUI, hud, client, getState }) => {
const prefs = getAllPrefs(getState());
if (!prefs.eagerEvaluation) {
return;
}
// The server does not support eager evaluation when replaying.
if (hud.currentTarget.isReplayEnabled()) {
return;
}
const originalExpression = expression;
dispatch({
type: SET_TERMINAL_INPUT,
expression,
});
let mapped;
({ expression, mapped } = await getMappedExpression(hud, expression));
const { frameActor, webConsoleFront } = webConsoleUI.getFrameActor();
const response = await client.evaluateJSAsync(expression, {
frameActor,
selectedNodeFront: webConsoleUI.getSelectedNodeFront(),
webConsoleFront,
mapped,
eager: true,
});
const result = response.exception || response.result;
// Don't show syntax errors or undefined results to the user.
if (result.isSyntaxError || result.type == "undefined") {
return;
}
// eslint-disable-next-line consistent-return
return dispatch({
type: SET_TERMINAL_EAGER_RESULT,
expression: originalExpression,
result,
});
};
}
module.exports = {
evaluateExpression,
focusInput,
setInputValue,
terminalInputChanged,
};

Просмотреть файл

@ -39,6 +39,9 @@ const JSTerm = createFactory(
const ConfirmDialog = createFactory(
require("devtools/client/webconsole/components/Input/ConfirmDialog")
);
const EagerEvaluation = createFactory(
require("devtools/client/webconsole/components/Input/EagerEvaluation")
);
// And lazy load the ones that may not be used.
loader.lazyGetter(this, "SideBar", () =>
@ -332,6 +335,10 @@ class App extends Component {
});
}
renderEagerEvaluation() {
return EagerEvaluation();
}
renderReverseSearch() {
const { serviceContainer, reverseSearchInitialValue } = this.props;
@ -411,6 +418,7 @@ class App extends Component {
const consoleOutput = this.renderConsoleOutput();
const notificationBox = this.renderNotificationBox();
const jsterm = this.renderJsTerm();
const eager = this.renderEagerEvaluation();
const reverseSearch = this.renderReverseSearch();
const sidebar = this.renderSideBar();
const confirmDialog = this.renderConfirmDialog();
@ -422,7 +430,8 @@ class App extends Component {
{ className: "flexible-output-input", key: "in-out-container" },
consoleOutput,
notificationBox,
jsterm
jsterm,
eager
),
editorMode
? GridElementWidthResizer({

Просмотреть файл

@ -0,0 +1,66 @@
/* 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";
const { Component } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const {
getTerminalEagerResult,
} = require("devtools/client/webconsole/selectors/history");
loader.lazyGetter(this, "REPS", function() {
return require("devtools/client/shared/components/reps/reps").REPS;
});
loader.lazyGetter(this, "MODE", function() {
return require("devtools/client/shared/components/reps/reps").MODE;
});
loader.lazyRequireGetter(
this,
"PropTypes",
"devtools/client/shared/vendor/react-prop-types"
);
/**
* Show the results of evaluating the current terminal text, if possible.
*/
class EagerEvaluation extends Component {
static get propTypes() {
return {
terminalEagerResult: PropTypes.any,
};
}
constructor(props) {
super(props);
}
render() {
const { terminalEagerResult } = this.props;
if (terminalEagerResult !== null) {
return dom.span(
{
className: "devtools-monospace eager-evaluation-result",
},
REPS.Rep({
object: terminalEagerResult.getGrip
? terminalEagerResult.getGrip()
: terminalEagerResult,
mode: MODE.LONG,
})
);
}
return null;
}
}
function mapStateToProps(state) {
return {
terminalEagerResult: getTerminalEagerResult(state),
};
}
module.exports = connect(mapStateToProps)(EagerEvaluation);

Просмотреть файл

@ -100,6 +100,8 @@ class JSTerm extends Component {
editorToggle: PropTypes.func.isRequired,
// Dismiss the editor onboarding UI.
editorOnboardingDismiss: PropTypes.func.isRequired,
// Set the last JS input value.
terminalInputChanged: PropTypes.func.isRequired,
// Is the input in editor mode.
editorMode: PropTypes.bool,
editorWidth: PropTypes.number,
@ -126,6 +128,14 @@ class JSTerm extends Component {
// The delay should be small enough to be unnoticed by the user.
this.autocompleteUpdate = debounce(this.props.autocompleteUpdate, 75, this);
// Updates to the terminal input which can trigger eager evaluations are
// similarly debounced.
this.terminalInputChanged = debounce(
this.props.terminalInputChanged,
75,
this
);
// Because the autocomplete has a slight delay (75ms), there can be time where the
// codeMirror completion text is out-of-date, which might lead to issue when the user
// accept the autocompletion while the update of the completion text is still pending.
@ -618,6 +628,7 @@ class JSTerm extends Component {
*/
_setValue(newValue = "") {
this.lastInputValue = newValue;
this.terminalInputChanged(newValue);
if (this.editor) {
// In order to get the autocomplete popup to work properly, we need to set the
@ -767,6 +778,7 @@ class JSTerm extends Component {
this.autocompleteUpdate();
}
this.lastInputValue = value;
this.terminalInputChanged(value);
}
}
@ -1264,6 +1276,8 @@ function mapDispatchToProps(dispatch) {
dispatch(actions.evaluateExpression(expression)),
editorToggle: () => dispatch(actions.editorToggle()),
editorOnboardingDismiss: () => dispatch(actions.editorOnboardingDismiss()),
terminalInputChanged: value =>
dispatch(actions.terminalInputChanged(value)),
};
}

Просмотреть файл

@ -5,6 +5,7 @@
DevToolsModules(
'ConfirmDialog.js',
'EagerEvaluation.js',
'EditorToolbar.js',
'JSTerm.js',
'ReverseSearchInput.css',

Просмотреть файл

@ -15,6 +15,8 @@ const actionTypes = {
EDITOR_TOGGLE: "EDITOR_TOGGLE",
EDITOR_ONBOARDING_DISMISS: "EDITOR_ONBOARDING_DISMISS",
EVALUATE_EXPRESSION: "EVALUATE_EXPRESSION",
SET_TERMINAL_INPUT: "SET_TERMINAL_INPUT",
SET_TERMINAL_EAGER_RESULT: "SET_TERMINAL_EAGER_RESULT",
FILTER_TEXT_SET: "FILTER_TEXT_SET",
FILTER_TOGGLE: "FILTER_TOGGLE",
FILTERS_CLEAR: "FILTERS_CLEAR",
@ -84,6 +86,7 @@ const prefs = {
// We use the same pref to enable the sidebar on webconsole and browser console.
SIDEBAR_TOGGLE: "devtools.webconsole.sidebarToggle",
AUTOCOMPLETE: "devtools.webconsole.input.autocomplete",
EAGER_EVALUATION: "devtools.webconsole.input.eagerEvaluation",
GROUP_WARNINGS: "devtools.webconsole.groupWarningMessages",
BROWSER_TOOLBOX_FISSION: "devtools.browsertoolbox.fission",
},

Просмотреть файл

@ -15,6 +15,8 @@ const {
REVERSE_SEARCH_INPUT_CHANGE,
REVERSE_SEARCH_BACK,
REVERSE_SEARCH_NEXT,
SET_TERMINAL_INPUT,
SET_TERMINAL_EAGER_RESULT,
} = require("devtools/client/webconsole/constants");
/**
@ -39,6 +41,9 @@ function getInitialState() {
reverseSearchEnabled: false,
currentReverseSearchResults: null,
currentReverseSearchResultsPosition: null,
terminalInput: null,
terminalEagerResult: null,
};
}
@ -61,6 +66,10 @@ function history(state = getInitialState(), action, prefsState) {
return reverseSearchBack(state);
case REVERSE_SEARCH_NEXT:
return reverseSearchNext(state);
case SET_TERMINAL_INPUT:
return setTerminalInput(state, action.expression);
case SET_TERMINAL_EAGER_RESULT:
return setTerminalEagerResult(state, action.expression, action.result);
}
return state;
}
@ -217,4 +226,22 @@ function reverseSearchNext(state) {
};
}
function setTerminalInput(state, expression) {
return {
...state,
terminalInput: expression,
terminalEagerResult: null,
};
}
function setTerminalEagerResult(state, expression, result) {
if (state.terminalInput == expression) {
return {
...state,
terminalEagerResult: result,
};
}
return state;
}
exports.history = history;

Просмотреть файл

@ -81,6 +81,11 @@ function getReverseSearchTotalResults(state) {
return currentReverseSearchResults.length;
}
function getTerminalEagerResult(state) {
const { history } = state;
return history.terminalEagerResult;
}
module.exports = {
getHistory,
getHistoryEntries,
@ -88,4 +93,5 @@ module.exports = {
getReverseSearchResult,
getReverseSearchResultPosition,
getReverseSearchTotalResults,
getTerminalEagerResult,
};

Просмотреть файл

@ -49,6 +49,7 @@ function configureStore(webConsoleUI, options = {}) {
options.logLimit || Math.max(getIntPref("devtools.hud.loglimit"), 1);
const sidebarToggle = getBoolPref(PREFS.FEATURES.SIDEBAR_TOGGLE);
const autocomplete = getBoolPref(PREFS.FEATURES.AUTOCOMPLETE);
const eagerEvaluation = getBoolPref(PREFS.FEATURES.EAGER_EVALUATION);
const groupWarnings = getBoolPref(PREFS.FEATURES.GROUP_WARNINGS);
const historyCount = getIntPref(PREFS.UI.INPUT_HISTORY_COUNT);
@ -57,6 +58,7 @@ function configureStore(webConsoleUI, options = {}) {
logLimit,
sidebarToggle,
autocomplete,
eagerEvaluation,
historyCount,
groupWarnings,
}),

Просмотреть файл

@ -251,6 +251,7 @@ skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1
[browser_jsterm_ctrl_key_nav.js]
skip-if = os != 'mac' # The tested ctrl+key shortcuts are OSX only
[browser_jsterm_document_no_xray.js]
[browser_jsterm_eager_evaluation.js]
[browser_jsterm_editor.js]
[browser_jsterm_editor_disabled_history_nav_with_keyboard.js]
[browser_jsterm_editor_enter.js]

Просмотреть файл

@ -0,0 +1,49 @@
/* 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";
const TEST_URI = `data:text/html;charset=utf-8,
<script>
let x = 3, y = 4;
function foo() {
x = 10;
}
</script>
`;
// Basic testing of eager evaluation functionality. Expressions which can be
// eagerly evaluated should show their results, and expressions with side
// effects should not perform those side effects.
add_task(async function() {
await pushPref("devtools.webconsole.input.eagerEvaluation", true);
const hud = await openNewTabAndConsole(TEST_URI);
setInputValue(hud, "x + y");
await waitForEagerEvaluationResult(hud, "7");
setInputValue(hud, "x + y + undefined");
await waitForEagerEvaluationResult(hud, "NaN");
setInputValue(hud, "-x / 0");
await waitForEagerEvaluationResult(hud, "-Infinity");
setInputValue(hud, "x = 10");
await waitForNoEagerEvaluationResult(hud);
setInputValue(hud, "x + 1");
await waitForEagerEvaluationResult(hud, "4");
setInputValue(hud, "foo()");
await waitForNoEagerEvaluationResult(hud);
setInputValue(hud, "x + 2");
await waitForEagerEvaluationResult(hud, "5");
setInputValue(hud, "x +");
await waitForNoEagerEvaluationResult(hud);
setInputValue(hud, "x + z");
await waitForEagerEvaluationResult(hud, /ReferenceError/);
});

Просмотреть файл

@ -1154,6 +1154,31 @@ function isReverseSearchInputFocused(hud) {
return document.activeElement == reverseSearchInput && documentIsFocused;
}
async function waitForEagerEvaluationResult(hud, text) {
await waitUntil(() => {
const elem = hud.ui.outputNode.querySelector(".eager-evaluation-result");
if (elem) {
if (text instanceof RegExp) {
return text.test(elem.innerText);
}
return elem.innerText == text;
}
return false;
});
ok(true, `Got eager evaluation result ${text}`);
}
// This just makes sure the eager evaluation result disappears. This will pass
// even for inputs which eventually have a result because nothing will be shown
// while the evaluation happens. Waiting here does make sure that a previous
// input was processed and sent down to the server for evaluating.
async function waitForNoEagerEvaluationResult(hud) {
await waitUntil(() => {
return !hud.ui.outputNode.querySelector(".eager-evaluation-result");
});
ok(true, `Eager evaluation result disappeared`);
}
/**
* Selects a node in the inspector.
*

Просмотреть файл

@ -27,6 +27,7 @@ pref("devtools.webconsole.sidebarToggle", true);
pref("devtools.webconsole.groupWarningMessages", false);
pref("devtools.webconsole.input.editor", false);
pref("devtools.webconsole.input.autocomplete", true);
pref("devtools.webconsole.input.eagerEvaluation", false);
pref("devtools.browserconsole.contentMessages", true);
pref("devtools.webconsole.input.editorWidth", 800);
pref("devtools.webconsole.input.editorOnboarding", true);

Просмотреть файл

@ -1145,6 +1145,7 @@ const WebConsoleActor = ActorClassWithSpec(webconsoleSpec, {
url: request.url,
selectedNodeActor: request.selectedNodeActor,
selectedObjectActor: request.selectedObjectActor,
eager: request.eager,
};
const { mapped } = request;

Просмотреть файл

@ -105,10 +105,15 @@ function isObject(value) {
exports.evalWithDebugger = function(string, options = {}, webConsole) {
const evalString = getEvalInput(string);
const { frame, dbg } = getFrameDbg(options, webConsole);
// early return for replay
if (dbg.replaying) {
if (options.eager) {
throw new Error("Eager evaluations are not supported while replaying");
}
return evalReplay(frame, dbg, evalString);
}
const { dbgWindow, bindSelf } = getDbgWindow(options, dbg, webConsole);
const helpers = getHelpers(dbgWindow, options, webConsole);
const { bindings, helperCache } = bindCommands(
@ -126,6 +131,11 @@ exports.evalWithDebugger = function(string, options = {}, webConsole) {
updateConsoleInputEvaluation(dbg, dbgWindow, webConsole);
let sideEffectData = null;
if (options.eager) {
sideEffectData = preventSideEffects(dbg);
}
const result = getEvalResult(
evalString,
evalOptions,
@ -134,6 +144,10 @@ exports.evalWithDebugger = function(string, options = {}, webConsole) {
dbgWindow
);
if (options.eager) {
allowSideEffects(dbg, sideEffectData);
}
const { helperResult } = helpers;
// Clean up helpers helpers and bindings
@ -163,7 +177,7 @@ function getEvalResult(string, evalOptions, bindings, frame, dbgWindow) {
// Attempt to initialize any declarations found in the evaluated string
// since they may now be stuck in an "initializing" state due to the
// error. Already-initialized bindings will be ignored.
if ("throw" in result) {
if (result && "throw" in result) {
parseErrorOutput(dbgWindow, string);
}
return result;
@ -236,6 +250,62 @@ function parseErrorOutput(dbgWindow, string) {
}
}
function preventSideEffects(dbg) {
if (dbg.onEnterFrame || dbg.onNativeCall) {
throw new Error("Debugger has hook installed");
}
const data = {
executedScripts: new Set(),
debuggees: dbg.getDebuggees(),
handler: {
hit: () => null,
},
};
dbg.addAllGlobalsAsDebuggees();
dbg.onEnterFrame = frame => {
const script = frame.script;
if (data.executedScripts.has(script)) {
return;
}
data.executedScripts.add(script);
const offsets = script.getEffectfulOffsets();
for (const offset of offsets) {
script.setBreakpoint(offset, data.handler);
}
};
dbg.onNativeCall = (callee, reason) => {
if (reason == "get") {
// Native getters are never considered effectful.
return undefined;
}
return null;
};
return data;
}
function allowSideEffects(dbg, data) {
for (const script of data.executedScripts) {
script.clearBreakpoint(data.handler);
}
for (const global of dbg.getDebuggees()) {
if (!data.debuggees.includes(global)) {
dbg.removeDebuggee(global);
}
}
dbg.onEnterFrame = undefined;
dbg.onNativeCall = undefined;
}
function updateConsoleInputEvaluation(dbg, dbgWindow, webConsole) {
// Adopt webConsole._lastConsoleInputEvaluation value in the new debugger,
// to prevent "Debugger.Object belongs to a different Debugger" exceptions

Просмотреть файл

@ -273,6 +273,10 @@ class ObjectFront extends FrontClassWithSpec(objectSpec) {
return response;
}
get isSyntaxError() {
return this._grip.preview && this._grip.preview.name == "SyntaxError";
}
}
/**

Просмотреть файл

@ -200,6 +200,7 @@ class WebConsoleFront extends FrontClassWithSpec(webconsoleSpec) {
selectedNodeActor: opts.selectedNodeActor,
selectedObjectActor: opts.selectedObjectActor,
mapped: opts.mapped,
eager: opts.eager,
};
const { resultID } = await super.evaluateJSAsync(options);

Просмотреть файл

@ -153,6 +153,7 @@ const webconsoleSpecPrototype = {
selectedNodeActor: Option(0, "string"),
selectedObjectActor: Option(0, "string"),
mapped: Option(0, "nullable:json"),
eager: Option(0, "nullable:boolean"),
},
response: RetVal("console.evaluatejsasync"),
},