зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1267810 - Add a module for registering keywords and handling keyword input sessions. r=adw
MozReview-Commit-ID: Ghqe5xLw67Y --HG-- extra : rebase_source : ef23c5eb90a7e882c09736724ed5b63773be061c
This commit is contained in:
Родитель
db0967a435
Коммит
1dc56d3c39
|
@ -472,7 +472,15 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
|
|||
list-style-image: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
|
||||
#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -760,7 +760,8 @@
|
|||
</hbox>
|
||||
</box>
|
||||
<box id="urlbar-display-box" align="center">
|
||||
<label class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
|
||||
<label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
|
||||
<label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
|
||||
</box>
|
||||
<hbox id="urlbar-icons">
|
||||
<image id="page-report-button"
|
||||
|
|
|
@ -56,6 +56,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
<field name="AppConstants" readonly="true">
|
||||
(Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
|
||||
</field>
|
||||
|
||||
<field name="ExtensionSearchHandler" readonly="true">
|
||||
(Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
|
||||
</field>
|
||||
|
||||
<constructor><![CDATA[
|
||||
this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
|
||||
.getService(Components.interfaces.nsIPrefService)
|
||||
|
@ -174,6 +179,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
returnValue = action.params.input;
|
||||
break;
|
||||
}
|
||||
case "extension": {
|
||||
returnValue = action.params.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let originalUrl = ReaderMode.getOriginalUrl(aValue);
|
||||
|
@ -478,6 +487,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
actionDetails
|
||||
);
|
||||
break;
|
||||
case "extension":
|
||||
this.handleRevert();
|
||||
// Give the extension control of handling the command.
|
||||
let searchString = action.params.content;
|
||||
let keyword = action.params.keyword;
|
||||
this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// This is a fallback for add-ons and old testing code that directly
|
||||
|
@ -595,7 +611,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
engineOrEngineName;
|
||||
let isOneOff = this.popup.oneOffSearchButtons
|
||||
.maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
|
||||
// Infer the type of the even which triggered the search.
|
||||
// Infer the type of the event which triggered the search.
|
||||
let eventType = "unknown";
|
||||
if (event instanceof KeyboardEvent) {
|
||||
eventType = "key";
|
||||
|
@ -1173,6 +1189,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
this._clearNoActions();
|
||||
this.formatValue();
|
||||
}
|
||||
if (ExtensionSearchHandler.hasActiveInputSession()) {
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
}
|
||||
]]></handler>
|
||||
|
||||
<handler event="dragstart" phase="capturing"><![CDATA[
|
||||
|
|
|
@ -421,6 +421,7 @@ BrowserGlue.prototype = {
|
|||
tag: 7,
|
||||
visiturl: 8,
|
||||
remotetab: 9,
|
||||
extension: 10,
|
||||
};
|
||||
if (actionType in buckets) {
|
||||
Services.telemetry
|
||||
|
|
|
@ -392,6 +392,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
|||
<!ENTITY openCmd.commandkey "l">
|
||||
<!ENTITY urlbar.placeholder2 "Search or enter address">
|
||||
<!ENTITY urlbar.accesskey "d">
|
||||
<!ENTITY urlbar.extension.label "Extension:">
|
||||
<!ENTITY urlbar.switchToTab.label "Switch to tab:">
|
||||
|
||||
<!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
|
||||
|
|
|
@ -79,6 +79,13 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar[actiontype="extension"] > #identity-box > #identity-icon {
|
||||
-moz-image-region: inherit;
|
||||
list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* SHARING ICON */
|
||||
|
||||
#sharing-icon {
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "ExtensionSearchHandler" ];
|
||||
|
||||
// Used to keep track of all of the registered keywords, where each keyword is
|
||||
// mapped to a KeywordInfo instance.
|
||||
let gKeywordMap = new Map();
|
||||
|
||||
// Used to keep track of the active input session.
|
||||
let gActiveInputSession = null;
|
||||
|
||||
// Used to keep track of who has control over the active suggestion callback
|
||||
// so older callbacks can be ignored. The callback ID should increment whenever
|
||||
// the input changes or the input session ends.
|
||||
let gCurrentCallbackID = 0;
|
||||
|
||||
// Handles keeping track of information associated to the registered keyword.
|
||||
class KeywordInfo {
|
||||
constructor(extension, description) {
|
||||
this._extension = extension;
|
||||
this._description = description;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
set description(desc) {
|
||||
this._description = desc;
|
||||
}
|
||||
|
||||
get extension() {
|
||||
return this._extension;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsible for handling communication between the extension and the urlbar.
|
||||
class InputSession {
|
||||
constructor(keyword, extension) {
|
||||
this._keyword = keyword;
|
||||
this._extension = extension;
|
||||
this._suggestionsCallback = null;
|
||||
this._searchCompleteCallback = null;
|
||||
}
|
||||
|
||||
get keyword() {
|
||||
return this._keyword;
|
||||
}
|
||||
|
||||
registerSuggestionsCallback(callback) {
|
||||
this._suggestionsCallback = callback;
|
||||
}
|
||||
|
||||
registerSearchCompleteCallback(callback) {
|
||||
this._onSearchComplete = callback;
|
||||
}
|
||||
|
||||
addSuggestions(suggestions) {
|
||||
if (this._suggestionsCallback) {
|
||||
this._suggestionsCallback(suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
start(eventName) {
|
||||
this._extension.emit(eventName);
|
||||
}
|
||||
|
||||
update(eventName, text) {
|
||||
this._extension.emit(eventName, text, ++gCurrentCallbackID);
|
||||
if (this._searchCompleteCallback) {
|
||||
this._searchCompleteCallback();
|
||||
}
|
||||
}
|
||||
|
||||
cancel(eventName) {
|
||||
this._extension.emit(eventName);
|
||||
if (this._searchCompleteCallback) {
|
||||
this._searchCompleteCallback();
|
||||
}
|
||||
}
|
||||
|
||||
end(eventName, text, disposition) {
|
||||
this._extension.emit(eventName, text, disposition);
|
||||
if (this._searchCompleteCallback) {
|
||||
this._searchCompleteCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ExtensionSearchHandler = Object.freeze({
|
||||
MSG_INPUT_STARTED: "webext-omnibox-input-started",
|
||||
MSG_INPUT_CHANGED: "webext-omnibox-input-changed",
|
||||
MSG_INPUT_ENTERED: "webext-omnibox-input-entered",
|
||||
MSG_INPUT_CANCELLED: "webext-omnibox-input-cancelled",
|
||||
|
||||
/**
|
||||
* Registers a keyword.
|
||||
*
|
||||
* @param {string} keyword The keyword to register.
|
||||
* @param {Extension} extension The extension registering the keyword.
|
||||
*/
|
||||
registerKeyword(keyword, extension) {
|
||||
if (gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is already registered: "${keyword}"`);
|
||||
}
|
||||
gKeywordMap.set(keyword, new KeywordInfo(extension, extension.name));
|
||||
},
|
||||
|
||||
/**
|
||||
* Unregisters a keyword.
|
||||
*
|
||||
* @param {string} keyword The keyword to unregister.
|
||||
*/
|
||||
unregisterKeyword(keyword) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
gActiveInputSession = null;
|
||||
gKeywordMap.delete(keyword);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a keyword is registered.
|
||||
*
|
||||
* @param {string} keyword The word to check.
|
||||
* @return {boolean} true if the word is a registered keyword.
|
||||
*/
|
||||
isKeywordRegistered(keyword) {
|
||||
return gKeywordMap.has(keyword);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {boolean} true if there is an active input session.
|
||||
*/
|
||||
hasActiveInputSession() {
|
||||
return gActiveInputSession != null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} keyword The keyword to look up.
|
||||
* @return {string} the description to use for the heuristic result.
|
||||
*/
|
||||
getDescription(keyword) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
return gKeywordMap.get(keyword).description;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the default suggestion for the registered keyword. The suggestion's
|
||||
* description will be used for the comment in the heuristic result.
|
||||
*
|
||||
* @param {string} keyword The keyword.
|
||||
* @param {string} description The description to use for the heuristic result.
|
||||
*/
|
||||
setDefaultSuggestion(keyword, {description}) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
gKeywordMap.get(keyword).description = description;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds suggestions for the registered keyword. This function will throw if
|
||||
* the keyword provided is not registered or active, or if the callback ID
|
||||
* provided is no longer equal to the active callback ID.
|
||||
*
|
||||
* @param {string} keyword The keyword.
|
||||
* @param {integer} id The ID of the suggestion callback.
|
||||
* @param {Array<Object>} suggestions An array of suggestions to provide to the urlbar.
|
||||
*/
|
||||
addSuggestions(keyword, id, suggestions) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (!gActiveInputSession || gActiveInputSession.keyword != keyword) {
|
||||
throw new Error(`The keyword provided is not apart of an active input session: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (id != gCurrentCallbackID) {
|
||||
throw new Error(`The callback is no longer active for the keyword provided: "${keyword}"`);
|
||||
}
|
||||
|
||||
gActiveInputSession.addSuggestions(suggestions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the input in the urlbar begins with `<keyword><space>`.
|
||||
*
|
||||
* If the keyword is inactive, MSG_INPUT_STARTED is emitted and the
|
||||
* keyword is marked as active. If the keyword is followed by any text,
|
||||
* MSG_INPUT_CHANGED is fired with the current callback ID that can be
|
||||
* used to provide suggestions to the urlbar while the callback ID is active.
|
||||
* The callback is invalidated when either the input changes or the urlbar blurs.
|
||||
*
|
||||
* @param {string} keyword The keyword to handle.
|
||||
* @param {string} text The search text in the urlbar.
|
||||
* @param {Function} callback The callback used to provide search suggestions.
|
||||
* @return {Promise} promise that resolves when the current search is complete.
|
||||
*/
|
||||
handleSearch(keyword, text, callback) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
|
||||
throw new Error("A different input session is already ongoing");
|
||||
}
|
||||
|
||||
if (!text || !text.startsWith(`${keyword} `)) {
|
||||
throw new Error(`The text provided must start with: "${keyword} "`);
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
throw new Error("A callback must be provided");
|
||||
}
|
||||
|
||||
let {extension} = gKeywordMap.get(keyword);
|
||||
|
||||
// The search text in the urlbar currently starts with <keyword><space>, and
|
||||
// we only want the text that follows.
|
||||
text = text.substring(keyword.length + 1);
|
||||
|
||||
// We fire MSG_INPUT_STARTED once we have <keyword><space>, and only fire
|
||||
// MSG_INPUT_CHANGED when we have text to process. This is different from
|
||||
// Chrome's behavior, which fires MSG_INPUT_STARTED right before MSG_INPUT_CHANGED
|
||||
// first fires, but this is a bug in Chrome according to https://crbug.com/258911.
|
||||
if (!gActiveInputSession) {
|
||||
gActiveInputSession = new InputSession(keyword, extension);
|
||||
gActiveInputSession.start(this.MSG_INPUT_STARTED);
|
||||
|
||||
// Update the input session if there is text to process.
|
||||
if (text.length) {
|
||||
gActiveInputSession.registerSuggestionsCallback(callback);
|
||||
gActiveInputSession.update(this.MSG_INPUT_CHANGED, text);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
gActiveInputSession.registerSuggestionsCallback(callback);
|
||||
gActiveInputSession.update(this.MSG_INPUT_CHANGED, text);
|
||||
|
||||
return new Promise(resolve => {
|
||||
gActiveInputSession.registerSearchCompleteCallback(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user submits a suggestion that was added by
|
||||
* an extension. MSG_INPUT_ENTERED is emitted to the extension with
|
||||
* the keyword, the current search string, and info about how the
|
||||
* the search should be handled. This ends the active input session.
|
||||
*
|
||||
* @param {string} keyword The keyword associated to the suggestion.
|
||||
* @param {string} text The search text in the urlbar.
|
||||
* @param {string} where Where the page should be opened. Accepted values are:
|
||||
* "current": open the page in the same tab.
|
||||
* "tab": open the page in a new foreground tab.
|
||||
* "tabshifted": open the page in a new background tab.
|
||||
*/
|
||||
handleInputEntered(keyword, text, where) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
|
||||
throw new Error("A different input session is already ongoing");
|
||||
}
|
||||
|
||||
if (!text || !text.startsWith(`${keyword} `)) {
|
||||
throw new Error(`The text provided must start with: "${keyword} "`);
|
||||
}
|
||||
|
||||
let dispositionMap = {
|
||||
current: "currentTab",
|
||||
tab: "newForegroundTab",
|
||||
tabshifted: "newBackgroundTab",
|
||||
}
|
||||
let disposition = dispositionMap[where];
|
||||
|
||||
if (!disposition) {
|
||||
throw new Error(`Invalid "where" argument: ${where}`);
|
||||
}
|
||||
|
||||
let {extension} = gKeywordMap.get(keyword);
|
||||
|
||||
// The search text in the urlbar currently starts with <keyword><space>, and
|
||||
// we only want to send the text that follows.
|
||||
text = text.substring(keyword.length + 1);
|
||||
|
||||
gActiveInputSession.end(this.MSG_INPUT_ENTERED, text, disposition)
|
||||
gActiveInputSession = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* If the user has ended the keyword input session without accepting the input,
|
||||
* MSG_INPUT_CANCELLED is emitted and the input session is ended.
|
||||
*/
|
||||
handleInputCancelled() {
|
||||
if (!gActiveInputSession) {
|
||||
throw new Error("There is no active input session");
|
||||
}
|
||||
gActiveInputSession.cancel(this.MSG_INPUT_CANCELLED);
|
||||
gActiveInputSession = null;
|
||||
}
|
||||
});
|
|
@ -69,6 +69,11 @@ const FRECENCY_DEFAULT = 1000;
|
|||
// always try to have at least MINIMUM_LOCAL_MATCHES local matches.
|
||||
const MINIMUM_LOCAL_MATCHES = 6;
|
||||
|
||||
// Extensions are allowed to add suggestions if they have registered a keyword
|
||||
// with the omnibox API. This is the maximum number of suggestions an extension
|
||||
// is allowed to add for a given search string.
|
||||
const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
|
||||
|
||||
// A regex that matches "single word" hostnames for whitelisting purposes.
|
||||
// The hostname will already have been checked for general validity, so we
|
||||
// don't need to be exhaustive here, so allow dashes anywhere.
|
||||
|
@ -264,6 +269,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
|
||||
"resource://gre/modules/ExtensionSearchHandler.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
|
||||
|
@ -715,11 +722,16 @@ function Search(searchString, searchParam, autocompleteListener,
|
|||
|
||||
// The index to insert remote matches at.
|
||||
this._remoteMatchesStartIndex = 0;
|
||||
// The index to insert local matches at.
|
||||
|
||||
this._localMatchesStartIndex = 0;
|
||||
|
||||
// Counts the number of inserted local matches.
|
||||
this._localMatchesCount = 0;
|
||||
// Counts the number of inserted remote matches.
|
||||
this._remoteMatchesCount = 0;
|
||||
// Counts the number of inserted extension matches.
|
||||
this._extensionMatchesCount = 0;
|
||||
}
|
||||
|
||||
Search.prototype = {
|
||||
|
@ -942,7 +954,19 @@ Search.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure to fill any remaining space.
|
||||
// Only add extension suggestions if the first token is a registered keyword
|
||||
// and the search string has characters after the first token.
|
||||
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
|
||||
this._originalSearchString.length > this._searchTokens[0].length) {
|
||||
yield this._matchExtensionSuggestions();
|
||||
if (!this.pending)
|
||||
return;
|
||||
} else if (ExtensionSearchHandler.hasActiveInputSession()) {
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
}
|
||||
|
||||
// Ensure to fill any remaining space. Suggestions which come from extensions are
|
||||
// inserted at the beginning, so any suggestions
|
||||
yield Promise.all(this._remoteMatchesPromises);
|
||||
}),
|
||||
|
||||
|
@ -950,7 +974,15 @@ Search.prototype = {
|
|||
// We always try to make the first result a special "heuristic" result. The
|
||||
// heuristics below determine what type of result it will be, if any.
|
||||
|
||||
let hasSearchTerms = this._searchTokens.length > 0 ;
|
||||
let hasSearchTerms = this._searchTokens.length > 0;
|
||||
|
||||
if (hasSearchTerms) {
|
||||
// It may be a keyword registered by an extension.
|
||||
let matched = yield this._matchExtensionHeuristicResult();
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._enableActions && hasSearchTerms) {
|
||||
// It may be a search engine with an alias - which works like a keyword.
|
||||
|
@ -1124,6 +1156,16 @@ Search.prototype = {
|
|||
return gotResult;
|
||||
},
|
||||
|
||||
_matchExtensionHeuristicResult: function* () {
|
||||
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
|
||||
this._originalSearchString.length > this._searchTokens[0].length) {
|
||||
let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
|
||||
this._addExtensionMatch(this._originalSearchString, description);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_matchPlacesKeyword: function* () {
|
||||
// The first word could be a keyword, so that's what we'll search.
|
||||
let keyword = this._searchTokens[0];
|
||||
|
@ -1236,6 +1278,24 @@ Search.prototype = {
|
|||
return true;
|
||||
},
|
||||
|
||||
_addExtensionMatch(content, comment) {
|
||||
if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._addMatch({
|
||||
value: PlacesUtils.mozActionURI("extension", {
|
||||
content,
|
||||
keyword: this._searchTokens[0]
|
||||
}),
|
||||
comment,
|
||||
icon: "chrome://browser/content/extension.svg",
|
||||
style: "action extension",
|
||||
frecency: FRECENCY_DEFAULT,
|
||||
extension: true,
|
||||
});
|
||||
},
|
||||
|
||||
_addSearchEngineMatch(match, query, suggestion) {
|
||||
let actionURLParams = {
|
||||
engineName: match.engineName,
|
||||
|
@ -1259,6 +1319,18 @@ Search.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
*_matchExtensionSuggestions() {
|
||||
let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
|
||||
suggestions => {
|
||||
suggestions.forEach(suggestion => {
|
||||
let content = `${this._searchTokens[0]} ${suggestion.content}`;
|
||||
this._addExtensionMatch(content, suggestion.description);
|
||||
});
|
||||
}
|
||||
);
|
||||
this._remoteMatchesPromises.push(promise);
|
||||
},
|
||||
|
||||
*_matchRemoteTabs() {
|
||||
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
|
||||
for (let {url, title, icon, deviceName} of matches) {
|
||||
|
@ -1464,6 +1536,11 @@ Search.prototype = {
|
|||
// Append after local matches.
|
||||
index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
|
||||
this._remoteMatchesCount++;
|
||||
} else if (match.extension) {
|
||||
index = this._localMatchesStartIndex;
|
||||
this._localMatchesStartIndex++;
|
||||
this._remoteMatchesStartIndex++;
|
||||
this._extensionMatchesCount++;
|
||||
} else {
|
||||
// This is a local match.
|
||||
if (match.frecency > FRECENCY_DEFAULT ||
|
||||
|
|
|
@ -64,6 +64,7 @@ if CONFIG['MOZ_PLACES']:
|
|||
'ClusterLib.js',
|
||||
'ColorAnalyzer_worker.js',
|
||||
'ColorConversion.js',
|
||||
'ExtensionSearchHandler.jsm',
|
||||
'History.jsm',
|
||||
'PlacesBackups.jsm',
|
||||
'PlacesDBUtils.jsm',
|
||||
|
|
|
@ -167,20 +167,21 @@ function* check_autocomplete(test) {
|
|||
do_print("onSearchBegin received");
|
||||
numSearchesStarted++;
|
||||
};
|
||||
let deferred = Promise.defer();
|
||||
input.onSearchComplete = () => {
|
||||
do_print("onSearchComplete received");
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
let searchCompletePromise = new Promise(resolve => {
|
||||
input.onSearchComplete = () => {
|
||||
do_print("onSearchComplete received");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
let expectedSearches = 1;
|
||||
if (test.incompleteSearch) {
|
||||
controller.startSearch(test.incompleteSearch);
|
||||
expectedSearches++;
|
||||
}
|
||||
|
||||
do_print("Searching for: '" + test.search + "'");
|
||||
controller.startSearch(test.search);
|
||||
yield deferred.promise;
|
||||
yield searchCompletePromise;
|
||||
|
||||
Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
|
||||
|
||||
|
@ -415,6 +416,22 @@ function makeSwitchToTabMatch(url, extra = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
function makeExtensionMatch(extra = {}) {
|
||||
let style = [ "action", "extension" ];
|
||||
if (extra.heuristic) {
|
||||
style.push("heuristic");
|
||||
}
|
||||
|
||||
return {
|
||||
uri: makeActionURI("extension", {
|
||||
content: extra.content,
|
||||
keyword: extra.keyword,
|
||||
}),
|
||||
title: extra.description,
|
||||
style,
|
||||
};
|
||||
}
|
||||
|
||||
function setFaviconForHref(href, iconHref) {
|
||||
return new Promise(resolve => {
|
||||
PlacesUtils.favicons.setAndFetchFaviconForPage(
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim:set ts=2 sw=2 sts=2 et:
|
||||
* 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/. */
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm");
|
||||
|
||||
let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController);
|
||||
|
||||
add_task(function* test_correct_errors_are_thrown() {
|
||||
let keyword = "foo";
|
||||
let anotherKeyword = "bar";
|
||||
let unregisteredKeyword = "baz";
|
||||
|
||||
// Register a keyword.
|
||||
ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
|
||||
|
||||
// Try registering the keyword again.
|
||||
Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }));
|
||||
|
||||
// Register a different keyword.
|
||||
ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
|
||||
|
||||
// Try calling handleSearch for an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {}));
|
||||
|
||||
// Try calling handleSearch without a callback.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `));
|
||||
|
||||
// Try getting the description for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword));
|
||||
|
||||
// Try getting the extension name for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword));
|
||||
|
||||
// Try setting the default suggestion for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion"));
|
||||
|
||||
// Try calling handleInputCancelled when there is no active input session.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
|
||||
|
||||
// Try calling handleInputEntered when there is no active input session.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
|
||||
|
||||
// Start a session by calling handleSearch with the registered keyword.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {});
|
||||
|
||||
// Try providing suggestions for an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []));
|
||||
|
||||
// Try providing suggestions for an inactive keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []));
|
||||
|
||||
// Try calling handleSearch for an inactive keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {}));
|
||||
|
||||
// Try calling addSuggestions with an old callback ID.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
|
||||
|
||||
// Add suggestions with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
|
||||
|
||||
// Add suggestions again with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
|
||||
|
||||
// Try calling addSuggestions with a future callback ID.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
|
||||
|
||||
// End the input session by calling handleInputCancelled.
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
|
||||
// Try calling handleInputCancelled after the session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
|
||||
|
||||
// Try calling handleSearch that doesn't have a space after the keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {}));
|
||||
|
||||
// Try calling handleSearch with text starting with the wrong keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {}));
|
||||
|
||||
// Start a new session by calling handleSearch with a different keyword
|
||||
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
|
||||
|
||||
// Try adding suggestions again with the same callback ID now that the input session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, []));
|
||||
|
||||
// Add suggestions with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
|
||||
|
||||
// Try adding suggestions with a valid callback ID but a different keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
|
||||
|
||||
// Try adding suggestions with a valid callback ID but an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []));
|
||||
|
||||
// Set the default suggestion.
|
||||
ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
|
||||
|
||||
// Try ending the session using handleInputEntered with a different keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab"));
|
||||
|
||||
// Try calling handleInputEntered with invalid text.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"));
|
||||
|
||||
// Try calling handleInputEntered with an invalid disposition.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid"));
|
||||
|
||||
// End the session by calling handleInputEntered.
|
||||
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
|
||||
|
||||
// Try calling handleInputEntered after the session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
|
||||
|
||||
// Unregister the keyword.
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
|
||||
// Try setting the default suggestion for the unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"}));
|
||||
|
||||
// Try handling a search with the unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}));
|
||||
|
||||
// Try unregistering the keyword again.
|
||||
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword));
|
||||
|
||||
// Unregister the other keyword.
|
||||
ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
|
||||
|
||||
// Try unregistering the word which was never registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword));
|
||||
|
||||
// Try setting the default suggestion for a word that was never registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"}));
|
||||
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_correct_events_are_emitted() {
|
||||
let events = [];
|
||||
function checkEvents(expectedEvents) {
|
||||
Assert.equal(events.length, expectedEvents.length, "The correct number of events fired");
|
||||
expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`));
|
||||
events = [];
|
||||
}
|
||||
|
||||
let mockExtension = { emit: message => events.push(message) };
|
||||
|
||||
let keyword = "foo";
|
||||
let anotherKeyword = "bar";
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
|
||||
|
||||
ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab");
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
|
||||
checkEvents([
|
||||
ExtensionSearchHandler.MSG_INPUT_STARTED,
|
||||
ExtensionSearchHandler.MSG_INPUT_CHANGED
|
||||
]);
|
||||
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {});
|
||||
checkEvents([
|
||||
ExtensionSearchHandler.MSG_INPUT_STARTED,
|
||||
ExtensionSearchHandler.MSG_INPUT_CHANGED
|
||||
]);
|
||||
|
||||
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} baz`, "tab");
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
});
|
||||
|
||||
add_task(function* test_removes_suggestion_if_its_content_is_typed_in() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Foo Bar";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "foo", description: "first suggestion"},
|
||||
{content: "bar", description: "second suggestion"},
|
||||
{content: "baz", description: "third suggestion"},
|
||||
]);
|
||||
controller.stopSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} unmatched`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} foo`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} bar`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} baz`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_extension_results_should_come_first() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let uri = NetUtil.newURI(`http://a.com/b`);
|
||||
yield PlacesTestUtils.addVisits([
|
||||
{ uri, title: `${keyword} -` },
|
||||
]);
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "foo", description: "first suggestion"},
|
||||
{content: "bar", description: "second suggestion"},
|
||||
{content: "baz", description: "third suggestion"},
|
||||
]);
|
||||
}
|
||||
controller.stopSearch();
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
// Start an input session before testing MSG_INPUT_CHANGED.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} -`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}),
|
||||
{ uri, title: `${keyword} -` }
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_setting_the_default_suggestion() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, []);
|
||||
}
|
||||
controller.stopSearch();
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
|
||||
description: "hello world"
|
||||
});
|
||||
|
||||
let searchString = `${keyword} search query`;
|
||||
yield check_autocomplete({
|
||||
search: searchString,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
|
||||
description: "foo bar"
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: searchString,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_maximum_number_of_suggestions_is_enforced() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "a", description: "first suggestion"},
|
||||
{content: "b", description: "second suggestion"},
|
||||
{content: "c", description: "third suggestion"},
|
||||
{content: "d", description: "fourth suggestion"},
|
||||
{content: "e", description: "fifth suggestion"},
|
||||
{content: "f", description: "sixth suggestion"},
|
||||
{content: "g", description: "seventh suggestion"},
|
||||
{content: "h", description: "eigth suggestion"},
|
||||
{content: "i", description: "ninth suggestion"},
|
||||
{content: "j", description: "tenth suggestion"},
|
||||
]);
|
||||
controller.stopSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
// Start an input session before testing MSG_INPUT_CHANGED.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} #`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
|
@ -24,6 +24,7 @@ support-files =
|
|||
[test_empty_search.js]
|
||||
[test_enabled.js]
|
||||
[test_escape_self.js]
|
||||
[test_extension_matches.js]
|
||||
[test_ignore_protocol.js]
|
||||
[test_keyword_search.js]
|
||||
[test_keyword_search_actions.js]
|
||||
|
|
|
@ -2127,6 +2127,10 @@ extends="chrome://global/content/bindings/popup.xml#popup">
|
|||
titleLooksLikeUrl = true;
|
||||
let visitStr = this._stringBundle.GetStringFromName("visit");
|
||||
this._setUpDescription(this._actionText, visitStr, true);
|
||||
} else if (action.type == "extension") {
|
||||
let content = action.params.content;
|
||||
displayUrl = content;
|
||||
this._setUpDescription(this._actionText, content, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче