gecko-dev/toolkit/components/satchel/nsFormAutoComplete.js
Andrew McCreight 5dec0e0beb Bug 1432992, part 1 - Remove definitions of Ci, Cr, Cc, and Cu. r=florian
This patch was autogenerated by my decomponents.py

It covers almost every file with the extension js, jsm, html, py,
xhtml, or xul.

It removes blank lines after removed lines, when the removed lines are
preceded by either blank lines or the start of a new block. The "start
of a new block" is defined fairly hackily: either the line starts with
//, ends with */, ends with {, <![CDATA[, """ or '''. The first two
cover comments, the third one covers JS, the fourth covers JS embedded
in XUL, and the final two cover JS embedded in Python. This also
applies if the removed line was the first line of the file.

It covers the pattern matching cases like "var {classes: Cc,
interfaces: Ci, utils: Cu, results: Cr} = Components;". It'll remove
the entire thing if they are all either Ci, Cr, Cc or Cu, or it will
remove the appropriate ones and leave the residue behind. If there's
only one behind, then it will turn it into a normal, non-pattern
matching variable definition. (For instance, "const { classes: Cc,
Constructor: CC, interfaces: Ci, utils: Cu } = Components" becomes
"const CC = Components.Constructor".)

MozReview-Commit-ID: DeSHcClQ7cG

--HG--
extra : rebase_source : d9c41878036c1ef7766ef5e91a7005025bc1d72b
2018-02-06 09:36:57 -08:00

633 lines
21 KiB
JavaScript

/* vim: set ts=4 sts=4 sw=4 et tw=80: */
/* 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";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserUtils", "resource://gre/modules/BrowserUtils.jsm");
function isAutocompleteDisabled(aField) {
if (aField.autocomplete !== "") {
return aField.autocomplete === "off";
}
return aField.form && aField.form.autocomplete === "off";
}
/**
* An abstraction to talk with the FormHistory database over
* the message layer. FormHistoryClient will take care of
* figuring out the most appropriate message manager to use,
* and what things to send.
*
* It is assumed that nsFormAutoComplete will only ever use
* one instance at a time, and will not attempt to perform more
* than one search request with the same instance at a time.
* However, nsFormAutoComplete might call remove() any number of
* times with the same instance of the client.
*
* @param {Object} clientInfo
* Info required to build the FormHistoryClient
* @param {Node} clientInfo.formField
* A DOM node that we're requesting form history for.
* @param {string} clientInfo.inputName
* The name of the input to do the FormHistory look-up with.
* If this is searchbar-history, then formField needs to be null,
* otherwise constructing will throw.
*/
function FormHistoryClient({ formField, inputName }) {
if (formField && inputName != this.SEARCHBAR_ID) {
let window = formField.ownerGlobal;
let topDocShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.sameTypeRootTreeItem
.QueryInterface(Ci.nsIDocShell);
this.mm = topDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
} else {
if (inputName == this.SEARCHBAR_ID && formField) {
throw new Error("FormHistoryClient constructed with both a " +
"formField and an inputName. This is not " +
"supported, and only empty results will be " +
"returned.");
}
this.mm = Services.cpmm;
}
this.inputName = inputName;
this.id = FormHistoryClient.nextRequestID++;
}
FormHistoryClient.prototype = {
SEARCHBAR_ID: "searchbar-history",
// It is assumed that nsFormAutoComplete only uses / cares about
// one FormHistoryClient at a time, and won't attempt to have
// multiple in-flight searches occurring with the same FormHistoryClient.
// We use an ID number per instantiated FormHistoryClient to make
// sure we only respond to messages that were meant for us.
id: 0,
callback: null,
inputName: "",
mm: null,
/**
* Query FormHistory for some results.
*
* @param {string} searchString
* The string to search FormHistory for. See
* FormHistory.getAutoCompleteResults.
* @param {Object} params
* An Object with search properties. See
* FormHistory.getAutoCompleteResults.
* @param {function} callback
* A callback function that will take a single
* argument (the found entries).
*/
requestAutoCompleteResults(searchString, params, callback) {
this.mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
id: this.id,
searchString,
params,
});
this.mm.addMessageListener("FormHistory:AutoCompleteSearchResults", this);
this.callback = callback;
},
/**
* Cancel an in-flight results request. This ensures that the
* callback that requestAutoCompleteResults was passed is never
* called from this FormHistoryClient.
*/
cancel() {
this.clearListeners();
},
/**
* Remove an item from FormHistory.
*
* @param {string} value
*
* The value to remove for this particular
* field.
*
* @param {string} guid
*
* The guid for the item being removed.
*/
remove(value, guid) {
this.mm.sendAsyncMessage("FormHistory:RemoveEntry", {
inputName: this.inputName,
value,
guid,
});
},
// Private methods
receiveMessage(msg) {
let { id, results } = msg.data;
if (id != this.id) {
return;
}
if (!this.callback) {
Cu.reportError("FormHistoryClient received message with no callback");
return;
}
this.callback(results);
this.clearListeners();
},
clearListeners() {
this.mm.removeMessageListener("FormHistory:AutoCompleteSearchResults", this);
this.callback = null;
},
};
FormHistoryClient.nextRequestID = 1;
function FormAutoComplete() {
this.init();
}
/**
* Implements the nsIFormAutoComplete interface in the main process.
*/
FormAutoComplete.prototype = {
classID: Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
_prefBranch: null,
_debug: true, // mirrors browser.formfill.debug
_enabled: true, // mirrors browser.formfill.enable preference
_agedWeight: 2,
_bucketSize: 1,
_maxTimeGroupings: 25,
_timeGroupingSize: 7 * 24 * 60 * 60 * 1000 * 1000,
_expireDays: null,
_boundaryWeight: 25,
_prefixWeight: 5,
// Only one query via FormHistoryClient is performed at a time, and the
// most recent FormHistoryClient which will be stored in _pendingClient
// while the query is being performed. It will be cleared when the query
// finishes, is cancelled, or an error occurs. If a new query occurs while
// one is already pending, the existing one is cancelled.
_pendingClient: null,
init() {
// Preferences. Add observer so we get notified of changes.
this._prefBranch = Services.prefs.getBranch("browser.formfill.");
this._prefBranch.addObserver("", this.observer, true);
this.observer._self = this;
this._debug = this._prefBranch.getBoolPref("debug");
this._enabled = this._prefBranch.getBoolPref("enable");
this._agedWeight = this._prefBranch.getIntPref("agedWeight");
this._bucketSize = this._prefBranch.getIntPref("bucketSize");
this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
this._expireDays = this._prefBranch.getIntPref("expire_days");
},
observer: {
_self: null,
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
]),
observe(subject, topic, data) {
let self = this._self;
if (topic == "nsPref:changed") {
let prefName = data;
self.log("got change to " + prefName + " preference");
switch (prefName) {
case "agedWeight":
self._agedWeight = self._prefBranch.getIntPref(prefName);
break;
case "debug":
self._debug = self._prefBranch.getBoolPref(prefName);
break;
case "enable":
self._enabled = self._prefBranch.getBoolPref(prefName);
break;
case "maxTimeGroupings":
self._maxTimeGroupings = self._prefBranch.getIntPref(prefName);
break;
case "timeGroupingSize":
self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000;
break;
case "bucketSize":
self._bucketSize = self._prefBranch.getIntPref(prefName);
break;
case "boundaryWeight":
self._boundaryWeight = self._prefBranch.getIntPref(prefName);
break;
case "prefixWeight":
self._prefixWeight = self._prefBranch.getIntPref(prefName);
break;
default:
self.log("Oops! Pref not handled, change ignored.");
}
}
},
},
// AutoCompleteE10S needs to be able to call autoCompleteSearchAsync without
// going through IDL in order to pass a mock DOM object field.
get wrappedJSObject() {
return this;
},
/*
* log
*
* Internal function for logging debug messages to the Error Console
* window
*/
log(message) {
if (!this._debug) {
return;
}
dump("FormAutoComplete: " + message + "\n");
Services.console.logStringMessage("FormAutoComplete: " + message);
},
/*
* autoCompleteSearchAsync
*
* aInputName -- |name| attribute from the form input being autocompleted.
* aUntrimmedSearchString -- current value of the input
* aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
* aPreviousResult -- previous search result, if any.
* aDatalistResult -- results from list=datalist for aField.
* aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
* that may be returned asynchronously.
*/
autoCompleteSearchAsync(aInputName,
aUntrimmedSearchString,
aField,
aPreviousResult,
aDatalistResult,
aListener) {
function sortBytotalScore(a, b) {
return b.totalScore - a.totalScore;
}
// Guard against void DOM strings filtering into this code.
if (typeof aInputName === "object") {
aInputName = "";
}
if (typeof aUntrimmedSearchString === "object") {
aUntrimmedSearchString = "";
}
let client = new FormHistoryClient({ formField: aField, inputName: aInputName });
// If we have datalist results, they become our "empty" result.
let emptyResult = aDatalistResult || new FormAutoCompleteResult(client,
[],
aInputName,
aUntrimmedSearchString,
null);
if (!this._enabled) {
if (aListener) {
aListener.onSearchCompletion(emptyResult);
}
return;
}
// don't allow form inputs (aField != null) to get results from search bar history
if (aInputName == "searchbar-history" && aField) {
this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
if (aListener) {
aListener.onSearchCompletion(emptyResult);
}
return;
}
if (aField && isAutocompleteDisabled(aField)) {
this.log("autoCompleteSearch not allowed due to autcomplete=off");
if (aListener) {
aListener.onSearchCompletion(emptyResult);
}
return;
}
this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
let searchString = aUntrimmedSearchString.trim().toLowerCase();
// reuse previous results if:
// a) length greater than one character (others searches are special cases) AND
// b) the the new results will be a subset of the previous results
if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
searchString.includes(aPreviousResult.searchString.trim().toLowerCase())) {
this.log("Using previous autocomplete result");
let result = aPreviousResult;
let wrappedResult = result.wrappedJSObject;
wrappedResult.searchString = aUntrimmedSearchString;
// Leaky abstraction alert: it would be great to be able to split
// this code between nsInputListAutoComplete and here but because of
// the way we abuse the formfill autocomplete API in e10s, we have
// to deal with the <datalist> results here as well (and down below
// in mergeResults).
// If there were datalist results result is a FormAutoCompleteResult
// as defined in nsFormAutoCompleteResult.jsm with the entire list
// of results in wrappedResult._values and only the results from
// form history in wrappedResult.entries.
// First, grab the entire list of old results.
let allResults = wrappedResult._labels;
let datalistResults, datalistLabels;
if (allResults) {
// We have datalist results, extract them from the values array.
// Both allResults and values arrays are in the form of:
// |--wR.entries--|
// <history entries><datalist entries>
let oldLabels = allResults.slice(wrappedResult.entries.length);
let oldValues = wrappedResult._values.slice(wrappedResult.entries.length);
datalistLabels = [];
datalistResults = [];
for (let i = 0; i < oldLabels.length; ++i) {
if (oldLabels[i].toLowerCase().includes(searchString)) {
datalistLabels.push(oldLabels[i]);
datalistResults.push(oldValues[i]);
}
}
}
let searchTokens = searchString.split(/\s+/);
// We have a list of results for a shorter search string, so just
// filter them further based on the new search string and add to a new array.
let entries = wrappedResult.entries;
let filteredEntries = [];
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
// Remove results that do not contain the token
// XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
if (searchTokens.some(tok => !entry.textLowerCase.includes(tok))) {
continue;
}
this._calculateScore(entry, searchString, searchTokens);
this.log("Reusing autocomplete entry '" + entry.text +
"' (" + entry.frecency + " / " + entry.totalScore + ")");
filteredEntries.push(entry);
}
filteredEntries.sort(sortBytotalScore);
wrappedResult.entries = filteredEntries;
// If we had datalistResults, re-merge them back into the filtered
// entries.
if (datalistResults) {
filteredEntries = filteredEntries.map(elt => elt.text);
let comments = new Array(filteredEntries.length + datalistResults.length).fill("");
comments[filteredEntries.length] = "separator";
// History entries don't have labels (their labels would be read
// from their values). Pad out the labels array so the datalist
// results (which do have separate values and labels) line up.
datalistLabels = new Array(filteredEntries.length).fill("").concat(datalistLabels);
wrappedResult._values = filteredEntries.concat(datalistResults);
wrappedResult._labels = datalistLabels;
wrappedResult._comments = comments;
}
if (aListener) {
aListener.onSearchCompletion(result);
}
} else {
this.log("Creating new autocomplete search result.");
// Start with an empty list.
let result = aDatalistResult ?
new FormAutoCompleteResult(client, [], aInputName, aUntrimmedSearchString, null) :
emptyResult;
let processEntry = (aEntries) => {
if (aField && aField.maxLength > -1) {
result.entries =
aEntries.filter(el => el.text.length <= aField.maxLength);
} else {
result.entries = aEntries;
}
if (aDatalistResult && aDatalistResult.matchCount > 0) {
result = this.mergeResults(result, aDatalistResult);
}
if (aListener) {
aListener.onSearchCompletion(result);
}
};
this.getAutoCompleteValues(client, aInputName, searchString, processEntry);
}
},
mergeResults(historyResult, datalistResult) {
let values = datalistResult.wrappedJSObject._values;
let labels = datalistResult.wrappedJSObject._labels;
let comments = new Array(values.length).fill("");
// historyResult will be null if form autocomplete is disabled. We
// still want the list values to display.
let entries = historyResult.wrappedJSObject.entries;
let historyResults = entries.map(entry => entry.text);
let historyComments = new Array(entries.length).fill("");
// now put the history results above the datalist suggestions
let finalValues = historyResults.concat(values);
let finalLabels = historyResults.concat(labels);
let finalComments = historyComments.concat(comments);
// This is ugly: there are two FormAutoCompleteResult classes in the
// tree, one in a module and one in this file. Datalist results need to
// use the one defined in the module but the rest of this file assumes
// that we use the one defined here. To get around that, we explicitly
// import the module here, out of the way of the other uses of
// FormAutoCompleteResult.
let {FormAutoCompleteResult} = ChromeUtils.import(
"resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
return new FormAutoCompleteResult(datalistResult.searchString,
Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
0,
"",
finalValues,
finalLabels,
finalComments,
historyResult);
},
stopAutoCompleteSearch() {
if (this._pendingClient) {
this._pendingClient.cancel();
this._pendingClient = null;
}
},
/*
* Get the values for an autocomplete list given a search string.
*
* client - a FormHistoryClient instance to perform the search with
* fieldName - fieldname field within form history (the form input name)
* searchString - string to search for
* callback - called when the values are available. Passed an array of objects,
* containing properties for each result. The callback is only called
* when successful.
*/
getAutoCompleteValues(client, fieldName, searchString, callback) {
let params = {
agedWeight: this._agedWeight,
bucketSize: this._bucketSize,
expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
fieldname: fieldName,
maxTimeGroupings: this._maxTimeGroupings,
timeGroupingSize: this._timeGroupingSize,
prefixWeight: this._prefixWeight,
boundaryWeight: this._boundaryWeight,
};
this.stopAutoCompleteSearch();
client.requestAutoCompleteResults(searchString, params, (entries) => {
this._pendingClient = null;
callback(entries);
});
this._pendingClient = client;
},
/*
* _calculateScore
*
* entry -- an nsIAutoCompleteResult entry
* aSearchString -- current value of the input (lowercase)
* searchTokens -- array of tokens of the search string
*
* Returns: an int
*/
_calculateScore(entry, aSearchString, searchTokens) {
let boundaryCalc = 0;
// for each word, calculate word boundary weights
for (let token of searchTokens) {
if (entry.textLowerCase.startsWith(token)) {
boundaryCalc++;
}
if (entry.textLowerCase.includes(" " + token)) {
boundaryCalc++;
}
}
boundaryCalc = boundaryCalc * this._boundaryWeight;
// now add more weight if we have a traditional prefix match and
// multiply boundary bonuses by boundary weight
if (entry.textLowerCase.startsWith(aSearchString)) {
boundaryCalc += this._prefixWeight;
}
entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
},
}; // end of FormAutoComplete implementation
// nsIAutoCompleteResult implementation
function FormAutoCompleteResult(client,
entries,
fieldName,
searchString,
messageManager) {
this.client = client;
this.entries = entries;
this.fieldName = fieldName;
this.searchString = searchString;
this.messageManager = messageManager;
}
FormAutoCompleteResult.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, Ci.nsISupportsWeakReference]),
// private
client: null,
entries: null,
fieldName: null,
_checkIndexBounds(index) {
if (index < 0 || index >= this.entries.length) {
throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
}
},
// Allow autoCompleteSearch to get at the JS object so it can
// modify some readonly properties for internal use.
get wrappedJSObject() {
return this;
},
// Interfaces from idl...
searchString: "",
errorDescription: "",
get defaultIndex() {
if (this.entries.length == 0) {
return -1;
}
return 0;
},
get searchResult() {
if (this.entries.length == 0) {
return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
}
return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
},
get matchCount() {
return this.entries.length;
},
getValueAt(index) {
this._checkIndexBounds(index);
return this.entries[index].text;
},
getLabelAt(index) {
return this.getValueAt(index);
},
getCommentAt(index) {
this._checkIndexBounds(index);
return "";
},
getStyleAt(index) {
this._checkIndexBounds(index);
return "";
},
getImageAt(index) {
this._checkIndexBounds(index);
return "";
},
getFinalCompleteValueAt(index) {
return this.getValueAt(index);
},
removeValueAt(index, removeFromDB) {
this._checkIndexBounds(index);
let [removedEntry] = this.entries.splice(index, 1);
if (removeFromDB) {
this.client.remove(removedEntry.text, removedEntry.guid);
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutoComplete]);