UrlbarProviderRestrictKeywordsAutofill.sys.mjs (6200B)
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 * This module exports a provider that offers restrict keywords autofill for 7 * search mode. 8 */ 9 10 import { 11 UrlbarProvider, 12 UrlbarUtils, 13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 14 15 const lazy = {}; 16 17 ChromeUtils.defineESModuleGetters(lazy, { 18 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 19 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 20 UrlbarTokenizer: 21 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 22 }); 23 24 const RESTRICT_KEYWORDS_FEATURE_GATE = "searchRestrictKeywords.featureGate"; 25 26 /** 27 * Class used to create the provider. 28 */ 29 export class UrlbarProviderRestrictKeywordsAutofill extends UrlbarProvider { 30 #autofillData; 31 #lowerCaseTokenToKeywords; 32 33 constructor() { 34 super(); 35 } 36 37 /** 38 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 39 */ 40 get type() { 41 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; 42 } 43 44 getPriority() { 45 return 1; 46 } 47 48 async #getLowerCaseTokenToKeywords() { 49 let tokenToKeywords = await lazy.UrlbarTokenizer.getL10nRestrictKeywords(); 50 51 this.#lowerCaseTokenToKeywords = new Map( 52 [...tokenToKeywords].map(([token, keywords]) => [ 53 token, 54 keywords.map(keyword => keyword.toLowerCase()), 55 ]) 56 ); 57 58 return this.#lowerCaseTokenToKeywords; 59 } 60 61 async #getKeywordAliases() { 62 return Array.from(await this.#lowerCaseTokenToKeywords.values()) 63 .flat() 64 .map(keyword => "@" + keyword); 65 } 66 67 async isActive(queryContext) { 68 if (!lazy.UrlbarPrefs.getScotchBonnetPref(RESTRICT_KEYWORDS_FEATURE_GATE)) { 69 return false; 70 } 71 72 this.#autofillData = null; 73 74 if ( 75 queryContext.searchMode || 76 queryContext.tokens.length != 1 || 77 queryContext.searchString.length == 1 || 78 queryContext.restrictSource || 79 !queryContext.searchString.startsWith("@") 80 ) { 81 return false; 82 } 83 84 // Returns partial autofill result when the user types 85 // @h, @hi, @hist, etc. 86 if (lazy.UrlbarPrefs.get("autoFill") && queryContext.allowAutofill) { 87 let instance = this.queryInstance; 88 let result = await this.#getAutofillResult(queryContext); 89 if (result && instance == this.queryInstance) { 90 this.#autofillData = { result, instance }; 91 return true; 92 } 93 } 94 95 // Returns full autofill result when user types keyword with space to 96 // enter seach mode. Example, "@history ". 97 let keywordAliases = await this.#getKeywordAliases(); 98 if ( 99 keywordAliases.some(keyword => 100 keyword.startsWith(queryContext.trimmedLowerCaseSearchString) 101 ) 102 ) { 103 return true; 104 } 105 106 return false; 107 } 108 109 /** 110 * Starts querying. 111 * 112 * @param {UrlbarQueryContext} queryContext 113 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 114 * Callback invoked by the provider to add a new result. 115 */ 116 async startQuery(queryContext, addCallback) { 117 if ( 118 this.#autofillData && 119 this.#autofillData.instance == this.queryInstance 120 ) { 121 addCallback(this, this.#autofillData.result); 122 return; 123 } 124 125 let instance = this.queryInstance; 126 let typedKeyword = queryContext.lowerCaseSearchString; 127 let typedKeywordTrimmed = 128 queryContext.trimmedLowerCaseSearchString.substring(1); 129 let tokenToKeywords = await this.#getLowerCaseTokenToKeywords(); 130 131 if (instance != this.queryInstance) { 132 return; 133 } 134 135 let restrictSymbol; 136 let aliasKeyword; 137 138 for (let [token, keywords] of tokenToKeywords) { 139 if (keywords.includes(typedKeywordTrimmed)) { 140 restrictSymbol = token; 141 aliasKeyword = "@" + typedKeywordTrimmed + " "; 142 break; 143 } 144 } 145 146 if (restrictSymbol && typedKeyword == aliasKeyword) { 147 let result = new lazy.UrlbarResult({ 148 type: UrlbarUtils.RESULT_TYPE.RESTRICT, 149 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 150 heuristic: true, 151 hideRowLabel: true, 152 payload: { 153 keyword: restrictSymbol, 154 providesSearchMode: false, 155 }, 156 }); 157 addCallback(this, result); 158 } 159 160 this.#autofillData = null; 161 } 162 163 cancelQuery() { 164 if (this.#autofillData?.instance == this.queryInstance) { 165 this.#autofillData = null; 166 } 167 } 168 169 async #getAutofillResult(queryContext) { 170 let tokenToKeywords = await this.#getLowerCaseTokenToKeywords(); 171 let { lowerCaseSearchString } = queryContext; 172 173 for (let [token, l10nRestrictKeywords] of tokenToKeywords.entries()) { 174 let keywords = [...l10nRestrictKeywords].map(keyword => `@${keyword}`); 175 let autofillKeyword = keywords.find(keyword => 176 keyword.startsWith(lowerCaseSearchString) 177 ); 178 179 // found the keyword 180 if (autofillKeyword) { 181 // Add an autofill result. Append a space so the user can hit enter 182 // or the right arrow key and immediately start typing their query. 183 let keywordPreservingUserCase = 184 queryContext.searchString + 185 autofillKeyword.substr(queryContext.searchString.length); 186 let value = keywordPreservingUserCase + " "; 187 let icon = UrlbarUtils.LOCAL_SEARCH_MODES.find( 188 mode => mode.restrict == token 189 )?.icon; 190 191 return new lazy.UrlbarResult({ 192 type: UrlbarUtils.RESULT_TYPE.RESTRICT, 193 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 194 hideRowLabel: true, 195 autofill: { 196 value, 197 selectionStart: queryContext.searchString.length, 198 selectionEnd: value.length, 199 }, 200 payload: { 201 icon, 202 keyword: token, 203 l10nRestrictKeywords, 204 autofillKeyword: keywordPreservingUserCase, 205 providesSearchMode: true, 206 }, 207 highlights: { 208 l10nRestrictKeywords: UrlbarUtils.HIGHLIGHT.TYPED, 209 autofillKeyword: UrlbarUtils.HIGHLIGHT.TYPED, 210 }, 211 }); 212 } 213 } 214 215 return null; 216 } 217 }