Bug 1294392 - consolidate the highlight and counter timers into one iterator timer. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Fri, 19 Aug 2016 17:31:31 +0200
changeset 403350 1a407b24a9803a2b8d0f115094b2a713a1bcce69
parent 403349 4d92551f854b80764c7b010c76043920c28c8e35
child 528883 ead833f00f06b4534430b11af8afa4ca9f892052
push id26887
push usermdeboer@mozilla.com
push dateFri, 19 Aug 2016 15:39:18 +0000
reviewersjaws
bugs1294392
milestone51.0a1
Bug 1294392 - consolidate the highlight and counter timers into one iterator timer. r?jaws MozReview-Commit-ID: 9HsKjRSx5KV
modules/libpref/init/all.js
toolkit/content/widgets/findbar.xml
toolkit/modules/Finder.jsm
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/FinderIterator.jsm
toolkit/modules/tests/xpcshell/test_FinderIterator.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -858,21 +858,21 @@ pref("accessibility.typeaheadfind.timeou
 pref("accessibility.typeaheadfind.enabletimeout", true);
 pref("accessibility.typeaheadfind.soundURL", "beep");
 pref("accessibility.typeaheadfind.enablesound", true);
 #ifdef XP_MACOSX
 pref("accessibility.typeaheadfind.prefillwithselection", false);
 #else
 pref("accessibility.typeaheadfind.prefillwithselection", true);
 #endif
-pref("accessibility.typeaheadfind.matchesCountTimeout", 100);
 pref("accessibility.typeaheadfind.matchesCountLimit", 1000);
 pref("findbar.highlightAll", false);
 pref("findbar.modalHighlight", false);
 pref("findbar.entireword", false);
+pref("findbar.iteratorTimeout", 100);
 
 // use Mac OS X Appearance panel text smoothing setting when rendering text, disabled by default
 pref("gfx.use_text_smoothing_setting", false);
 
 // Number of characters to consider emphasizing for rich autocomplete results
 pref("toolkit.autocomplete.richBoundaryCutoff", 200);
 
 // Variable controlling logging for osfile.
--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -369,18 +369,16 @@
         this._foundURL = null;
 
         let prefsvc = this._prefsvc;
 
         this._quickFindTimeoutLength =
           prefsvc.getIntPref("accessibility.typeaheadfind.timeout");
         this._flashFindBar =
           prefsvc.getIntPref("accessibility.typeaheadfind.flashBar");
-        this._matchesCountTimeoutLength =
-          prefsvc.getIntPref("accessibility.typeaheadfind.matchesCountTimeout");
         this._matchesCountLimit =
           prefsvc.getIntPref("accessibility.typeaheadfind.matchesCountLimit");
         this._useModalHighlight = prefsvc.getBoolPref("findbar.modalHighlight");
 
         prefsvc.addObserver("accessibility.typeaheadfind",
                             this._observer, false);
         prefsvc.addObserver("accessibility.typeaheadfind.linksonly",
                             this._observer, false);
@@ -451,28 +449,20 @@
           if (this._flashFindBarTimeout) {
             clearInterval(this._flashFindBarTimeout);
             this._flashFindBarTimeout = null;
           }
           if (this._quickFindTimeout) {
             clearTimeout(this._quickFindTimeout);
             this._quickFindTimeout = null;
           }
-          if (this._highlightTimeout) {
-            clearTimeout(this._highlightTimeout);
-            this._highlightTimeout = null;
-          }
           if (this._findResetTimeout) {
             clearTimeout(this._findResetTimeout);
             this._findResetTimeout = null;
           }
-          if (this._updateMatchesCountTimeout) {
-            clearTimeout(this._updateMatchesCountTimeout);
-            this._updateMatchesCountTimeout = null;
-          }
         ]]></body>
       </method>
 
       <method name="_setFindCloseTimeout">
         <body><![CDATA[
           if (this._quickFindTimeout)
             clearTimeout(this._quickFindTimeout);
 
@@ -507,24 +497,18 @@
         - @param aRes
         -        the result of the find operation
         -->
       <method name="_updateMatchesCount">
         <body><![CDATA[
           if (this._matchesCountLimit == 0 || !this._dispatchFindEvent("matchescount"))
             return;
 
-          if (this._updateMatchesCountTimeout) {
-            window.clearTimeout(this._updateMatchesCountTimeout);
-          }
-          this._updateMatchesCountTimeout =
-            window.setTimeout(() => {
-              this.browser.finder.requestMatchesCount(this._findField.value, this._matchesCountLimit,
-                  this._findMode == this.FIND_LINKS);
-            }, this._matchesCountTimeoutLength);
+            this.browser.finder.requestMatchesCount(this._findField.value,
+              this._matchesCountLimit, this._findMode == this.FIND_LINKS);
         ]]></body>
       </method>
 
       <!--
         - Turns highlight on or off.
         - @param aHighlight (boolean)
         -        Whether to turn the highlight on or off
         - @param aFromPrefObserver (boolean)
@@ -580,30 +564,25 @@
             this._prefsvc.setBoolPref("findbar.highlightAll", aHighlight);
           }
           this._highlightAll = aHighlight;
           let checkbox = this.getElement("highlight");
           checkbox.checked = this._highlightAll;
         ]]></body>
       </method>
 
-      <method name="_setHighlightTimeout">
+      <method name="_maybeHighlightAll">
         <body><![CDATA[
-          if (this._highlightTimeout)
-            clearTimeout(this._highlightTimeout);
-
           let word = this._findField.value;
           // Bug 429723. Don't attempt to highlight ""
           if (!this._highlightAll || !word)
             return;
 
-          this._highlightTimeout = setTimeout(() => {
-            this.browser.finder.highlight(true, word,
-              this._findMode == this.FIND_LINKS);
-          }, 500);
+          this.browser.finder.highlight(true, word,
+            this._findMode == this.FIND_LINKS);
         ]]></body>
       </method>
 
       <!--
         - Updates the case-sensitivity mode of the findbar and its UI.
         - @param [optional] aString
         -        The string for which case sensitivity might be turned on.
         -        This only used when case-sensitivity is in auto mode,
@@ -644,18 +623,17 @@
         -->
       <method name="_setCaseSensitivity">
         <parameter name="aCaseSensitivity"/>
         <body><![CDATA[
           this._typeAheadCaseSensitive = aCaseSensitivity;
           this._updateCaseSensitivity();
           this._findFailedString = null;
           this._find();
-          if (this.getElement("highlight").checked)
-            this._setHighlightTimeout();
+          this._maybeHighlightAll();
 
           this._dispatchFindEvent("casesensitivitychange");
         ]]></body>
       </method>
 
       <!--
         - Updates the entire-word mode of the findbar and its UI.
         -->
@@ -691,17 +669,17 @@
         <body><![CDATA[
           let prefsvc =
             Components.classes["@mozilla.org/preferences-service;1"]
                       .getService(Components.interfaces.nsIPrefBranch);
 
           // Just set the pref; our observer will change the find bar behavior.
           prefsvc.setBoolPref("findbar.entireword", aEntireWord);
 
-          this._setHighlightTimeout();
+          this._maybeHighlightAll();
         ]]></body>
       </method>
 
       <field name="_strBundle">null</field>
       <property name="strBundle">
         <getter><![CDATA[
           if (!this._strBundle) {
             this._strBundle =
@@ -1037,17 +1015,17 @@
             // Getting here means the user commanded a find op. Make sure any
             // initial prefilling is ignored if it hasn't happened yet.
             if (this._startFindDeferred) {
               this._startFindDeferred.resolve();
               this._startFindDeferred = null;
             }
 
             this._enableFindButtons(val);
-            this._setHighlightTimeout();
+            this._maybeHighlightAll();
 
             this._updateCaseSensitivity(val);
             this._updateEntireWord();
 
             this.browser.finder.fastFind(val, this._findMode == this.FIND_LINKS,
                                          this._findMode != this.FIND_NORMAL);
           }
 
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -221,28 +221,16 @@ Finder.prototype = {
 
     this.clipboardSearchString = searchString;
     return searchString;
   },
 
   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,
-        findAgain: false,
-        drawOutline: false,
-        storeResult: false
-      });
-    }
   }),
 
   getInitialSelection: function() {
     this._getWindow().setTimeout(() => {
       let initialSelection = this.getActiveSelectionText();
       for (let l of this._listeners) {
         try {
           l.onCurrentSelection(initialSelection, true);
@@ -385,18 +373,16 @@ Finder.prototype = {
         break;
       case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
         controller.scrollLine(true);
         break;
     }
   },
 
   _notifyMatchesCount: function(result = this._currentMatchesCountResult) {
-    if (!result)
-      return;
     // The `_currentFound` property is only used for internal bookkeeping.
     delete result._currentFound;
     if (result.total == this._currentMatchLimit)
       result.total = -1;
 
     for (let l of this._listeners) {
       try {
         l.onMatchesCountResult(result);
@@ -415,32 +401,32 @@ Finder.prototype = {
       });
       return;
     }
 
     let window = this._getWindow();
     this._currentFoundRange = this._fastFind.getFoundRange();
     this._currentMatchLimit = aMatchLimit;
 
-    this._currentMatchesCountResult = {
-      total: 0,
-      current: 0,
-      _currentFound: false
-    };
-
     this.iterator.start({
       caseSensitive: this._fastFind.caseSensitive,
       entireWord: this._fastFind.entireWord,
       finder: this,
       limit: aMatchLimit,
       linksOnly: aLinksOnly,
       listener: this,
       useCache: true,
       word: aWord
-    }).then(this._notifyMatchesCount.bind(this));
+    }).then(() => {
+      // Without a valid result, there's nothing to notify about. This happens
+      // when the iterator was started before and won the race.
+      if (!this._currentMatchesCountResult || !this._currentMatchesCountResult.total)
+        return;
+      this._notifyMatchesCount();
+    });
   },
 
   // FinderIterator listener implementation
 
   onIteratorRangeFound(range) {
     let result = this._currentMatchesCountResult;
     if (!result)
       return;
@@ -455,19 +441,25 @@ Finder.prototype = {
         range.endOffset == this._currentFoundRange.endOffset);
     }
   },
 
   onIteratorReset() {},
 
   onIteratorRestart({ word, linksOnly }) {
     this.requestMatchesCount(word, this._currentMatchLimit, linksOnly);
-   },
+  },
 
-   onIteratorStart() {},
+  onIteratorStart() {
+    this._currentMatchesCountResult = {
+      total: 0,
+      current: 0,
+      _currentFound: false
+    };
+  },
 
   _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
    * top-level content document.
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -14,17 +14,17 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
 XPCOMUtils.defineLazyGetter(this, "kDebug", () => {
   const kDebugPref = "findbar.modalHighlight.debug";
   return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref);
 });
 
 const kContentChangeThresholdPx = 5;
-const kModalHighlightRepaintFreqMs = 10;
+const kModalHighlightRepaintFreqMs = 100;
 const kHighlightAllPref = "findbar.highlightAll";
 const kModalHighlightPref = "findbar.modalHighlight";
 const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size",
   "font-size-adjust", "font-stretch", "font-variant", "font-weight", "line-height",
   "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("");
@@ -214,16 +214,17 @@ FinderHighlighter.prototype = {
     if (!controller || !doc || !doc.documentElement) {
       // Without the selection controller,
       // we are unable to (un)highlight any matches
       return this._found;
     }
 
     if (highlight) {
       let params = {
+        allowDistance: 1,
         caseSensitive: this.finder._fastFind.caseSensitive,
         entireWord: this.finder._fastFind.entireWord,
         linksOnly, word,
         finder: this.finder,
         listener: this,
         useCache: true
       };
       if (this.iterator._areParamsEqual(params, this._lastIteratorParams))
@@ -468,22 +469,24 @@ FinderHighlighter.prototype = {
    * everything when the user starts to find in page again.
    */
   onLocationChange() {
     this.clear();
 
     if (!this._modalHighlightOutline)
       return;
 
-    if (kDebug)
+    if (kDebug) {
       this._modalHighlightOutline.remove();
-    try {
-      this.finder._getWindow().document
-        .removeAnonymousContent(this._modalHighlightOutline);
-    } catch (ex) {}
+    } else {
+      try {
+        this.finder._getWindow().document
+            .removeAnonymousContent(this._modalHighlightOutline);
+      } catch (ex) {}
+    }
 
     this._modalHighlightOutline = null;
   },
 
   /**
    * When `kModalHighlightPref` pref changed during a session, this callback is
    * invoked. When modal highlighting is turned off, we hide the CanvasFrame
    * contents.
@@ -791,26 +794,29 @@ FinderHighlighter.prototype = {
   },
 
   /**
    * Safely remove the mask AnoymousContent node from the CanvasFrame.
    *
    * @param {nsIDOMWindow} window
    */
   _removeHighlightAllMask(window) {
-    if (this._modalHighlightAllMask) {
-      // If the current window isn't the one the content was inserted into, this
-      // will fail, but that's fine.
-      if (kDebug)
-        this._modalHighlightAllMask.remove();
+    if (!this._modalHighlightAllMask)
+      return;
+
+    // If the current window isn't the one the content was inserted into, this
+    // will fail, but that's fine.
+    if (kDebug) {
+      this._modalHighlightAllMask.remove();
+    } else {
       try {
         window.document.removeAnonymousContent(this._modalHighlightAllMask);
       } catch (ex) {}
-      this._modalHighlightAllMask = null;
     }
+    this._modalHighlightAllMask = null;
   },
 
   /**
    * Doing a full repaint each time a range is delivered by the highlight iterator
    * is way too costly, thus we pipe the frequency down to every
    * `kModalHighlightRepaintFreqMs` milliseconds.
    *
    * @param {nsIDOMWindow} window
--- a/toolkit/modules/FinderIterator.jsm
+++ b/toolkit/modules/FinderIterator.jsm
@@ -3,31 +3,37 @@
  * 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/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
+const kDebug = false;
 const kIterationSizeMax = 100;
+const kTimeoutPref = "findbar.iteratorTimeout";
 
 /**
  * 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,
+  _timeout: Services.prefs.getIntPref(kTimeoutPref),
+  _timer: null,
   ranges: [],
   running: false,
 
   // Expose `kIterationSizeMax` to the outside world for unit tests to use.
   get kIterationSizeMax() { return kIterationSizeMax },
 
   get params() {
     if (!this._currentParams && !this._previousParams)
@@ -45,40 +51,45 @@ this.FinderIterator = {
    * 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 `listener.onIteratorRangeFound` 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 {Boolean}  options.caseSensitive Whether to search in case sensitive
-   *                                         mode
-   * @param {Boolean}  options.entireWord    Whether to search in entire-word mode
-   * @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 {Object}   options.listener    Listener object that implements the
-   *                                       following callback functions:
-   *                                        - onIteratorRangeFound({nsIDOMRange} range);
-   *                                        - onIteratorReset();
-   *                                        - onIteratorRestart({Object} iterParams);
-   *                                        - onIteratorStart({Object} iterParams);
-   * @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
+   * @param {Number}  [options.allowDistance] Allowed edit distance between the
+   *                                          current word and `options.word`
+   *                                          when the iterator is already running
+   * @param {Boolean} options.caseSensitive   Whether to search in case sensitive
+   *                                          mode
+   * @param {Boolean} options.entireWord      Whether to search in entire-word mode
+   * @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 {Object}  options.listener        Listener object that implements the
+   *                                          following callback functions:
+   *                                           - onIteratorRangeFound({nsIDOMRange} range);
+   *                                           - onIteratorReset();
+   *                                           - onIteratorRestart({Object} iterParams);
+   *                                           - onIteratorStart({Object} iterParams);
+   * @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({ caseSensitive, entireWord, finder, limit, linksOnly, listener, useCache, word }) {
+  start({ allowDistance, caseSensitive, entireWord, finder, limit, linksOnly, listener, useCache, word }) {
     // Take care of default values for non-required options.
+    if (typeof allowDistance != "number")
+      allowDistance = 0;
     if (typeof limit != "number")
       limit = -1;
     if (typeof linksOnly != "boolean")
       linksOnly = false;
     if (typeof useCache != "boolean")
       useCache = false;
 
     // Validate the options.
@@ -111,19 +122,26 @@ this.FinderIterator = {
     // If we're not running anymore and we're requesting the previous result, use it.
     if (!this.running && this._previousResultAvailable(iterParams)) {
       this._yieldPreviousResult(listener, 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}'`);
+      // parameters, otherwise report an error with the most common reason.
+      if (!this._areParamsEqual(this._currentParams, iterParams, allowDistance)) {
+        if (kDebug) {
+          Cu.reportError(`We're currently iterating over '${this._currentParams.word}', not '${word}'\n` +
+            new Error().stack);
+        }
+        this._listeners.delete(listener);
+        resolver();
+        return promise;
+      }
 
       // if we're still running, yield the set we have built up this far.
       this._yieldIntermediateResult(listener, window);
 
       return promise;
     }
 
     // Start!
@@ -140,16 +158,21 @@ this.FinderIterator = {
    *
    * @param {Boolean} [cachePrevious] Whether to save the result for later.
    *                                  Optional.
    */
   stop(cachePrevious = false) {
     if (!this.running)
       return;
 
+    if (this._timer) {
+      clearTimeout(this._timer);
+      this._timer = null;
+    }
+
     if (cachePrevious) {
       this._previousRanges = [].concat(this.ranges);
       this._previousParams = Object.assign({}, this._currentParams);
     } else {
       this._previousRanges = [];
       this._previousParams = null;
     }
 
@@ -185,16 +208,21 @@ this.FinderIterator = {
 
   /**
    * 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() {
+    if (this._timer) {
+      clearTimeout(this._timer);
+      this._timer = null;
+    }
+
     this._catchingUp.clear();
     this._currentParams = this._previousParams = null;
     this._previousRanges = [];
     this.ranges = [];
     this.running = false;
 
     this._notifyListeners("reset");
     for (let [, { onEnd }] of this._listeners)
@@ -261,26 +289,29 @@ this.FinderIterator = {
     return !!(useCache &&
       this._areParamsEqual(this._previousParams, { caseSensitive, entireWord, linksOnly, word }) &&
       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)
+   * @param  {Object} paramSet1       First set of params (left hand side)
+   * @param  {Object} paramSet2       Second set of params (right hand side)
+   * @param  {Number} [allowDistance] Allowed edit distance between the two words.
+   *                                  Optional, defaults to '0', which means 'no
+   *                                  distance'.
    * @return {Boolean}
    */
-  _areParamsEqual(paramSet1, paramSet2) {
+  _areParamsEqual(paramSet1, paramSet2, allowDistance = 0) {
     return (!!paramSet1 && !!paramSet2 &&
       paramSet1.caseSensitive === paramSet2.caseSensitive &&
       paramSet1.entireWord === paramSet2.entireWord &&
       paramSet1.linksOnly === paramSet2.linksOnly &&
-      paramSet1.word == paramSet2.word);
+      this._distance(paramSet1.word, paramSet2.word) <= allowDistance);
   },
 
   /**
    * 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.
@@ -379,16 +410,27 @@ this.FinderIterator = {
    * @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) {
+    if (this._timeout) {
+      if (this._timer)
+        clearTimeout(this._timer);
+      yield new Promise(resolve => this._timer = setTimeout(resolve, this._timeout));
+      this._timer = null;
+      // During the timeout, we could have gotten the signal to stop iterating.
+      // Make sure we do here.
+      if (!this.running || spawnId !== this._spawnId)
+        return;
+    }
+
     this._notifyListeners("start", this.params);
 
     // 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) {
@@ -564,10 +606,70 @@ this.FinderIterator = {
         isInsideLink = (node.getAttributeNS(XLink_NS, "type") == "simple");
         break;
       }
 
       node = node.parentNode;
     } while (node);
 
     return isInsideLink;
+  },
+
+  /**
+   * Calculate the Levenshtein distance between two words.
+   * The implementation of this method was heavily inspired by
+   * http://locutus.io/php/strings/levenshtein/index.html
+   * License: MIT.
+   *
+   * @param  {String} word1   Word to compare against
+   * @param  {String} word2   Word that may be different
+   * @param  {Number} costIns The cost to insert a character
+   * @param  {Number} costRep The cost to replace a character
+   * @param  {Number} costDel The cost to delete a character
+   * @return {Number}
+   */
+  _distance(word1 = "", word2 = "", costIns = 1, costRep = 1, costDel = 1) {
+    if (word1 === word2)
+      return 0;
+
+    let l1 = word1.length;
+    let l2 = word2.length;
+    if (!l1)
+      return l2 * costIns;
+    if (!l2)
+      return l1 * costDel;
+
+    let p1 = new Array(l2 + 1)
+    let p2 = new Array(l2 + 1)
+
+    let i1, i2, c0, c1, c2, tmp;
+
+    for (i2 = 0; i2 <= l2; i2++)
+      p1[i2] = i2 * costIns;
+
+    for (i1 = 0; i1 < l1; i1++) {
+      p2[0] = p1[0] + costDel;
+
+      for (i2 = 0; i2 < l2; i2++) {
+        c0 = p1[i2] + ((word1[i1] === word2[i2]) ? 0 : costRep);
+        c1 = p1[i2 + 1] + costDel;
+
+        if (c1 < c0)
+          c0 = c1;
+
+        c2 = p2[i2] + costIns;
+
+        if (c2 < c0)
+          c0 = c2;
+
+        p2[i2 + 1] = c0;
+      }
+
+      tmp = p1;
+      p1 = p2;
+      p2 = tmp;
+    }
+
+    c0 = p1[l2];
+
+    return c0;
   }
 };
--- a/toolkit/modules/tests/xpcshell/test_FinderIterator.js
+++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
@@ -136,17 +136,17 @@ add_task(function* test_stop() {
     listener: { onIteratorRangeFound(range) { ++count; } },
     word: findText
   });
 
   FinderIterator.stop();
 
   yield whenDone;
 
-  Assert.equal(count, 100, "Number of ranges should match `kIterationSizeMax`");
+  Assert.equal(count, 0, "Number of ranges should be 0");
 
   FinderIterator.reset();
 });
 
 add_task(function* test_reset() {
   let findText = "tik";
   let rangeCount = 142;
   prepareIterator(findText, rangeCount);
@@ -156,29 +156,28 @@ add_task(function* test_reset() {
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: { onIteratorRangeFound(range) { ++count; } },
     word: findText
   });
 
   Assert.ok(FinderIterator.running, "Yup, running we are");
-  Assert.equal(count, 100, "Number of ranges should match `kIterationSizeMax`");
-  Assert.equal(FinderIterator.ranges.length, 100,
-    "Number of ranges should match `kIterationSizeMax`");
+  Assert.equal(count, 0, "Number of ranges should match 0");
+  Assert.equal(FinderIterator.ranges.length, 0, "Number of ranges should match 0");
 
   FinderIterator.reset();
 
   Assert.ok(!FinderIterator.running, "Nope, running we are not");
   Assert.equal(FinderIterator.ranges.length, 0, "No ranges after reset");
   Assert.equal(FinderIterator._previousRanges.length, 0, "No ranges after reset");
 
   yield whenDone;
 
-  Assert.equal(count, 100, "Number of ranges should match `kIterationSizeMax`");
+  Assert.equal(count, 0, "Number of ranges should match 0");
 });
 
 add_task(function* test_parallel_starts() {
   let findText = "tak";
   let rangeCount = 2143;
   prepareIterator(findText, rangeCount);
 
   // Start off the iterator.
@@ -187,17 +186,17 @@ add_task(function* test_parallel_starts(
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: { onIteratorRangeFound(range) { ++count; } },
     word: findText
   });
 
   // Start again after a few milliseconds.
-  yield new Promise(resolve => gMockWindow.setTimeout(resolve, 20));
+  yield new Promise(resolve => gMockWindow.setTimeout(resolve, 120));
   Assert.ok(FinderIterator.running, "We ought to be running here");
 
   let count2 = 0;
   let whenDone2 = FinderIterator.start({
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: { onIteratorRangeFound(range) { ++count2; } },
@@ -214,9 +213,54 @@ add_task(function* test_parallel_starts(
   yield whenDone2;
 
   Assert.greater(count, FinderIterator.kIterationSizeMax, "At least one range should've been found");
   Assert.less(count, rangeCount, "Not all ranges should've been found");
   Assert.greater(count2, FinderIterator.kIterationSizeMax, "At least one range should've been found");
   Assert.less(count2, rangeCount, "Not all ranges should've been found");
 
   Assert.equal(count2, count, "The second start was later, but should have caught up");
+
+  FinderIterator.reset();
 });
+
+add_task(function* test_allowDistance() {
+  let findText = "gup";
+  let rangeCount = 20;
+  prepareIterator(findText, rangeCount);
+
+  // Start off the iterator.
+  let count = 0;
+  let whenDone = FinderIterator.start({
+    caseSensitive: false,
+    entireWord: false,
+    finder: gMockFinder,
+    listener: { onIteratorRangeFound(range) { ++count; } },
+    word: findText
+  });
+
+  let count2 = 0;
+  let whenDone2 = FinderIterator.start({
+    caseSensitive: false,
+    entireWord: false,
+    finder: gMockFinder,
+    listener: { onIteratorRangeFound(range) { ++count2; } },
+    word: "gu"
+  });
+
+  let count3 = 0;
+  let whenDone3 = FinderIterator.start({
+    allowDistance: 1,
+    caseSensitive: false,
+    entireWord: false,
+    finder: gMockFinder,
+    listener: { onIteratorRangeFound(range) { ++count3; } },
+    word: "gu"
+  });
+
+  yield Promise.all([whenDone, whenDone2, whenDone3]);
+
+  Assert.equal(count, rangeCount, "The first iterator invocation should yield all results");
+  Assert.equal(count2, 0, "The second iterator invocation should yield _no_ results");
+  Assert.equal(count3, rangeCount, "The first iterator invocation should yield all results");
+
+  FinderIterator.reset();
+});