summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/reducers/autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/reducers/autocomplete.js')
-rw-r--r--devtools/client/webconsole/reducers/autocomplete.js181
1 files changed, 181 insertions, 0 deletions
diff --git a/devtools/client/webconsole/reducers/autocomplete.js b/devtools/client/webconsole/reducers/autocomplete.js
new file mode 100644
index 0000000000..348ff9f7f9
--- /dev/null
+++ b/devtools/client/webconsole/reducers/autocomplete.js
@@ -0,0 +1,181 @@
+/* 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 {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ EVALUATE_EXPRESSION,
+ UPDATE_HISTORY_POSITION,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+ WILL_NAVIGATE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function getDefaultState(overrides = {}) {
+ return Object.freeze({
+ cache: null,
+ matches: [],
+ matchProp: null,
+ isElementAccess: false,
+ pendingRequestId: null,
+ isUnsafeGetter: false,
+ getterPath: null,
+ authorizedEvaluations: [],
+ ...overrides,
+ });
+}
+
+function autocomplete(state = getDefaultState(), action) {
+ switch (action.type) {
+ case WILL_NAVIGATE:
+ return getDefaultState();
+ case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
+ return autoCompleteRetrieveFromCache(state, action);
+ case AUTOCOMPLETE_PENDING_REQUEST:
+ return {
+ ...state,
+ cache: null,
+ pendingRequestId: action.id,
+ };
+ case AUTOCOMPLETE_DATA_RECEIVE:
+ if (action.id !== state.pendingRequestId) {
+ return state;
+ }
+
+ if (action.data.matches === null) {
+ return getDefaultState();
+ }
+
+ if (action.data.isUnsafeGetter) {
+ // We only want to display the getter confirm popup if the last char is a dot or
+ // an opening bracket, or if the user forced the autocompletion with Ctrl+Space.
+ if (
+ action.input.endsWith(".") ||
+ action.input.endsWith("[") ||
+ action.force
+ ) {
+ return {
+ ...getDefaultState(),
+ isUnsafeGetter: true,
+ getterPath: action.data.getterPath,
+ authorizedEvaluations: action.authorizedEvaluations,
+ };
+ }
+
+ return {
+ ...state,
+ pendingRequestId: null,
+ };
+ }
+
+ return {
+ ...state,
+ authorizedEvaluations: action.authorizedEvaluations,
+ getterPath: null,
+ isUnsafeGetter: false,
+ pendingRequestId: null,
+ cache: {
+ input: action.input,
+ frameActorId: action.frameActorId,
+ ...action.data,
+ },
+ ...action.data,
+ };
+ // Reset the autocomplete data when:
+ // - clear is explicitely called
+ // - the user navigates the history
+ // - or an expression was evaluated.
+ case AUTOCOMPLETE_CLEAR:
+ return getDefaultState({
+ authorizedEvaluations: state.authorizedEvaluations,
+ });
+ case EVALUATE_EXPRESSION:
+ case UPDATE_HISTORY_POSITION:
+ case REVERSE_SEARCH_INPUT_CHANGE:
+ case REVERSE_SEARCH_BACK:
+ case REVERSE_SEARCH_NEXT:
+ return getDefaultState();
+ }
+
+ return state;
+}
+
+/**
+ * Retrieve from cache action reducer.
+ *
+ * @param {Object} state
+ * @param {Object} action
+ * @returns {Object} new state.
+ */
+function autoCompleteRetrieveFromCache(state, action) {
+ const { input } = action;
+ const { cache } = state;
+
+ let filterBy = input;
+ if (cache.isElementAccess) {
+ // if we're performing an element access, we can simply retrieve whatever comes
+ // after the last opening bracket.
+ filterBy = input.substring(input.lastIndexOf("[") + 1);
+ } else {
+ // Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
+ const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
+ // If input contains non-alphanumerics, use the part after the last one
+ // to filter the cache.
+ if (lastNonAlpha) {
+ filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
+ }
+ }
+ const stripWrappingQuotes = s =>
+ s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
+ const filterByLc = filterBy.toLocaleLowerCase();
+ const looseMatching =
+ !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
+ const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy);
+ const newList = cache.matches.filter(l => {
+ if (needStripQuote) {
+ l = stripWrappingQuotes(l);
+ }
+
+ if (looseMatching) {
+ return l.toLocaleLowerCase().startsWith(filterByLc);
+ }
+
+ return l.startsWith(filterBy);
+ });
+
+ newList.sort((a, b) => {
+ const startingQuoteRegex = /^('|"|`)/;
+ const aFirstMeaningfulChar = startingQuoteRegex.test(a) ? a[1] : a[0];
+ const bFirstMeaningfulChar = startingQuoteRegex.test(b) ? b[1] : b[0];
+ const lA =
+ aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
+ const lB =
+ bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
+ if (lA === lB) {
+ if (a === filterBy) {
+ return -1;
+ }
+ if (b === filterBy) {
+ return 1;
+ }
+ return a.localeCompare(b);
+ }
+ return lA ? -1 : 1;
+ });
+
+ return {
+ ...state,
+ isUnsafeGetter: false,
+ getterPath: null,
+ matches: newList,
+ matchProp: filterBy,
+ isElementAccess: cache.isElementAccess,
+ };
+}
+
+exports.autocomplete = autocomplete;