project-text-search.js (3913B)
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 5 /** 6 * Redux actions for the search state 7 * 8 * @module actions/search 9 */ 10 11 import { isFulfilled } from "../utils/async-value"; 12 import { 13 getFirstSourceActorForGeneratedSource, 14 getSourceList, 15 getSettledSourceTextContent, 16 isSourceBlackBoxed, 17 getSearchOptions, 18 } from "../selectors/index"; 19 import { createLocation } from "../utils/location"; 20 import { matchesGlobPatterns } from "../utils/source"; 21 import { loadSourceText } from "./sources/loadSourceText"; 22 import { searchKeys } from "../constants"; 23 24 export function searchSources(query, onUpdatedResults, signal) { 25 return async ({ dispatch, getState, searchWorker }) => { 26 dispatch({ 27 type: "SET_PROJECT_SEARCH_QUERY", 28 query, 29 }); 30 31 const searchOptions = getSearchOptions( 32 getState(), 33 searchKeys.PROJECT_SEARCH 34 ); 35 const validSources = getSourceList(getState()).filter( 36 source => 37 !isSourceBlackBoxed(getState(), source) && 38 !matchesGlobPatterns(source, searchOptions.excludePatterns) 39 ); 40 // Sort original entries first so that search results are more useful. 41 // Deprioritize third-party scripts, so their results show last. 42 validSources.sort((a, b) => { 43 function isThirdParty(source) { 44 return ( 45 source?.url && 46 (source.url.includes("node_modules") || 47 source.url.includes("bower_components")) 48 ); 49 } 50 51 if (a.isOriginal && !isThirdParty(a)) { 52 return -1; 53 } 54 55 if (b.isOriginal && !isThirdParty(b)) { 56 return 1; 57 } 58 59 if (!isThirdParty(a) && isThirdParty(b)) { 60 return -1; 61 } 62 if (isThirdParty(a) && !isThirdParty(b)) { 63 return 1; 64 } 65 return 0; 66 }); 67 const results = []; 68 for (const source of validSources) { 69 const sourceActor = getFirstSourceActorForGeneratedSource( 70 getState(), 71 source.id 72 ); 73 await dispatch(loadSourceText(source, sourceActor)); 74 75 // This is the only asynchronous call in this method. 76 // We may have stopped the search by closing the search panel or changing the query. 77 // Avoid any further unecessary computation when the React Component tells us the query was cancelled. 78 if (signal.aborted) { 79 return; 80 } 81 82 const result = await searchSource(source, sourceActor, query, { 83 getState, 84 searchWorker, 85 }); 86 if (signal.aborted) { 87 return; 88 } 89 90 if (result) { 91 results.push(result); 92 onUpdatedResults(results, false, signal); 93 } 94 } 95 onUpdatedResults(results, true, signal); 96 }; 97 } 98 99 export async function searchSource( 100 source, 101 sourceActor, 102 query, 103 { getState, searchWorker } 104 ) { 105 const state = getState(); 106 const location = createLocation({ 107 source, 108 sourceActor, 109 }); 110 111 const content = getSettledSourceTextContent(state, location); 112 let matches = []; 113 114 if (content && isFulfilled(content) && content.value.type === "text") { 115 const options = getSearchOptions(state, searchKeys.PROJECT_SEARCH); 116 matches = await searchWorker.findSourceMatches( 117 content.value, 118 query, 119 options 120 ); 121 } 122 if (!matches.length) { 123 return null; 124 } 125 return { 126 type: "RESULT", 127 location, 128 // `matches` are generated by project-search worker's `findSourceMatches` method 129 matches: matches.map(m => ({ 130 type: "MATCH", 131 location: createLocation({ 132 ...location, 133 // `matches` only contain line and column 134 // `location` will already refer to the right source/sourceActor 135 line: m.line, 136 column: m.column, 137 }), 138 matchIndex: m.matchIndex, 139 match: m.match, 140 value: m.value, 141 })), 142 }; 143 }