autocomplete.js (5154B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { 7 AUTOCOMPLETE_CLEAR, 8 AUTOCOMPLETE_DATA_RECEIVE, 9 AUTOCOMPLETE_PENDING_REQUEST, 10 AUTOCOMPLETE_RETRIEVE_FROM_CACHE, 11 EVALUATE_EXPRESSION, 12 UPDATE_HISTORY_POSITION, 13 REVERSE_SEARCH_INPUT_CHANGE, 14 REVERSE_SEARCH_BACK, 15 REVERSE_SEARCH_NEXT, 16 WILL_NAVIGATE, 17 } = require("resource://devtools/client/webconsole/constants.js"); 18 19 function getDefaultState(overrides = {}) { 20 return Object.freeze({ 21 cache: null, 22 matches: [], 23 matchProp: null, 24 isElementAccess: false, 25 pendingRequestId: null, 26 isUnsafeGetter: false, 27 getterPath: null, 28 authorizedEvaluations: [], 29 ...overrides, 30 }); 31 } 32 33 function autocomplete(state = getDefaultState(), action) { 34 switch (action.type) { 35 case WILL_NAVIGATE: 36 return getDefaultState(); 37 case AUTOCOMPLETE_RETRIEVE_FROM_CACHE: 38 return autoCompleteRetrieveFromCache(state, action); 39 case AUTOCOMPLETE_PENDING_REQUEST: 40 return { 41 ...state, 42 cache: null, 43 pendingRequestId: action.id, 44 }; 45 case AUTOCOMPLETE_DATA_RECEIVE: 46 if (action.id !== state.pendingRequestId) { 47 return state; 48 } 49 50 if (action.data.matches === null) { 51 return getDefaultState(); 52 } 53 54 if (action.data.isUnsafeGetter) { 55 // We only want to display the getter confirm popup if the last char is a dot or 56 // an opening bracket, or if the user forced the autocompletion with Ctrl+Space. 57 if ( 58 action.input.endsWith(".") || 59 action.input.endsWith("[") || 60 action.force 61 ) { 62 return { 63 ...getDefaultState(), 64 isUnsafeGetter: true, 65 getterPath: action.data.getterPath, 66 authorizedEvaluations: action.authorizedEvaluations, 67 }; 68 } 69 70 return { 71 ...state, 72 pendingRequestId: null, 73 }; 74 } 75 76 return { 77 ...state, 78 authorizedEvaluations: action.authorizedEvaluations, 79 getterPath: null, 80 isUnsafeGetter: false, 81 pendingRequestId: null, 82 cache: { 83 input: action.input, 84 frameActorId: action.frameActorId, 85 ...action.data, 86 }, 87 ...action.data, 88 }; 89 // Reset the autocomplete data when: 90 // - clear is explicitely called 91 // - the user navigates the history 92 // - or an expression was evaluated. 93 case AUTOCOMPLETE_CLEAR: 94 return getDefaultState({ 95 authorizedEvaluations: state.authorizedEvaluations, 96 }); 97 case EVALUATE_EXPRESSION: 98 case UPDATE_HISTORY_POSITION: 99 case REVERSE_SEARCH_INPUT_CHANGE: 100 case REVERSE_SEARCH_BACK: 101 case REVERSE_SEARCH_NEXT: 102 return getDefaultState(); 103 } 104 105 return state; 106 } 107 108 /** 109 * Retrieve from cache action reducer. 110 * 111 * @param {object} state 112 * @param {object} action 113 * @returns {object} new state. 114 */ 115 function autoCompleteRetrieveFromCache(state, action) { 116 const { input } = action; 117 const { cache } = state; 118 119 let filterBy = input; 120 if (cache.isElementAccess) { 121 // if we're performing an element access, we can simply retrieve whatever comes 122 // after the last opening bracket. 123 filterBy = input.substring(input.lastIndexOf("[") + 1); 124 } else { 125 // Find the last non-alphanumeric other than "_", ":", or "$" if it exists. 126 const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/); 127 // If input contains non-alphanumerics, use the part after the last one 128 // to filter the cache. 129 if (lastNonAlpha) { 130 filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); 131 } 132 } 133 const stripWrappingQuotes = s => 134 s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1"); 135 const filterByLc = filterBy.toLocaleLowerCase(); 136 const looseMatching = 137 !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0]; 138 const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy); 139 const newList = cache.matches.filter(l => { 140 if (needStripQuote) { 141 l = stripWrappingQuotes(l); 142 } 143 144 if (looseMatching) { 145 return l.toLocaleLowerCase().startsWith(filterByLc); 146 } 147 148 return l.startsWith(filterBy); 149 }); 150 151 newList.sort((a, b) => { 152 const startingQuoteRegex = /^('|"|`)/; 153 const aFirstMeaningfulChar = startingQuoteRegex.test(a) ? a[1] : a[0]; 154 const bFirstMeaningfulChar = startingQuoteRegex.test(b) ? b[1] : b[0]; 155 const lA = 156 aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar; 157 const lB = 158 bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar; 159 if (lA === lB) { 160 if (a === filterBy) { 161 return -1; 162 } 163 if (b === filterBy) { 164 return 1; 165 } 166 return a.localeCompare(b); 167 } 168 return lA ? -1 : 1; 169 }); 170 171 return { 172 ...state, 173 isUnsafeGetter: false, 174 getterPath: null, 175 matches: newList, 176 matchProp: filterBy, 177 isElementAccess: cache.isElementAccess, 178 }; 179 } 180 181 exports.autocomplete = autocomplete;