--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -550,17 +550,18 @@
this._setHighlightAll(aHighlight, aFromPrefObserver);
let word = this._findField.value;
// Bug 429723. Don't attempt to highlight ""
if (aHighlight && !word)
return;
- this.browser.finder.highlight(aHighlight, word);
+ this.browser.finder.highlight(aHighlight, word,
+ this._findMode == this.FIND_LINKS);
// Update the matches count
this._updateMatchesCount(this.nsITypeAheadFind.FIND_FOUND);
]]></body>
</method>
<!--
- Updates the highlight-all mode of the findbar and its UI.
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -35,17 +35,26 @@ function Finder(docShell) {
this._highlighter = null;
docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress)
.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
}
Finder.prototype = {
+ get iterator() {
+ if (this._iterator)
+ return this._iterator;
+ this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
+ return this._iterator;
+ },
+
destroy: function() {
+ if (this._iterator)
+ this._iterator.reset();
if (this._highlighter) {
this._highlighter.clear();
this._highlighter.hide();
}
this.listeners = [];
this._docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress)
.removeProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
@@ -82,16 +91,22 @@ Finder.prototype = {
linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
}
options.linkURL = linkURL;
options.rect = this._getResultRect();
options.searchString = this._searchString;
+ if (!this.iterator.continueRunning({
+ linksOnly: options.linksOnly,
+ word: options.searchString
+ })) {
+ this.iterator.stop();
+ }
this.highlighter.update(options);
for (let l of this._listeners) {
try {
l.onFindResult(options);
} catch (ex) {}
}
},
@@ -145,17 +160,18 @@ Finder.prototype = {
fastFind: function (aSearchString, aLinksOnly, aDrawOutline) {
this._lastFindResult = this._fastFind.find(aSearchString, aLinksOnly);
let searchString = this._fastFind.searchString;
this._notify({
searchString,
result: this._lastFindResult,
findBackwards: false,
findAgain: false,
- drawOutline: aDrawOutline
+ drawOutline: aDrawOutline,
+ linksOnly: aLinksOnly
});
},
/**
* Repeat the previous search. Should only be called after a previous
* call to Finder.fastFind.
*
* @param aFindBackwards Controls the search direction:
@@ -165,18 +181,19 @@ Finder.prototype = {
*/
findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
this._lastFindResult = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
let searchString = this._fastFind.searchString;
this._notify({
searchString,
result: this._lastFindResult,
findBackwards: aFindBackwards,
- fidnAgain: true,
- drawOutline: aDrawOutline
+ findAgain: true,
+ drawOutline: aDrawOutline,
+ linksOnly: aLinksOnly
});
},
/**
* Forcibly set the search string of the find clipboard to the currently
* selected text in the window, on supported platforms (i.e. OSX).
*/
setSearchStringToSelection: function() {
@@ -185,20 +202,18 @@ Finder.prototype = {
// Empty strings are rather useless to search for.
if (!searchString.length)
return null;
this.clipboardSearchString = searchString;
return searchString;
},
- highlight: Task.async(function* (aHighlight, aWord) {
- this.highlighter.maybeAbort();
-
- let found = yield this.highlighter.highlight(aHighlight, aWord, null);
+ highlight: Task.async(function* (aHighlight, aWord, aLinksOnly) {
+ let found = yield this.highlighter.highlight(aHighlight, aWord, null, aLinksOnly);
this.highlighter.notifyFinished(aHighlight);
if (aHighlight) {
let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
: Ci.nsITypeAheadFind.FIND_NOTFOUND;
this._notify({
searchString: aWord,
result,
findBackwards: false,
@@ -356,189 +371,58 @@ Finder.prototype = {
try {
l.onMatchesCountResult(result);
} catch (ex) {}
}
},
requestMatchesCount: function(aWord, aMatchLimit, aLinksOnly) {
if (this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
- this.searchString == "") {
- return this._notifyMatchesCount({
+ this.searchString == "" || !aWord) {
+ this._notifyMatchesCount({
total: 0,
current: 0
});
- }
- let window = this._getWindow();
- let result = this._countMatchesInWindow(aWord, aMatchLimit, aLinksOnly, window);
-
- // Count matches in (i)frames AFTER searching through the main window.
- for (let frame of result._framesToCount) {
- // We've reached our limit; no need to do more work.
- if (result.total == -1 || result.total == aMatchLimit)
- break;
- this._countMatchesInWindow(aWord, aMatchLimit, aLinksOnly, frame, result);
+ return;
}
- // The `_currentFound` and `_framesToCount` properties are only used for
- // internal bookkeeping between recursive calls.
- delete result._currentFound;
- delete result._framesToCount;
-
- this._notifyMatchesCount(result);
- return undefined;
- },
-
- /**
- * Counts the number of matches for the searched word in the passed window's
- * content.
- * @param aWord
- * the word to search for.
- * @param aMatchLimit
- * the maximum number of matches shown (for speed reasons).
- * @param aLinksOnly
- * whether we should only search through links.
- * @param aWindow
- * the window to search in. Passing undefined will search the
- * current content window. Optional.
- * @param aStats
- * the Object that is returned by this function. It may be passed as an
- * argument here in the case of a recursive call.
- * @returns an object stating the number of matches and a vector for the current match.
- */
- _countMatchesInWindow: function(aWord, aMatchLimit, aLinksOnly, aWindow = null, aStats = null) {
- aWindow = aWindow || this._getWindow();
- aStats = aStats || {
+ let window = this._getWindow();
+ let result = {
total: 0,
current: 0,
- _framesToCount: new Set(),
_currentFound: false
};
-
- // If we already reached our max, there's no need to do more work!
- if (aStats.total == -1 || aStats.total == aMatchLimit) {
- aStats.total = -1;
- return aStats;
- }
-
- this._collectFrames(aWindow, aStats);
-
let foundRange = this._fastFind.getFoundRange();
- for(let range of this._findIterator(aWord, aWindow)) {
- if (!aLinksOnly || this._rangeStartsInLink(range)) {
- ++aStats.total;
- if (!aStats._currentFound) {
- ++aStats.current;
- aStats._currentFound = (foundRange &&
+ this.iterator.start({
+ finder: this,
+ limit: aMatchLimit,
+ linksOnly: aLinksOnly,
+ onRange: range => {
+ ++result.total;
+ if (!result._currentFound) {
+ ++result.current;
+ result._currentFound = (foundRange &&
range.startContainer == foundRange.startContainer &&
range.startOffset == foundRange.startOffset &&
range.endContainer == foundRange.endContainer &&
range.endOffset == foundRange.endOffset);
}
- }
- if (aStats.total == aMatchLimit) {
- aStats.total = -1;
- break;
- }
- }
-
- return aStats;
- },
-
- /**
- * Basic wrapper around nsIFind that provides a generator yielding
- * a range each time an occurence of `aWord` string is found.
- *
- * @param aWord
- * the word to search for.
- * @param aWindow
- * the window to search in.
- */
- _findIterator: function* (aWord, aWindow) {
- let doc = aWindow.document;
- let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
- doc.body : doc.documentElement;
-
- if (!body)
- return;
-
- let searchRange = doc.createRange();
- searchRange.selectNodeContents(body);
-
- let startPt = searchRange.cloneRange();
- startPt.collapse(true);
-
- let endPt = searchRange.cloneRange();
- endPt.collapse(false);
-
- let retRange = null;
-
- let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
- .createInstance()
- .QueryInterface(Ci.nsIFind);
- finder.caseSensitive = this._fastFind.caseSensitive;
- finder.entireWord = this._fastFind.entireWord;
+ },
+ useCache: true,
+ word: aWord
+ }).then(() => {
+ // The `_currentFound` property is only used for internal bookkeeping.
+ delete result._currentFound;
- while ((retRange = finder.Find(aWord, searchRange, startPt, endPt))) {
- yield retRange;
- startPt = retRange.cloneRange();
- startPt.collapse(false);
- }
- },
+ if (result.total == aMatchLimit)
+ result.total = -1;
- /**
- * Helper method for `_countMatchesInWindow` that recursively collects all
- * visible (i)frames inside a window.
- *
- * @param aWindow
- * the window to extract the (i)frames from.
- * @param aStats
- * Object that contains a Set called '_framesToCount'
- */
- _collectFrames: function(aWindow, aStats) {
- if (!aWindow.frames || !aWindow.frames.length)
- return;
- // Casting `aWindow.frames` to an Iterator doesn't work, so we're stuck with
- // a plain, old for-loop.
- for (let i = 0, l = aWindow.frames.length; i < l; ++i) {
- let frame = aWindow.frames[i];
- // Don't count matches in hidden frames.
- let frameEl = frame && frame.frameElement;
- if (!frameEl)
- continue;
- // Construct a range around the frame element to check its visiblity.
- let range = aWindow.document.createRange();
- range.setStart(frameEl, 0);
- range.setEnd(frameEl, 0);
- if (!this._fastFind.isRangeVisible(range, this._getDocShell(range), true))
- continue;
- // All good, so add it to the set to count later.
- if (!aStats._framesToCount.has(frame))
- aStats._framesToCount.add(frame);
- this._collectFrames(frame, aStats);
- }
- },
-
- /**
- * Helper method to extract the docShell reference from a Window or Range object.
- *
- * @param aWindowOrRange
- * Window object to query. May also be a Range, from which the owner
- * window will be queried.
- * @returns nsIDocShell
- */
- _getDocShell: function(aWindowOrRange) {
- let window = aWindowOrRange;
- // Ranges may also be passed in, so fetch its window.
- if (aWindowOrRange instanceof Ci.nsIDOMRange)
- window = aWindowOrRange.startContainer.ownerDocument.defaultView;
- return window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShell);
+ this._notifyMatchesCount(result);
+ });
},
_getWindow: function () {
return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
},
/**
* Get the bounding selection rect in CSS px relative to the origin of the
@@ -644,61 +528,26 @@ Finder.prototype = {
.QueryInterface(Ci.nsIDocShell);
let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
return controller;
},
- /**
- * Determines whether a range is inside a link.
- * @param aRange
- * the range to check
- * @returns true if the range starts in a link
- */
- _rangeStartsInLink: function(aRange) {
- let isInsideLink = false;
- let node = aRange.startContainer;
-
- if (node.nodeType == node.ELEMENT_NODE) {
- if (node.hasChildNodes) {
- let childNode = node.item(aRange.startOffset);
- if (childNode)
- node = childNode;
- }
- }
-
- const XLink_NS = "http://www.w3.org/1999/xlink";
- const HTMLAnchorElement = (node.ownerDocument || node).defaultView.HTMLAnchorElement;
- do {
- if (node instanceof HTMLAnchorElement) {
- isInsideLink = node.hasAttribute("href");
- break;
- } else if (typeof node.hasAttributeNS == "function" &&
- node.hasAttributeNS(XLink_NS, "href")) {
- isInsideLink = (node.getAttributeNS(XLink_NS, "type") == "simple");
- break;
- }
-
- node = node.parentNode;
- } while (node);
-
- return isInsideLink;
- },
-
// Start of nsIWebProgressListener implementation.
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
if (!aWebProgress.isTopLevel)
return;
// Avoid leaking if we change the page.
this._previousLink = null;
this.highlighter.onLocationChange();
+ this.iterator.reset();
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};
function GetClipboardSearchString(aLoadContext) {
let searchString = "";
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -9,17 +9,16 @@ this.EXPORTED_SYMBOLS = ["FinderHighligh
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
-const kHighlightIterationSizeMax = 100;
const kModalHighlightRepaintFreqMs = 10;
const kModalHighlightPref = "findbar.modalHighlight";
const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size",
"font-size-adjust", "font-stretch", "font-variant", "font-weight", "letter-spacing",
"text-emphasis", "text-orientation", "text-transform", "word-spacing"];
const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
let parts = prop.split("-");
return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -96,16 +95,23 @@ const kXULNS = "http://www.mozilla.org/k
* @param {Finder} finder Finder.jsm instance
*/
function FinderHighlighter(finder) {
this.finder = finder;
this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
}
FinderHighlighter.prototype = {
+ get iterator() {
+ if (this._iterator)
+ return this._iterator;
+ this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
+ return this._iterator;
+ },
+
get modalStyleSheet() {
if (!this._modalStyleSheet) {
this._modalStyleSheet = kModalStyle.replace(/(\.|#)findbar-/g,
"$1" + kModalIdPrefix + "-findbar-");
}
return this._modalStyleSheet;
},
@@ -126,83 +132,52 @@ FinderHighlighter.prototype = {
for (let l of this.finder._listeners) {
try {
l.onHighlightFinished(highlight);
} catch (ex) {}
}
},
/**
- * Whilst the iterator is running, it's possible to abort it. This may be useful
- * if the word to highlight was updated in the meantime.
- */
- maybeAbort() {
- this.clear();
- if (!this._abortHighlight) {
- return;
- }
- this._abortHighlight();
- },
-
- /**
- * Uses the iterator in Finder.jsm to find all the words to highlight and makes
- * sure not to block the thread whilst running.
- *
- * @param {String} word Needle to search for and highlight when found
- * @param {nsIDOMWindow} window Window object, whose DOM tree should be traversed
- * @param {Function} onFind Callback invoked for each found occurrence
- * @yield {Promise} that resolves once the iterator has finished
- */
- iterator: Task.async(function* (word, window, onFind) {
- let count = 0;
- for (let range of this.finder._findIterator(word, window)) {
- onFind(range);
- if (++count >= kHighlightIterationSizeMax) {
- count = 0;
- // Sleep for the rest of this cycle.
- yield new Promise(resolve => resolve());
- }
- }
- }),
-
- /**
* Toggle highlighting all occurrences of a word in a page. This method will
* be called recursively for each (i)frame inside a page.
*
- * @param {Booolean} highlight Whether highlighting should be turned on
- * @param {String} word Needle to search for and highlight when found
- * @param {nsIDOMWindow} window Window object, whose DOM tree should be traversed
- * @yield {Promise} that resolves once the operation has finished
+ * @param {Booolean} highlight Whether highlighting should be turned on
+ * @param {String} word Needle to search for and highlight when found
+ * @param {Boolean} linksOnly Only consider nodes that are links for the search
+ * @yield {Promise} that resolves once the operation has finished
*/
- highlight: Task.async(function* (highlight, word, window) {
- let finderWindow = this.finder._getWindow();
- window = window || finderWindow;
- let found = false;
- for (let i = 0; window.frames && i < window.frames.length; i++) {
- if (yield this.highlight(highlight, word, window.frames[i])) {
- found = true;
- }
- }
-
+ highlight: Task.async(function* (highlight, word, linksOnly) {
+ let window = this.finder._getWindow();
let controller = this.finder._getSelectionController(window);
let doc = window.document;
+ let found = false;
+
+ this.clear();
+
if (!controller || !doc || !doc.documentElement) {
// Without the selection controller,
// we are unable to (un)highlight any matches
return found;
}
if (highlight) {
- yield this.iterator(word, window, range => {
- this.highlightRange(range, controller, finderWindow);
- found = true;
+ yield this.iterator.start({
+ linksOnly, word,
+ finder: this.finder,
+ onRange: range => {
+ this.highlightRange(range, controller, window);
+ found = true;
+ },
+ useCache: true
});
} else {
this.hide(window);
this.clear();
+ this.iterator.reset();
// Removing the highlighting always succeeds, so return true.
found = true;
}
return found;
}),
@@ -566,17 +541,16 @@ FinderHighlighter.prototype = {
for (let dims of range.getClientRects()) {
rects.add({
height: dims.bottom - dims.top,
width: dims.right - dims.left,
y: dims.top + scrollY,
x: dims.left + scrollX
});
}
- range.collapse();
if (!this._modalHighlightRectsMap)
this._modalHighlightRectsMap = new Map();
this._modalHighlightRectsMap.set(range, rects);
this.show(window);
// We don't repaint the mask right away, but pass it off to a render loop of
// sorts.
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/FinderIterator.jsm
@@ -0,0 +1,489 @@
+/* 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 = ["FinderIterator"];
+
+const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+const kIterationSizeMax = 100;
+
+/**
+ * FinderIterator singleton. See the documentation for the `start()` method to
+ * learn more.
+ */
+this.FinderIterator = {
+ _currentParams: null,
+ _listeners: new Map(),
+ _catchingUp: new Set(),
+ _previousParams: null,
+ _previousRanges: [],
+ _spawnId: 0,
+ ranges: [],
+ running: false,
+
+ /**
+ * Start iterating the active Finder docShell, using the options below. When
+ * it already started at the request of another consumer, we first yield the
+ * results we already collected before continuing onward to yield fresh results.
+ * We make sure to pause every `kIterationSizeMax` iterations to make sure we
+ * don't block the host process too long. In the case of a break like this, we
+ * yield `undefined`, instead of a range.
+ * Upon re-entrance after a break, we check if `stop()` was called during the
+ * break and if so, we stop iterating.
+ * Results are also passed to the `onRange` callback method, along with a flag
+ * that specifies if the result comes from the cache or is fresh. The callback
+ * also adheres to the `limit` flag.
+ * The returned promise is resolved when 1) the limit is reached, 2) when all
+ * the ranges have been found or 3) when `stop()` is called whilst iterating.
+ *
+ * @param {Finder} options.finder Currently active Finder instance
+ * @param {Number} [options.limit] Limit the amount of results to be
+ * passed back. Optional, defaults to no
+ * limit.
+ * @param {Boolean} [options.linksOnly] Only yield ranges that are inside a
+ * hyperlink (used by QuickFind).
+ * Optional, defaults to `false`.
+ * @param {Function} options.onRange Callback invoked when a range is found
+ * @param {Boolean} [options.useCache] Whether to allow results already
+ * present in the cache or demand fresh.
+ * Optional, defaults to `false`.
+ * @param {String} options.word Word to search for
+ * @return {Promise}
+ */
+ start({ finder, limit, linksOnly, onRange, useCache, word }) {
+ // Take care of default values for non-required options.
+ if (typeof limit != "number")
+ limit = -1;
+ if (typeof linksOnly != "boolean")
+ linksOnly = false;
+ if (typeof useCache != "boolean")
+ useCache = false;
+
+ // Validate the options.
+ if (!finder)
+ throw new Error("Missing required option 'finder'");
+ if (!word)
+ throw new Error("Missing required option 'word'");
+ if (typeof onRange != "function")
+ throw new TypeError("Missing valid, required option 'onRange'");
+
+ // Don't add the same listener twice.
+ if (this._listeners.has(onRange))
+ throw new Error("Already listening to iterator results");
+
+ let window = finder._getWindow();
+ let resolver;
+ let promise = new Promise(resolve => resolver = resolve);
+ let iterParams = { linksOnly, useCache, word };
+
+ this._listeners.set(onRange, { limit, onEnd: resolver });
+
+ // If we're not running anymore and we're requesting the previous result, use it.
+ if (!this.running && this._previousResultAvailable(iterParams)) {
+ this._yieldPreviousResult(onRange, window);
+ return promise;
+ }
+
+ if (this.running) {
+ // Double-check if we're not running the iterator with a different set of
+ // parameters, otherwise throw an error with the most common reason.
+ if (!this._areParamsEqual(this._currentParams, iterParams))
+ throw new Error(`We're currently iterating over '${this._currentParams.word}', not '${word}'`);
+
+ // if we're still running, yield the set we have built up this far.
+ this._yieldIntermediateResult(onRange, window);
+
+ return promise;
+ }
+
+ // Start!
+ this.running = true;
+ this._currentParams = iterParams;
+ this._findAllRanges(finder, window, ++this._spawnId);
+
+ return promise;
+ },
+
+ /**
+ * Stop the currently running iterator as soon as possible and optionally cache
+ * the result for later.
+ *
+ * @param {Boolean} [cachePrevious] Whether to save the result for later.
+ * Optional.
+ */
+ stop(cachePrevious = false) {
+ if (!this.running)
+ return;
+
+ if (cachePrevious) {
+ this._previousRanges = [].concat(this.ranges);
+ this._previousParams = Object.assign({}, this._currentParams);
+ } else {
+ this._previousRanges = [];
+ this._previousParams = null;
+ }
+
+ this._catchingUp.clear();
+ this._currentParams = null;
+ this.ranges = [];
+ this.running = false;
+
+ for (let [, { onEnd }] of this._listeners)
+ onEnd();
+ this._listeners.clear();
+ },
+
+ /**
+ * Reset the internal state of the iterator. Typically this would be called
+ * when the docShell is not active anymore, which makes the current and cached
+ * previous result invalid.
+ * If the iterator is running, it will be stopped as soon as possible.
+ */
+ reset() {
+ this._catchingUp.clear();
+ this._currentParams = this._previousParams = null;
+ this._previousRanges = [];
+ this.ranges = [];
+ this.running = false;
+
+ for (let [, { onEnd }] of this._listeners)
+ onEnd();
+ this._listeners.clear();
+ },
+
+ /**
+ * Check if the currently running iterator parameters are the same as the ones
+ * passed through the arguments. When `true`, we can keep it running as-is and
+ * the consumer should stop the iterator when `false`.
+ *
+ * @param {Boolean} options.linksOnly Whether to search for the word to be
+ * present in links only
+ * @param {String} options.word The word being searched for
+ * @return {Boolean}
+ */
+ continueRunning({ linksOnly, word }) {
+ return (this.running &&
+ this._currentParams.linksOnly === linksOnly &&
+ this._currentParams.word == word);
+ },
+
+ /**
+ * Internal; check if an iteration request is available in the previous result
+ * that we cached.
+ *
+ * @param {Boolean} options.linksOnly Whether to search for the word to be
+ * present in links only
+ * @param {Boolean} options.useCache Whether the consumer wants to use the
+ * cached previous result at all
+ * @param {String} options.word The word being searched for
+ * @return {Boolean}
+ */
+ _previousResultAvailable({ linksOnly, useCache, word }) {
+ return !!(useCache &&
+ this._areParamsEqual(this._previousParams, { word, linksOnly }) &&
+ this._previousRanges.length);
+ },
+
+ /**
+ * Internal; compare if two sets of iterator parameters are equivalent.
+ *
+ * @param {Object} paramSet1 First set of params (left hand side)
+ * @param {Object} paramSet2 Second set of params (right hand side)
+ * @return {Boolean}
+ */
+ _areParamsEqual(paramSet1, paramSet2) {
+ return (!!paramSet1 && !!paramSet2 &&
+ paramSet1.linksOnly === paramSet2.linksOnly &&
+ paramSet1.word == paramSet2.word);
+ },
+
+ /**
+ * Internal; iterate over a predefined set of ranges that have been collected
+ * before.
+ * Also here, we make sure to pause every `kIterationSizeMax` iterations to
+ * make sure we don't block the host process too long. In the case of a break
+ * like this, we yield `undefined`, instead of a range.
+ *
+ * @param {Function} onRange Callback invoked when a range is found
+ * @param {Array} rangeSource Set of ranges to iterate over
+ * @param {nsIDOMWindow} window The window object is only really used
+ * for access to `setTimeout`
+ * @yield {nsIDOMRange}
+ */
+ _yieldResult: function* (onRange, rangeSource, window) {
+ // We keep track of the number of iterations to allow a short pause between
+ // every `kIterationSizeMax` number of iterations.
+ let iterCount = 0;
+ let { limit, onEnd } = this._listeners.get(onRange);
+ let ranges = rangeSource.slice(0, limit > -1 ? limit : undefined);
+ for (let range of ranges) {
+ try {
+ range.startContainer;
+ } catch (ex) {
+ // Don't yield dead objects, so use the escape hatch.
+ if (ex.message.includes("dead object"))
+ return;
+ }
+
+ // Pass a flag that is `true` when we're returning the result from a
+ // cached previous iteration.
+ onRange(range, !this.running);
+ yield range;
+
+ if (++iterCount >= kIterationSizeMax) {
+ iterCount = 0;
+ // Make sure to save the current limit for later.
+ this._listeners.set(onRange, { limit, onEnd });
+ // Sleep for the rest of this cycle.
+ yield new Promise(resolve => window.setTimeout(resolve, 0));
+ // After a sleep, the set of ranges may have updated.
+ ranges = rangeSource.slice(0, limit > -1 ? limit : undefined);
+ }
+
+ if (limit !== -1 && --limit === 0) {
+ // We've reached our limit; no need to do more work.
+ this._listeners.delete(onRange);
+ onEnd();
+ return;
+ }
+ }
+
+ // Save the updated limit globally.
+ this._listeners.set(onRange, { limit, onEnd });
+ },
+
+ /**
+ * Internal; iterate over the set of previously found ranges. Meanwhile it'll
+ * mark the listener as 'catching up', meaning it will not receive fresh
+ * results from a running iterator.
+ *
+ * @param {Function} onRange Callback invoked when a range is found
+ * @param {nsIDOMWindow} window The window object is only really used
+ * for access to `setTimeout`
+ * @yield {nsIDOMRange}
+ */
+ _yieldPreviousResult: Task.async(function* (onRange, window) {
+ this._catchingUp.add(onRange);
+ yield* this._yieldResult(onRange, this._previousRanges, window);
+ this._catchingUp.delete(onRange);
+ let { onEnd } = this._listeners.get(onRange);
+ if (onEnd) {
+ onEnd();
+ this._listeners.delete(onRange);
+ }
+ }),
+
+ /**
+ * Internal; iterate over the set of already found ranges. Meanwhile it'll
+ * mark the listener as 'catching up', meaning it will not receive fresh
+ * results from the running iterator.
+ *
+ * @param {Function} onRange Callback invoked when a range is found
+ * @param {nsIDOMWindow} window The window object is only really used
+ * for access to `setTimeout`
+ * @yield {nsIDOMRange}
+ */
+ _yieldIntermediateResult: Task.async(function* (onRange, window) {
+ this._catchingUp.add(onRange);
+ yield* this._yieldResult(onRange, this.ranges, window);
+ this._catchingUp.delete(onRange);
+ }),
+
+ /**
+ * Internal; see the documentation of the start() method above.
+ *
+ * @param {Finder} finder Currently active Finder instance
+ * @param {nsIDOMWindow} window The window to search in
+ * @param {Number} spawnId Since `stop()` is synchronous and this method
+ * is not, this identifier is used to learn if
+ * it's supposed to still continue after a pause.
+ * @yield {nsIDOMRange}
+ */
+ _findAllRanges: Task.async(function* (finder, window, spawnId) {
+ // First we collect all frames we need to search through, whilst making sure
+ // that the parent window gets dibs.
+ let frames = [window].concat(this._collectFrames(window, finder));
+ let { linksOnly, word } = this._currentParams;
+ let iterCount = 0;
+ for (let frame of frames) {
+ for (let range of this._iterateDocument(word, frame, finder)) {
+ // Between iterations, for example after a sleep of one cycle, we could
+ // have gotten the signal to stop iterating. Make sure we do here.
+ if (!this.running || spawnId !== this._spawnId)
+ return;
+
+ // Deal with links-only mode here.
+ if (linksOnly && this._rangeStartsInLink(range))
+ continue;
+
+ this.ranges.push(range);
+
+ // Call each listener with the range we just found.
+ for (let [onRange, { limit, onEnd }] of this._listeners) {
+ if (this._catchingUp.has(onRange))
+ continue;
+
+ onRange(range);
+
+ if (limit !== -1 && --limit === 0) {
+ // We've reached our limit; no need to do more work for this listener.
+ this._listeners.delete(onRange);
+ onEnd();
+ continue;
+ }
+
+ // Save the updated limit globally.
+ this._listeners.set(onRange, { limit, onEnd });
+ }
+
+ yield range;
+
+ if (++iterCount >= kIterationSizeMax) {
+ iterCount = 0;
+ // Sleep for the rest of this cycle.
+ yield new Promise(resolve => window.setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ // When the iterating has finished, make sure we reset and save the state
+ // properly.
+ this.stop(true);
+ }),
+
+ /**
+ * Internal; basic wrapper around nsIFind that provides a generator yielding
+ * a range each time an occurence of `word` string is found.
+ *
+ * @param {String} word The word to search for
+ * @param {nsIDOMWindow} window The window to search in
+ * @param {Finder} finder The Finder instance
+ * @yield {nsIDOMRange}
+ */
+ _iterateDocument: function* (word, window, finder) {
+ let doc = window.document;
+ let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
+ doc.body : doc.documentElement;
+
+ if (!body)
+ return;
+
+ let searchRange = doc.createRange();
+ searchRange.selectNodeContents(body);
+
+ let startPt = searchRange.cloneRange();
+ startPt.collapse(true);
+
+ let endPt = searchRange.cloneRange();
+ endPt.collapse(false);
+
+ let retRange = null;
+
+ let nsIFind = Cc["@mozilla.org/embedcomp/rangefind;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsIFind);
+ nsIFind.caseSensitive = finder._fastFind.caseSensitive;
+ nsIFind.entireWord = finder._fastFind.entireWord;
+
+ while ((retRange = nsIFind.Find(word, searchRange, startPt, endPt))) {
+ yield retRange;
+ startPt = retRange.cloneRange();
+ startPt.collapse(false);
+ }
+ },
+
+ /**
+ * Internal; helper method for the iterator that recursively collects all
+ * visible (i)frames inside a window.
+ *
+ * @param {nsIDOMWindow} window The window to extract the (i)frames from
+ * @param {Finder} finder The Finder instance
+ * @return {Array} Stack of frames to iterate over
+ */
+ _collectFrames(window, finder) {
+ let frames = [];
+ if (!window.frames || !window.frames.length)
+ return frames;
+
+ // Casting `window.frames` to an Iterator doesn't work, so we're stuck with
+ // a plain, old for-loop.
+ for (let i = 0, l = window.frames.length; i < l; ++i) {
+ let frame = window.frames[i];
+ // Don't count matches in hidden frames.
+ let frameEl = frame && frame.frameElement;
+ if (!frameEl)
+ continue;
+ // Construct a range around the frame element to check its visiblity.
+ let range = window.document.createRange();
+ range.setStart(frameEl, 0);
+ range.setEnd(frameEl, 0);
+ if (!finder._fastFind.isRangeVisible(range, this._getDocShell(range), true))
+ continue;
+ // All conditions pass, so push the current frame and its children on the
+ // stack.
+ frames.push(frame, ...this._collectFrames(frame, finder));
+ }
+
+ return frames;
+ },
+
+ /**
+ * Internal; helper method to extract the docShell reference from a Window or
+ * Range object.
+ *
+ * @param {nsIDOMRange} windowOrRange Window object to query. May also be a
+ * Range, from which the owner window will
+ * be queried.
+ * @return {nsIDocShell}
+ */
+ _getDocShell(windowOrRange) {
+ let window = windowOrRange;
+ // Ranges may also be passed in, so fetch its window.
+ if (windowOrRange instanceof Ci.nsIDOMRange)
+ window = windowOrRange.startContainer.ownerDocument.defaultView;
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ },
+
+ /**
+ * Internal; determines whether a range is inside a link.
+ *
+ * @param {nsIDOMRange} range the range to check
+ * @return {Boolean} True if the range starts in a link
+ */
+ _rangeStartsInLink(range) {
+ let isInsideLink = false;
+ let node = range.startContainer;
+
+ if (node.nodeType == node.ELEMENT_NODE) {
+ if (node.hasChildNodes) {
+ let childNode = node.item(range.startOffset);
+ if (childNode)
+ node = childNode;
+ }
+ }
+
+ const XLink_NS = "http://www.w3.org/1999/xlink";
+ const HTMLAnchorElement = (node.ownerDocument || node).defaultView.HTMLAnchorElement;
+ do {
+ if (node instanceof HTMLAnchorElement) {
+ isInsideLink = node.hasAttribute("href");
+ break;
+ } else if (typeof node.hasAttributeNS == "function" &&
+ node.hasAttributeNS(XLink_NS, "href")) {
+ isInsideLink = (node.getAttributeNS(XLink_NS, "type") == "simple");
+ break;
+ }
+
+ node = node.parentNode;
+ } while (node);
+
+ return isInsideLink;
+ }
+};
--- a/toolkit/modules/RemoteFinder.jsm
+++ b/toolkit/modules/RemoteFinder.jsm
@@ -145,19 +145,20 @@ RemoteFinder.prototype = {
findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
this._browser.messageManager.sendAsyncMessage("Finder:FindAgain",
{ findBackwards: aFindBackwards,
linksOnly: aLinksOnly,
drawOutline: aDrawOutline });
},
- highlight: function (aHighlight, aWord) {
+ highlight: function (aHighlight, aWord, aLinksOnly) {
this._browser.messageManager.sendAsyncMessage("Finder:Highlight",
{ highlight: aHighlight,
+ linksOnly: aLinksOnly,
word: aWord });
},
enableSelection: function () {
this._browser.messageManager.sendAsyncMessage("Finder:EnableSelection");
},
removeSelection: function () {
@@ -294,17 +295,17 @@ RemoteFinderListener.prototype = {
this._finder.fastFind(data.searchString, data.linksOnly, data.drawOutline);
break;
case "Finder:FindAgain":
this._finder.findAgain(data.findBackwards, data.linksOnly, data.drawOutline);
break;
case "Finder:Highlight":
- this._finder.highlight(data.highlight, data.word);
+ this._finder.highlight(data.highlight, data.word, data.linksOnly);
break;
case "Finder:HighlightAllChange":
this._finder.onHighlightAllChange(data.highlightAll);
break;
case "Finder:EnableSelection":
this._finder.enableSelection();
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -35,16 +35,17 @@ EXTRA_JS_MODULES += [
'Color.jsm',
'Console.jsm',
'debug.js',
'DeferredTask.jsm',
'Deprecated.jsm',
'FileUtils.jsm',
'Finder.jsm',
'FinderHighlighter.jsm',
+ 'FinderIterator.jsm',
'Geometry.jsm',
'GMPInstallManager.jsm',
'GMPUtils.jsm',
'Http.jsm',
'InlineSpellChecker.jsm',
'InlineSpellCheckerContent.jsm',
'Integration.jsm',
'LoadContextInfo.jsm',