1import { OptionsPaneItemDescriptor } from '../OptionsPaneItemDescriptor'; 2import { OptionsPaneCategoryDescriptor } from '../OptionsPaneCategoryDescriptor'; 3 4export interface OptionSearchResults { 5 optionHits: OptionsPaneItemDescriptor[]; 6 overrideHits: OptionsPaneCategoryDescriptor[]; 7 totalCount: number; 8} 9 10export class OptionSearchEngine { 11 constructor( 12 private categories: OptionsPaneCategoryDescriptor[], 13 private overrides: OptionsPaneCategoryDescriptor[] 14 ) {} 15 16 search(query: string): OptionSearchResults { 17 const searchRegex = new RegExp(query, 'i'); 18 19 const optionHits = this.collectHits(this.categories, searchRegex, []); 20 const sortedHits = optionHits.sort(compareHit).map((x) => x.item); 21 22 const overrideHits = this.collectHits(this.overrides, searchRegex, []); 23 const sortedOverridesHits = overrideHits.sort(compareHit).map((x) => x.item); 24 25 return { 26 optionHits: sortedHits, 27 overrideHits: this.buildOverrideHitCategories(sortedOverridesHits), 28 totalCount: this.getAllOptionsCount(this.categories), 29 }; 30 } 31 32 private collectHits(categories: OptionsPaneCategoryDescriptor[], searchRegex: RegExp, hits: SearchHit[]) { 33 for (const category of categories) { 34 const categoryNameMatch = searchRegex.test(category.props.title); 35 36 for (const item of category.items) { 37 if (searchRegex.test(item.props.title)) { 38 hits.push({ item: item, rank: 1 }); 39 continue; 40 } 41 if (item.props.description && searchRegex.test(item.props.description)) { 42 hits.push({ item: item, rank: 2 }); 43 continue; 44 } 45 if (categoryNameMatch) { 46 hits.push({ item: item, rank: 3 }); 47 } 48 } 49 50 if (category.categories.length > 0) { 51 this.collectHits(category.categories, searchRegex, hits); 52 } 53 } 54 55 return hits; 56 } 57 58 getAllOptionsCount(categories: OptionsPaneCategoryDescriptor[]) { 59 var total = 0; 60 61 for (const category of categories) { 62 total += category.items.length; 63 64 if (category.categories.length > 0) { 65 total += this.getAllOptionsCount(category.categories); 66 } 67 } 68 69 return total; 70 } 71 72 buildOverrideHitCategories(hits: OptionsPaneItemDescriptor[]): OptionsPaneCategoryDescriptor[] { 73 const categories: Record<string, OptionsPaneCategoryDescriptor> = {}; 74 75 for (const hit of hits) { 76 let category = categories[hit.parent.props.title]; 77 78 if (!category) { 79 category = categories[hit.parent.props.title] = new OptionsPaneCategoryDescriptor(hit.parent.props); 80 // Add matcher item as that should always be shown 81 category.addItem(hit.parent.items[0]); 82 } 83 84 // Prevent adding matcher twice since it's automatically added for every override 85 if (category.items[0].props.title !== hit.props.title) { 86 category.addItem(hit); 87 } 88 } 89 90 return Object.values(categories); 91 } 92} 93 94interface SearchHit { 95 item: OptionsPaneItemDescriptor; 96 rank: number; 97} 98 99function compareHit(left: SearchHit, right: SearchHit) { 100 return left.rank - right.rank; 101} 102