--- a/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js
@@ -40,11 +40,11 @@ add_task(async function() {
await inspector.once("console-var-ready");
is(jstermInput.value, "temp1", "second console variable is named temp1");
result = await jsterm.execute();
isnot(result.textContent.indexOf('<p id="console-var-multi">'), -1,
"variable temp1 references correct node");
- jsterm.clearHistory();
+ hud.ui.consoleOutput.dispatchClearHistory();
}
});
--- a/devtools/client/inspector/test/browser_inspector_menu-06-other.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-06-other.js
@@ -1,13 +1,17 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
+const {
+ getHistoryEntries,
+} = require("devtools/client/webconsole/selectors/history");
+
// Tests for menuitem functionality that doesn't fit into any specific category
const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
add_task(async function() {
let { inspector, toolbox, testActor } = await openInspectorForURL(TEST_URL);
await testShowDOMProperties();
await testDuplicateNode();
await testDeleteNode();
await testDeleteTextNode();
@@ -25,17 +29,19 @@ add_task(async function() {
info("Triggering 'Show DOM Properties' and waiting for inspector open");
showDOMPropertiesNode.click();
await consoleOpened;
let webconsoleUI = toolbox.getPanel("webconsole").hud.ui;
let messagesAdded = webconsoleUI.once("new-messages");
await messagesAdded;
info("Checking if 'inspect($0)' was evaluated");
- ok(webconsoleUI.jsterm.history[0] === "inspect($0)");
+
+ let state = webconsoleUI.consoleOutput.getStore().getState();
+ ok(getHistoryEntries(state)[0] === "inspect($0)");
await toolbox.toggleSplitConsole();
}
async function testDuplicateNode() {
info("Testing 'Duplicate Node' menu item for normal elements.");
await selectNode(".duplicate", inspector);
is((await testActor.getNumberOfElementMatches(".duplicate")), 1,
"There should initially be 1 .duplicate node");
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/actions/history.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_PLACEHOLDER,
+} = require("devtools/client/webconsole/constants");
+
+/**
+ * Append a new value in the history of executed expressions,
+ * or overwrite the most recent entry. The most recent entry may
+ * contain the last edited input value that was not evaluated yet.
+ */
+function appendToHistory(expression) {
+ return {
+ type: APPEND_TO_HISTORY,
+ expression: expression,
+ };
+}
+
+/**
+ * Clear the console history altogether. Note that this will not affect
+ * other consoles that are already opened (since they have their own copy),
+ * but it will reset the array for all newly-opened consoles.
+ */
+function clearHistory() {
+ return {
+ type: CLEAR_HISTORY,
+ };
+}
+
+/**
+ * Fired when the console history from previous Firefox sessions is loaded.
+ */
+function historyLoaded(entries) {
+ return {
+ type: HISTORY_LOADED,
+ entries,
+ };
+}
+
+/**
+ * Update place-holder position in the history list.
+ */
+function updatePlaceHolder(direction, expression) {
+ return {
+ type: UPDATE_HISTORY_PLACEHOLDER,
+ direction,
+ expression,
+ };
+}
+
+module.exports = {
+ appendToHistory,
+ clearHistory,
+ historyLoaded,
+ updatePlaceHolder,
+};
--- a/devtools/client/webconsole/actions/index.js
+++ b/devtools/client/webconsole/actions/index.js
@@ -6,13 +6,14 @@
"use strict";
const actionModules = [
require("./filters"),
require("./messages"),
require("./ui"),
require("./notifications"),
+ require("./history"),
];
const actions = Object.assign({}, ...actionModules);
module.exports = actions;
--- a/devtools/client/webconsole/actions/moz.build
+++ b/devtools/client/webconsole/actions/moz.build
@@ -1,12 +1,13 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'filters.js',
+ 'history.js',
'index.js',
'messages.js',
'notifications.js',
'ui.js',
)
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -9,71 +9,89 @@ const Services = require("Services");
loader.lazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
loader.lazyRequireGetter(this, "Debugger", "Debugger");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
-loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor");
const l10n = require("devtools/client/webconsole/webconsole-l10n");
-// Constants used for defining the direction of JSTerm input history navigation.
-const HISTORY_BACK = -1;
-const HISTORY_FORWARD = 1;
-
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
-
-const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount";
const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline";
function gSequenceId() {
return gSequenceId.n++;
}
gSequenceId.n = 0;
+// React & Redux
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");
+
+// History Modules
+const {
+ getHistory,
+ getHistoryValue
+} = require("devtools/client/webconsole/selectors/history");
+const historyActions = require("devtools/client/webconsole/actions/history");
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const {
+ HISTORY_BACK,
+ HISTORY_FORWARD
+} = require("devtools/client/webconsole/constants");
/**
* Create a JSTerminal (a JavaScript command line). This is attached to an
* existing HeadsUpDisplay (a Web Console instance). This code is responsible
* with handling command line input and code evaluation.
*
* @constructor
* @param object webConsoleFrame
* The WebConsoleFrame object that owns this JSTerm instance.
*/
class JSTerm extends Component {
static get propTypes() {
return {
+ // Append new executed expression into history list (action).
+ appendToHistory: PropTypes.func.isRequired,
+ // Remove all entries from the history list (action).
+ clearHistory: PropTypes.func.isRequired,
+ // Returns previous or next value from the history
+ // (depending on direction argument).
+ getValueFromHistory: PropTypes.func.isRequired,
+ // History of executed expression (state).
+ history: PropTypes.object.isRequired,
+ // Console object.
hud: PropTypes.object.isRequired,
- // Handler for clipboard 'paste' event (also used for 'drop' event).
+ // Handler for clipboard 'paste' event (also used for 'drop' event, callback).
onPaste: PropTypes.func,
codeMirrorEnabled: PropTypes.bool,
+ // Update position in the history after executing an expression (action).
+ updatePlaceHolder: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
const {
hud,
} = props;
this.hud = hud;
this.hudId = this.hud.hudId;
- this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT);
- this._loadHistory();
/**
* Stores the data for the last completion.
* @type object
*/
this.lastCompletion = { value: null };
this._keyPress = this._keyPress.bind(this);
@@ -123,21 +141,16 @@ class JSTerm extends Component {
/**
* Tells if the autocomplete popup was navigated since the last open.
*
* @private
* @type boolean
*/
this._autocompletePopupNavigated = false;
- /**
- * History of code that was executed.
- * @type array
- */
- this.history = [];
this.autocompletePopup = null;
this.inputNode = null;
this.completeNode = null;
this.COMPLETE_FORWARD = 0;
this.COMPLETE_BACKWARD = 1;
this.COMPLETE_HINT_ONLY = 2;
this.COMPLETE_PAGEUP = 3;
@@ -209,73 +222,24 @@ class JSTerm extends Component {
this.inputNode.addEventListener("focus", this._focusEventHandler);
this.focus();
}
this.hud.window.addEventListener("blur", this._blurEventHandler);
this.lastInputValue && this.setInputValue(this.lastInputValue);
}
- shouldComponentUpdate() {
- // XXX: For now, everything is handled in an imperative way and we only want React
- // to do the initial rendering of the component.
+ shouldComponentUpdate(nextProps, nextState) {
+ // XXX: For now, everything is handled in an imperative way and we
+ // only want React to do the initial rendering of the component.
// This should be modified when the actual refactoring will take place.
return false;
}
/**
- * Load the console history from previous sessions.
- * @private
- */
- _loadHistory() {
- this.history = [];
- this.historyIndex = this.historyPlaceHolder = 0;
-
- this.historyLoaded = asyncStorage.getItem("webConsoleHistory")
- .then(value => {
- if (Array.isArray(value)) {
- // Since it was gotten asynchronously, there could be items already in
- // the history. It's not likely but stick them onto the end anyway.
- this.history = value.concat(this.history);
-
- // Holds the number of entries in history. This value is incremented
- // in this.execute().
- this.historyIndex = this.history.length;
-
- // Holds the index of the history entry that the user is currently
- // viewing. This is reset to this.history.length when this.execute()
- // is invoked.
- this.historyPlaceHolder = this.history.length;
- }
- }, console.error);
- }
-
- /**
- * Clear the console history altogether. Note that this will not affect
- * other consoles that are already opened (since they have their own copy),
- * but it will reset the array for all newly-opened consoles.
- * @returns Promise
- * Resolves once the changes have been persisted.
- */
- clearHistory() {
- this.history = [];
- this.historyIndex = this.historyPlaceHolder = 0;
- return this.storeHistory();
- }
-
- /**
- * Stores the console history for future console instances.
- * @returns Promise
- * Resolves once the changes have been persisted.
- */
- storeHistory() {
- return asyncStorage.setItem("webConsoleHistory", this.history);
- }
-
- /**
* Getter for the element that holds the messages we display.
* @type Element
*/
get outputNode() {
return this.hud.outputNode;
}
/**
@@ -323,17 +287,17 @@ class JSTerm extends Component {
let helperHasRawOutput = !!(helperResult || {}).rawOutput;
if (helperResult && helperResult.type) {
switch (helperResult.type) {
case "clearOutput":
this.clearOutput();
break;
case "clearHistory":
- this.clearHistory();
+ this.props.clearHistory();
break;
case "inspectObject":
this.inspectObjectActor(helperResult.object);
break;
case "error":
try {
errorMessage = l10n.getStr(helperResult.message);
} catch (ex) {
@@ -389,27 +353,19 @@ class JSTerm extends Component {
let resultCallback = msg => deferred.resolve(msg);
// attempt to execute the content of the inputNode
executeString = executeString || this.getInputValue();
if (!executeString) {
return null;
}
- // Append a new value in the history of executed code, or overwrite the most
- // recent entry. The most recent entry may contain the last edited input
- // value that was not evaluated yet.
- this.history[this.historyIndex++] = executeString;
- this.historyPlaceHolder = this.history.length;
+ // Append executed expression into the history list.
+ this.props.appendToHistory(executeString);
- if (this.history.length > this.inputHistoryCount) {
- this.history.splice(0, this.history.length - this.inputHistoryCount);
- this.historyIndex = this.historyPlaceHolder = this.history.length;
- }
- this.storeHistory();
WebConsoleUtils.usageCount++;
this.setInputValue("");
this.clearCompletion();
let selectedNodeActor = null;
let inspectorSelection = this.hud.owner.getInspectorSelection();
if (inspectorSelection && inspectorSelection.nodeFront) {
selectedNodeActor = inspectorSelection.nodeFront.actorID;
@@ -887,49 +843,36 @@ class JSTerm extends Component {
*
* @param number direction
* History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
*
* @returns boolean
* True if the input value changed, false otherwise.
*/
historyPeruse(direction) {
- if (!this.history.length) {
+ let {
+ history,
+ updatePlaceHolder,
+ getValueFromHistory,
+ } = this.props;
+
+ if (!history.entries.length) {
return false;
}
- // Up Arrow key
- if (direction == HISTORY_BACK) {
- if (this.historyPlaceHolder <= 0) {
- return false;
- }
- let inputVal = this.history[--this.historyPlaceHolder];
+ let newInputValue = getValueFromHistory(direction);
+ let expression = this.getInputValue();
+ updatePlaceHolder(direction, expression);
- // Save the current input value as the latest entry in history, only if
- // the user is already at the last entry.
- // Note: this code does not store changes to items that are already in
- // history.
- if (this.historyPlaceHolder + 1 == this.historyIndex) {
- this.history[this.historyIndex] = this.getInputValue() || "";
- }
-
- this.setInputValue(inputVal);
- } else if (direction == HISTORY_FORWARD) {
- // Down Arrow key
- if (this.historyPlaceHolder >= (this.history.length - 1)) {
- return false;
- }
-
- let inputVal = this.history[++this.historyPlaceHolder];
- this.setInputValue(inputVal);
- } else {
- throw new Error("Invalid argument 0");
+ if (newInputValue != null) {
+ this.setInputValue(newInputValue);
+ return true;
}
- return true;
+ return false;
}
/**
* Test for multiline input.
*
* @return boolean
* True if CR or LF found in node value; else false.
*/
@@ -1399,9 +1342,27 @@ class JSTerm extends Component {
onPaste: onPaste,
onDrop: onPaste,
})
)
);
}
}
-module.exports = JSTerm;
+// Redux connect
+
+function mapStateToProps(state) {
+ return {
+ history: getHistory(state),
+ getValueFromHistory: (direction) => getHistoryValue(state, direction),
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ appendToHistory: (expr) => dispatch(historyActions.appendToHistory(expr)),
+ clearHistory: () => dispatch(historyActions.clearHistory()),
+ updatePlaceHolder: (direction, expression) =>
+ dispatch(historyActions.updatePlaceHolder(direction, expression)),
+ };
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(JSTerm);
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -25,16 +25,20 @@ const actionTypes = {
REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
APPEND_NOTIFICATION: "APPEND_NOTIFICATION",
REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE",
+ APPEND_TO_HISTORY: "APPEND_TO_HISTORY",
+ CLEAR_HISTORY: "CLEAR_HISTORY",
+ HISTORY_LOADED: "HISTORY_LOADED",
+ UPDATE_HISTORY_PLACEHOLDER: "UPDATE_HISTORY_PLACEHOLDER",
};
const prefs = {
PREFS: {
// Filter preferences only have the suffix since they can be used either for the
// webconsole or the browser console.
FILTER: {
ERROR: "filter.error",
@@ -47,16 +51,18 @@ const prefs = {
NETXHR: "filter.netxhr",
},
UI: {
// Filter bar UI preference only have the suffix since it can be used either for
// the webconsole or the browser console.
FILTER_BAR: "ui.filterbar",
// Persist is only used by the webconsole.
PERSIST: "devtools.webconsole.persistlog",
+ // Max number of entries in history list.
+ INPUT_HISTORY_COUNT: "devtools.webconsole.inputHistoryCount",
},
FEATURES: {
// We use the same pref to enable the sidebar on webconsole and browser console.
SIDEBAR_TOGGLE: "devtools.webconsole.sidebarToggle",
JSTERM_CODE_MIRROR: "devtools.webconsole.jsterm.codeMirror",
}
}
};
@@ -133,19 +139,26 @@ const chromeRDPEnums = {
};
const jstermCommands = {
JSTERM_COMMANDS: {
INSPECT: "inspectObject"
}
};
+// Constants used for defining the direction of JSTerm input history navigation.
+const historyCommands = {
+ HISTORY_BACK: -1,
+ HISTORY_FORWARD: 1,
+};
+
// Combine into a single constants object
module.exports = Object.assign({
FILTERS,
DEFAULT_FILTERS,
DEFAULT_FILTERS_VALUES,
},
actionTypes,
chromeRDPEnums,
jstermCommands,
prefs,
+ historyCommands,
);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/reducers/history.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_PLACEHOLDER,
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("devtools/client/webconsole/constants");
+
+/**
+ * Create default initial state for this reducer.
+ */
+function getInitialState() {
+ return {
+ // Array with history entries
+ entries: [],
+
+ // Holds the index of the history entry that the user is currently
+ // viewing. This is reset to this.history.length when APPEND_TO_HISTORY
+ // action is fired.
+ placeHolder: undefined,
+
+ // Holds the number of entries in history. This value is incremented
+ // when APPEND_TO_HISTORY action is fired and used to get previous
+ // value from the command line when the user goes backward.
+ index: 0,
+ };
+}
+
+function history(state = getInitialState(), action, prefsState) {
+ switch (action.type) {
+ case APPEND_TO_HISTORY:
+ return appendToHistory(state, prefsState, action.expression);
+ case CLEAR_HISTORY:
+ return clearHistory(state);
+ case HISTORY_LOADED:
+ return historyLoaded(state, action.entries);
+ case UPDATE_HISTORY_PLACEHOLDER:
+ return updatePlaceHolder(state, action.direction, action.expression);
+ }
+ return state;
+}
+
+function appendToHistory(state, prefsState, expression) {
+ // Clone state
+ state = {...state};
+ state.entries = [...state.entries];
+
+ // Append new expression
+ state.entries[state.index++] = expression;
+ state.placeHolder = state.entries.length;
+
+ // Remove entries if the limit is reached
+ if (state.entries.length > prefsState.historyCount) {
+ state.entries.splice(0, state.entries.length - prefsState.historyCount);
+ state.index = state.placeHolder = state.entries.length;
+ }
+
+ return state;
+}
+
+function clearHistory(state) {
+ return getInitialState();
+}
+
+/**
+ * Handling HISTORY_LOADED action that is fired when history
+ * entries created in previous Firefox session are loaded
+ * from async-storage.
+ *
+ * Loaded entries are appended before the ones that were
+ * added to the state in this session.
+ */
+function historyLoaded(state, entries) {
+ let newEntries = [...entries, ...state.entries];
+ return {
+ ...state,
+ entries: newEntries,
+ placeHolder: newEntries.length,
+ index: newEntries.length,
+ };
+}
+
+function updatePlaceHolder(state, direction, expression) {
+ // Handle UP arrow key => HISTORY_BACK
+ // Handle DOWN arrow key => HISTORY_FORWARD
+ if (direction == HISTORY_BACK) {
+ if (state.placeHolder <= 0) {
+ return state;
+ }
+
+ // Clone state
+ state = {...state};
+
+ // Save the current input value as the latest entry in history, only if
+ // the user is already at the last entry.
+ // Note: this code does not store changes to items that are already in
+ // history.
+ if (state.placeHolder == state.index) {
+ state.entries = [...state.entries];
+ state.entries[state.index] = expression || "";
+ }
+
+ state.placeHolder--;
+ } else if (direction == HISTORY_FORWARD) {
+ if (state.placeHolder >= (state.entries.length - 1)) {
+ return state;
+ }
+
+ state = {
+ ...state,
+ placeHolder: state.placeHolder + 1,
+ };
+ }
+
+ return state;
+}
+
+exports.history = history;
--- a/devtools/client/webconsole/reducers/index.js
+++ b/devtools/client/webconsole/reducers/index.js
@@ -5,16 +5,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { filters } = require("./filters");
const { messages } = require("./messages");
const { prefs } = require("./prefs");
const { ui } = require("./ui");
const { notifications } = require("./notifications");
+const { history } = require("./history");
exports.reducers = {
filters,
messages,
prefs,
ui,
notifications,
+ history,
};
--- a/devtools/client/webconsole/reducers/moz.build
+++ b/devtools/client/webconsole/reducers/moz.build
@@ -1,13 +1,14 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'filters.js',
+ 'history.js',
'index.js',
'messages.js',
'notifications.js',
'prefs.js',
'ui.js',
)
--- a/devtools/client/webconsole/reducers/prefs.js
+++ b/devtools/client/webconsole/reducers/prefs.js
@@ -4,16 +4,17 @@
* 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 PrefState = (overrides) => Object.freeze(Object.assign({
logLimit: 1000,
sidebarToggle: false,
jstermCodeMirror: false,
+ historyCount: 50,
}, overrides));
function prefs(state = PrefState(), action) {
return state;
}
exports.PrefState = PrefState;
exports.prefs = prefs;
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/selectors/history.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("devtools/client/webconsole/constants");
+
+function getHistory(state) {
+ return state.history;
+}
+
+function getHistoryEntries(state) {
+ return state.history.entries;
+}
+
+function getHistoryValue(state, direction) {
+ if (direction == HISTORY_BACK) {
+ return getPreviousHistoryValue(state);
+ }
+ if (direction == HISTORY_FORWARD) {
+ return getNextHistoryValue(state);
+ }
+ return null;
+}
+
+function getNextHistoryValue(state) {
+ if (state.history.placeHolder < (state.history.entries.length - 1)) {
+ return state.history.entries[state.history.placeHolder + 1];
+ }
+ return null;
+}
+
+function getPreviousHistoryValue(state) {
+ if (state.history.placeHolder > 0) {
+ return state.history.entries[state.history.placeHolder - 1];
+ }
+ return null;
+}
+
+module.exports = {
+ getHistory,
+ getHistoryEntries,
+ getHistoryValue,
+};
--- a/devtools/client/webconsole/selectors/moz.build
+++ b/devtools/client/webconsole/selectors/moz.build
@@ -1,12 +1,13 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'filters.js',
+ 'history.js',
'messages.js',
'notifications.js',
'prefs.js',
'ui.js',
)
--- a/devtools/client/webconsole/store.js
+++ b/devtools/client/webconsole/store.js
@@ -19,49 +19,56 @@ const {
MESSAGES_ADD,
MESSAGES_CLEAR,
PRIVATE_MESSAGES_CLEAR,
REMOVED_ACTORS_CLEAR,
NETWORK_MESSAGE_UPDATE,
PREFS,
INITIALIZE,
FILTER_TOGGLE,
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
} = require("devtools/client/webconsole/constants");
const { reducers } = require("./reducers/index");
const {
getMessage,
getAllMessagesUiById,
} = require("devtools/client/webconsole/selectors/messages");
const DataProvider = require("devtools/client/netmonitor/src/connector/firefox-data-provider");
const {
getAllNetworkMessagesUpdateById,
} = require("devtools/client/webconsole/selectors/messages");
const {getPrefsService} = require("devtools/client/webconsole/utils/prefs");
+const historyActions = require("devtools/client/webconsole/actions/history");
+
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
/**
* Create and configure store for the Console panel. This is the place
* where various enhancers and middleware can be registered.
*/
function configureStore(hud, options = {}) {
const prefsService = getPrefsService(hud);
const {
getBoolPref,
getIntPref,
} = prefsService;
const logLimit = options.logLimit
|| Math.max(getIntPref("devtools.hud.loglimit"), 1);
const sidebarToggle = getBoolPref(PREFS.FEATURES.SIDEBAR_TOGGLE);
const jstermCodeMirror = getBoolPref(PREFS.FEATURES.JSTERM_CODE_MIRROR);
+ const historyCount = getIntPref(PREFS.UI.INPUT_HISTORY_COUNT);
const initialState = {
prefs: PrefState({
logLimit,
sidebarToggle,
jstermCodeMirror,
+ historyCount,
}),
filters: FilterState({
error: getBoolPref(PREFS.FILTER.ERROR),
warn: getBoolPref(PREFS.FILTER.WARN),
info: getBoolPref(PREFS.FILTER.INFO),
debug: getBoolPref(PREFS.FILTER.DEBUG),
log: getBoolPref(PREFS.FILTER.LOG),
css: getBoolPref(PREFS.FILTER.CSS),
@@ -70,21 +77,27 @@ function configureStore(hud, options = {
}),
ui: UiState({
filterBarVisible: getBoolPref(PREFS.UI.FILTER_BAR),
networkMessageActiveTabId: "headers",
persistLogs: getBoolPref(PREFS.UI.PERSIST),
})
};
+ // Prepare middleware.
+ let middleware = applyMiddleware(
+ thunk.bind(null, {prefsService}),
+ historyPersistenceMiddleware,
+ );
+
return createStore(
createRootReducer(),
initialState,
compose(
- applyMiddleware(thunk.bind(null, {prefsService})),
+ middleware,
enableActorReleaser(hud),
enableBatching(),
enableNetProvider(hud),
enableMessagesCacheClearing(hud),
ensureCSSErrorReportingEnabled(hud),
)
);
}
@@ -94,24 +107,29 @@ function thunk(options = {}, { dispatch,
return (typeof action === "function")
? action(dispatch, getState, options)
: next(action);
};
}
function createRootReducer() {
return function rootReducer(state, action) {
- // We want to compute the new state for all properties except "messages".
+ // We want to compute the new state for all properties except
+ // "messages" and "history". These two reducers are handled
+ // separately since they are receiving additional arguments.
const newState = [...Object.entries(reducers)].reduce((res, [key, reducer]) => {
- if (key !== "messages") {
+ if (key !== "messages" && key !== "history") {
res[key] = reducer(state[key], action);
}
return res;
}, {});
+ // Pass prefs state as additional argument to the history reducer.
+ newState.history = reducers.history(state.history, action, newState.prefs);
+
return Object.assign(newState, {
// specifically pass the updated filters and prefs state as additional arguments.
messages: reducers.messages(
state.messages,
action,
newState.filters,
newState.prefs,
),
@@ -222,17 +240,17 @@ function enableNetProvider(hud) {
updateRequest: (id, data, batch) => {
proxy.dispatchRequestUpdate(id, data);
}
};
// Data provider implements async logic for fetching
// data from the backend. It's created the first
// time it's needed.
- if (!dataProvider) {
+ if (!dataProvider && proxy.webConsoleClient) {
dataProvider = new DataProvider({
actions,
webConsoleClient: proxy.webConsoleClient
});
// /!\ This is terrible, but it allows ResponsePanel to be able to call
// `dataProvider.requestData` to fetch response content lazily.
// `proxy.networkDataProvider` is put by WebConsoleOutputWrapper on
@@ -318,12 +336,47 @@ function enableMessagesCacheClearing(hud
function releaseActors(removedActors, proxy) {
if (!proxy) {
return;
}
removedActors.forEach(actor => proxy.releaseActor(actor));
}
+/**
+ * History persistence middleware is responsible for loading
+ * and maintaining history of executed expressions in JSTerm.
+ */
+function historyPersistenceMiddleware(store) {
+ let historyLoaded = false;
+ asyncStorage.getItem("webConsoleHistory").then(value => {
+ if (Array.isArray(value)) {
+ store.dispatch(historyActions.historyLoaded(value));
+ }
+ historyLoaded = true;
+ }, err => {
+ historyLoaded = true;
+ console.error(err);
+ });
+
+ return next => action => {
+ const res = next(action);
+
+ let triggerStoreActions = [
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ ];
+
+ // Save the current history entries when modified, but wait till
+ // entries from the previous session are loaded.
+ if (historyLoaded && triggerStoreActions.includes(action.type)) {
+ const state = store.getState();
+ asyncStorage.setItem("webConsoleHistory", state.history.entries);
+ }
+
+ return res;
+ };
+}
+
// Provide the store factory for test code so that each test is working with
// its own instance.
module.exports.configureStore = configureStore;
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_return_key_no_selection.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_return_key_no_selection.js
@@ -11,18 +11,26 @@ const TEST_URI = `data:text/html;charset
<head>
<script>
window.testBugA = "hello world";
window.testBugB = "hello world 2";
</script>
</head>
<body>bug 873250 - test pressing return with open popup, but no selection</body>`;
+const {
+ getHistoryEntries,
+} = require("devtools/client/webconsole/selectors/history");
+
add_task(async function() {
- let { jsterm } = await openNewTabAndConsole(TEST_URI);
+ const {
+ jsterm,
+ ui,
+ } = await openNewTabAndConsole(TEST_URI);
+
const {
autocompletePopup: popup,
completeNode,
} = jsterm;
const onPopUpOpen = popup.once("popup-opened");
info("wait for popup to show");
@@ -38,11 +46,14 @@ add_task(async function() {
info("press Return and wait for popup to hide");
const onPopUpClose = popup.once("popup-closed");
executeSoon(() => EventUtils.synthesizeKey("KEY_Enter"));
await onPopUpClose;
ok(!popup.isOpen, "popup is not open after KEY_Enter");
is(jsterm.getInputValue(), "", "inputNode is empty after KEY_Enter");
is(completeNode.value, "", "completeNode is empty");
- is(jsterm.history[jsterm.history.length - 1], "window.testBug",
+
+ const state = ui.consoleOutput.getStore().getState();
+ const entries = getHistoryEntries(state);
+ is(entries[entries.length - 1], "window.testBug",
"jsterm history is correct");
});
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_history.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_history.js
@@ -3,20 +3,23 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests the console history feature accessed via the up and down arrow keys.
"use strict";
const TEST_URI = "data:text/html;charset=UTF-8,test";
-const HISTORY_BACK = -1;
-const HISTORY_FORWARD = 1;
const COMMANDS = ["document", "window", "window.location"];
+const {
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("devtools/client/webconsole/constants");
+
add_task(async function() {
const { jsterm } = await openNewTabAndConsole(TEST_URI);
const { inputNode } = jsterm;
jsterm.clearOutput();
for (let command of COMMANDS) {
info(`Executing command ${command}`);
jsterm.setInputValue(command);
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_history_persist.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_history_persist.js
@@ -9,76 +9,99 @@
"use strict";
requestLongerTimeout(2);
const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
"persisting history - bug 943306";
const INPUT_HISTORY_COUNT = 10;
+const {
+ getHistoryEntries,
+} = require("devtools/client/webconsole/selectors/history");
+
add_task(async function() {
info("Setting custom input history pref to " + INPUT_HISTORY_COUNT);
Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount", INPUT_HISTORY_COUNT);
// First tab: run a bunch of commands and then make sure that you can
// navigate through their history.
let hud1 = await openNewTabAndConsole(TEST_URI);
+ let state1 = hud1.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state1)),
+ "[]",
+ "No history on first tab initially");
+ await populateInputHistory(hud1);
- is(JSON.stringify(hud1.jsterm.history), "[]", "No history on first tab initially");
- await populateInputHistory(hud1);
- is(JSON.stringify(hud1.jsterm.history),
+ state1 = hud1.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state1)),
'["0","1","2","3","4","5","6","7","8","9"]',
"First tab has populated history");
// Second tab: Just make sure that you can navigate through the history
// generated by the first tab.
let hud2 = await openNewTabAndConsole(TEST_URI, false);
- is(JSON.stringify(hud2.jsterm.history),
+ let state2 = hud2.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state2)),
'["0","1","2","3","4","5","6","7","8","9"]',
"Second tab has populated history");
await testNavigatingHistoryInUI(hud2);
- is(JSON.stringify(hud2.jsterm.history),
+
+ state2 = hud2.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state2)),
'["0","1","2","3","4","5","6","7","8","9",""]',
"An empty entry has been added in the second tab due to history perusal");
// Third tab: Should have the same history as first tab, but if we run a
// command, then the history of the first and second shouldn't be affected
let hud3 = await openNewTabAndConsole(TEST_URI, false);
- is(JSON.stringify(hud3.jsterm.history),
+ let state3 = hud3.ui.consoleOutput.getStore().getState();
+
+ is(JSON.stringify(getHistoryEntries(state3)),
'["0","1","2","3","4","5","6","7","8","9"]',
"Third tab has populated history");
// Set input value separately from execute so UP arrow accurately navigates
// history.
hud3.jsterm.setInputValue('"hello from third tab"');
await hud3.jsterm.execute();
- is(JSON.stringify(hud1.jsterm.history),
+ state1 = hud1.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state1)),
'["0","1","2","3","4","5","6","7","8","9"]',
"First tab history hasn't changed due to command in third tab");
- is(JSON.stringify(hud2.jsterm.history),
+
+ state2 = hud2.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state2)),
'["0","1","2","3","4","5","6","7","8","9",""]',
"Second tab history hasn't changed due to command in third tab");
- is(JSON.stringify(hud3.jsterm.history),
+
+ state3 = hud3.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state3)),
'["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
"Third tab has updated history (and purged the first result) after " +
"running a command");
// Fourth tab: Should have the latest command from the third tab, followed
// by the rest of the history from the first tab.
let hud4 = await openNewTabAndConsole(TEST_URI, false);
- is(JSON.stringify(hud4.jsterm.history),
+ let state4 = hud4.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state4)),
'["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
"Fourth tab has most recent history");
- await hud4.jsterm.clearHistory();
- is(JSON.stringify(hud4.jsterm.history), "[]", "Clearing history for a tab works");
+ await hud4.jsterm.props.clearHistory();
+ state4 = hud4.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state4)),
+ "[]",
+ "Clearing history for a tab works");
let hud5 = await openNewTabAndConsole(TEST_URI, false);
- is(JSON.stringify(hud5.jsterm.history), "[]",
+ let state5 = hud5.ui.consoleOutput.getStore().getState();
+ is(JSON.stringify(getHistoryEntries(state5)), "[]",
"Clearing history carries over to a new tab");
info("Clearing custom input history pref");
Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount");
});
/**
* Populate the history by running the following commands:
--- a/devtools/client/webconsole/test/mochitest/head.js
+++ b/devtools/client/webconsole/test/mochitest/head.js
@@ -65,17 +65,17 @@ registerCleanupFunction(async function()
*/
async function openNewTabAndConsole(url, clearJstermHistory = true) {
let toolbox = await openNewTabAndToolbox(url, "webconsole");
let hud = toolbox.getCurrentPanel().hud;
hud.jsterm._lazyVariablesView = false;
if (clearJstermHistory) {
// Clearing history that might have been set in previous tests.
- await hud.jsterm.clearHistory();
+ await hud.ui.consoleOutput.dispatchClearHistory();
}
return hud;
}
/**
* Subscribe to the store and log out stringinfied versions of messages.
* This is a helper function for debugging, to make is easier to see what
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -362,16 +362,20 @@ WebConsoleOutputWrapper.prototype = {
this.setTimeoutIfNeeded();
},
batchedMessagesAdd: function(message) {
this.queuedMessageAdds.push(message);
this.setTimeoutIfNeeded();
},
+ dispatchClearHistory: function() {
+ store.dispatch(actions.clearHistory());
+ },
+
/**
* Returns a Promise that resolves once any async dispatch is finally dispatched.
*/
waitAsyncDispatches: function() {
if (!this.throttledDispatchPromise) {
return Promise.resolve();
}
return this.throttledDispatchPromise;