/* 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;