Bug 202251 - Add an option to ignore diacritics when searching. r=fluent-reviewers,mikedeboer,jfkthame,flod
☠☠ backed out by 469a80cf68ca ☠ ☠
authorAlex Henrie <alexhenrie24@gmail.com>
Thu, 05 Dec 2019 18:08:20 +0000
changeset 505686 b89936db7178260311cbd3438497ed16d0b5be55
parent 505685 01679b21503d67157064840b22bafecec880dfa7
child 505687 c21f94d31eb5b95405d0f8850559af13af9b7ea5
push id36886
push usernbeleuzu@mozilla.com
push dateFri, 06 Dec 2019 04:43:57 +0000
treeherdermozilla-central@10160518ddc8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfluent-reviewers, mikedeboer, jfkthame, flod
bugs202251
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 202251 - Add an option to ignore diacritics when searching. r=fluent-reviewers,mikedeboer,jfkthame,flod Differential Revision: https://phabricator.services.mozilla.com/D51841
intl/unicharutil/util/nsUnicharUtils.cpp
intl/unicharutil/util/nsUnicharUtils.h
intl/unicharutil/util/nsUnicodeProperties.cpp
intl/unicharutil/util/nsUnicodeProperties.h
mobile/android/app/mobile.js
mobile/android/modules/geckoview/GeckoViewContent.jsm
modules/libpref/init/all.js
toolkit/actors/FinderChild.jsm
toolkit/components/extensions/FindContent.jsm
toolkit/components/find/nsFind.cpp
toolkit/components/find/nsFind.h
toolkit/components/find/nsFindService.cpp
toolkit/components/find/nsFindService.h
toolkit/components/find/nsIFind.idl
toolkit/components/find/nsIFindService.idl
toolkit/components/find/nsIWebBrowserFind.idl
toolkit/components/find/nsWebBrowserFind.cpp
toolkit/components/find/nsWebBrowserFind.h
toolkit/components/typeaheadfind/nsITypeAheadFind.idl
toolkit/components/typeaheadfind/nsTypeAheadFind.cpp
toolkit/components/typeaheadfind/nsTypeAheadFind.h
toolkit/components/windowcreator/test/test_nsFind.html
toolkit/content/tests/chrome/findbar_events_window.xhtml
toolkit/content/tests/chrome/findbar_window.xhtml
toolkit/content/widgets/findbar.js
toolkit/locales/en-US/chrome/global/findbar.properties
toolkit/locales/en-US/toolkit/main-window/findbar.ftl
toolkit/modules/Finder.jsm
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/FinderIterator.jsm
toolkit/modules/FinderParent.jsm
toolkit/modules/tests/xpcshell/test_FinderIterator.js
--- a/intl/unicharutil/util/nsUnicharUtils.cpp
+++ b/intl/unicharutil/util/nsUnicharUtils.cpp
@@ -157,16 +157,42 @@ void ToFoldedCase(const char16_t* aIn, c
       aOut[i++] = H_SURROGATE(ch);
       aOut[i] = L_SURROGATE(ch);
       continue;
     }
     aOut[i] = ToFoldedCase(ch);
   }
 }
 
+uint32_t ToNaked(uint32_t aChar) {
+  if (IS_ASCII(aChar)) {
+    return aChar;
+  }
+  return mozilla::unicode::GetNaked(aChar);
+}
+
+void ToNaked(nsAString& aString) {
+  char16_t* buf = aString.BeginWriting();
+  ToNaked(buf, buf, aString.Length());
+}
+
+void ToNaked(const char16_t* aIn, char16_t* aOut, uint32_t aLen) {
+  for (uint32_t i = 0; i < aLen; i++) {
+    uint32_t ch = aIn[i];
+    if (i < aLen - 1 && NS_IS_SURROGATE_PAIR(ch, aIn[i + 1])) {
+      ch = mozilla::unicode::GetNaked(SURROGATE_TO_UCS4(ch, aIn[i + 1]));
+      NS_ASSERTION(!IS_IN_BMP(ch), "stripping crossed BMP/SMP boundary!");
+      aOut[i++] = H_SURROGATE(ch);
+      aOut[i] = L_SURROGATE(ch);
+      continue;
+    }
+    aOut[i] = ToNaked(ch);
+  }
+}
+
 int32_t nsCaseInsensitiveStringComparator::operator()(const char16_t* lhs,
                                                       const char16_t* rhs,
                                                       uint32_t lLength,
                                                       uint32_t rLength) const {
   return (lLength == rLength) ? CaseInsensitiveCompare(lhs, rhs, lLength)
                               : (lLength > rLength) ? 1 : -1;
 }
 
--- a/intl/unicharutil/util/nsUnicharUtils.h
+++ b/intl/unicharutil/util/nsUnicharUtils.h
@@ -51,16 +51,20 @@ inline bool IsUpperCase(uint32_t c) { re
 inline bool IsLowerCase(uint32_t c) { return ToUpperCase(c) != c; }
 
 #ifdef MOZILLA_INTERNAL_API
 
 uint32_t ToFoldedCase(uint32_t aChar);
 void ToFoldedCase(nsAString& aString);
 void ToFoldedCase(const char16_t* aIn, char16_t* aOut, uint32_t aLen);
 
+uint32_t ToNaked(uint32_t aChar);
+void ToNaked(nsAString& aString);
+void ToNaked(const char16_t* aIn, char16_t* aOut, uint32_t aLen);
+
 class nsCaseInsensitiveStringComparator : public nsStringComparator {
  public:
   nsCaseInsensitiveStringComparator() = default;
 
   virtual int32_t operator()(const char16_t*, const char16_t*, uint32_t,
                              uint32_t) const override;
 };
 
--- a/intl/unicharutil/util/nsUnicodeProperties.cpp
+++ b/intl/unicharutil/util/nsUnicodeProperties.cpp
@@ -3,18 +3,21 @@
 /* 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/. */
 
 #include "nsUnicodeProperties.h"
 #include "nsUnicodePropertyData.cpp"
 
 #include "mozilla/ArrayUtils.h"
+#include "mozilla/HashTable.h"
 #include "nsCharTraits.h"
 
+#include "unicode/normalizer2.h"
+
 #define UNICODE_BMP_LIMIT 0x10000
 #define UNICODE_LIMIT 0x110000
 
 const nsCharProps2& GetCharProps2(uint32_t aCh) {
   if (aCh < UNICODE_BMP_LIMIT) {
     return sCharProp2Values[sCharProp2Pages[0][aCh >> kCharProp2CharBits]]
                            [aCh & ((1 << kCharProp2CharBits) - 1)];
   }
@@ -300,11 +303,55 @@ uint32_t CountGraphemeClusters(const cha
   uint32_t result = 0;
   while (!iter.AtEnd()) {
     ++result;
     iter.Next();
   }
   return result;
 }
 
+uint32_t GetNaked(uint32_t aCh) {
+  using namespace mozilla;
+
+  static const UNormalizer2* normalizer;
+  static HashMap<uint32_t, uint32_t> decompositions;
+
+  HashMap<uint32_t, uint32_t>::Ptr entry = decompositions.lookup(aCh);
+  if (entry.found()) {
+    return entry->value();
+  }
+
+  UErrorCode error = U_ZERO_ERROR;
+  if (!normalizer) {
+    normalizer = unorm2_getNFDInstance(&error);
+    if (U_FAILURE(error)) {
+      return aCh;
+    }
+  }
+
+  UChar ds[16];
+  uint32_t dc;
+  if (unorm2_getDecomposition(normalizer, aCh, ds, sizeof(ds) / sizeof(UChar),
+                              &error) > 0) {
+    if (NS_IS_HIGH_SURROGATE(ds[0])) {
+      dc = SURROGATE_TO_UCS4(ds[0], ds[1]);
+    } else {
+      dc = ds[0];
+    }
+    if (IS_IN_BMP(dc) != IS_IN_BMP(aCh)) {
+      // Mappings that would change the length of a UTF-16 string are not
+      // currently supported.
+      dc = aCh;
+    }
+    if (!decompositions.putNew(aCh, dc)) {
+      // We're out of memory, so delete the cache to free some up.
+      decompositions.clearAndCompact();
+    }
+  } else {
+    dc = aCh;
+  }
+
+  return dc;
+}
+
 }  // end namespace unicode
 
 }  // end namespace mozilla
--- a/intl/unicharutil/util/nsUnicodeProperties.h
+++ b/intl/unicharutil/util/nsUnicodeProperties.h
@@ -224,16 +224,19 @@ class ClusterIterator {
 #ifdef DEBUG
   const char16_t* mText;
 #endif
 };
 
 // Count the number of grapheme clusters in the given string
 uint32_t CountGraphemeClusters(const char16_t* aText, uint32_t aLength);
 
+// Remove diacritics from a character
+uint32_t GetNaked(uint32_t aCh);
+
 // A simple reverse iterator for a string of char16_t codepoints that
 // advances by Unicode grapheme clusters
 class ClusterReverseIterator {
  public:
   ClusterReverseIterator(const char16_t* aText, uint32_t aLength)
       : mPos(aText + aLength), mLimit(aText) {}
 
   operator const char16_t*() const { return mPos; }
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -233,16 +233,17 @@ pref("keyword.enabled", true);
 pref("browser.fixup.domainwhitelist.localhost", true);
 
 pref("accessibility.typeaheadfind", false);
 pref("accessibility.typeaheadfind.timeout", 5000);
 pref("accessibility.typeaheadfind.flashBar", 1);
 pref("accessibility.typeaheadfind.linksonly", false);
 pref("accessibility.typeaheadfind.casesensitive", 0);
 pref("accessibility.browsewithcaret_shortcut.enabled", false);
+pref("findbar.matchdiacritics", 0);
 
 // Whether the character encoding menu is under the main Firefox button. This
 // preference is a string so that localizers can alter it.
 pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
 
 // SSL error page behaviour
 pref("browser.ssl_override_behavior", 2);
 pref("browser.xul.error_pages.expert_bad_cert", false);
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -327,16 +327,17 @@ class GeckoViewContent extends GeckoView
 
       onCurrentSelection() {},
 
       onHighlightFinished() {},
     };
 
     finder.caseSensitive = !!aData.matchCase;
     finder.entireWord = !!aData.wholeWord;
+    finder.matchDiacritics = !!aData.matchDiacritics;
     finder.addResultListener(this._finderListener);
 
     const drawOutline =
       this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline;
 
     if (!aData.searchString || aData.searchString === finder.searchString) {
       // Search again.
       aData.searchString = finder.searchString;
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -763,16 +763,21 @@ pref("accessibility.typeaheadfind.enable
   pref("accessibility.typeaheadfind.prefillwithselection", false);
 #else
   pref("accessibility.typeaheadfind.prefillwithselection", true);
 #endif
 pref("accessibility.typeaheadfind.matchesCountLimit", 1000);
 pref("findbar.highlightAll", false);
 pref("findbar.entireword", false);
 pref("findbar.iteratorTimeout", 100);
+// matchdiacritics: controls the find bar's diacritic matching
+//     0 - "never"  (ignore diacritics)
+//     1 - "always" (match diacritics)
+// other - "auto"   (match diacritics if input has diacritics, ignore otherwise)
+pref("findbar.matchdiacritics", 0);
 
 // 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/actors/FinderChild.jsm
+++ b/toolkit/actors/FinderChild.jsm
@@ -23,16 +23,20 @@ class FinderChild extends JSWindowActorC
   receiveMessage(aMessage) {
     let data = aMessage.data;
 
     switch (aMessage.name) {
       case "Finder:CaseSensitive":
         this.finder.caseSensitive = data.caseSensitive;
         break;
 
+      case "Finder:MatchDiacritics":
+        this.finder.matchDiacritics = data.matchDiacritics;
+        break;
+
       case "Finder:EntireWord":
         this.finder.entireWord = data.entireWord;
         break;
 
       case "Finder:SetSearchStringToSelection": {
         return new Promise(resolve => {
           resolve(this.finder.setSearchStringToSelection());
         });
--- a/toolkit/components/extensions/FindContent.jsm
+++ b/toolkit/components/extensions/FindContent.jsm
@@ -46,42 +46,45 @@ class FindContent {
    *
    * Performs a search which will cache found ranges in `iterator._previousRanges`.  Cached
    * data can then be used by `highlightResults`, `_collectRectData` and `_serializeRangeData`.
    *
    * @param {object} params - the params.
    * @param {string} queryphrase - the text to search for.
    * @param {boolean} caseSensitive - whether to use case sensitive matches.
    * @param {boolean} includeRangeData - whether to collect and return range data.
+   * @param {boolean} matchDiacritics - whether diacritics must match.
    * @param {boolean} searchString - whether to collect and return rect data.
    *
    * @returns {object} that includes:
    *   {number} count - number of results found.
    *   {array} rangeData (if opted) - serialized representation of ranges found.
    *   {array} rectData (if opted) - rect data of ranges found.
    */
   findRanges(params) {
     return new Promise(resolve => {
       let {
         queryphrase,
         caseSensitive,
         entireWord,
         includeRangeData,
         includeRectData,
+        matchDiacritics,
       } = params;
 
       this.iterator.reset();
 
       // Cast `caseSensitive` and `entireWord` to boolean, otherwise _iterator.start will throw.
       let iteratorPromise = this.iterator.start({
         word: queryphrase,
         caseSensitive: !!caseSensitive,
         entireWord: !!entireWord,
         finder: this.finder,
         listener: this.finder,
+        matchDiacritics: !!matchDiacritics,
         useSubFrames: false,
       });
 
       iteratorPromise.then(() => {
         let rangeData;
         let rectData;
         if (includeRangeData) {
           rangeData = this._serializeRangeData();
--- a/toolkit/components/find/nsFind.cpp
+++ b/toolkit/components/find/nsFind.cpp
@@ -56,17 +56,20 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFind)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFind)
 
 NS_IMPL_CYCLE_COLLECTION(nsFind)
 
 nsFind::nsFind()
-    : mFindBackward(false), mCaseSensitive(false), mWordBreaker(nullptr) {}
+    : mFindBackward(false),
+      mCaseSensitive(false),
+      mMatchDiacritics(false),
+      mWordBreaker(nullptr) {}
 
 nsFind::~nsFind() = default;
 
 #ifdef DEBUG_FIND
 #  define DEBUG_FIND_PRINTF(...) printf(__VA_ARGS__)
 #else
 #  define DEBUG_FIND_PRINTF(...) /* nothing */
 #endif
@@ -392,16 +395,32 @@ nsFind::GetEntireWord(bool* aEntireWord)
 }
 
 NS_IMETHODIMP
 nsFind::SetEntireWord(bool aEntireWord) {
   mWordBreaker = aEntireWord ? nsContentUtils::WordBreaker() : nullptr;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsFind::GetMatchDiacritics(bool* aMatchDiacritics) {
+  if (!aMatchDiacritics) {
+    return NS_ERROR_NULL_POINTER;
+  }
+
+  *aMatchDiacritics = mMatchDiacritics;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFind::SetMatchDiacritics(bool aMatchDiacritics) {
+  mMatchDiacritics = aMatchDiacritics;
+  return NS_OK;
+}
+
 // Here begins the find code. A ten-thousand-foot view of how it works: Find
 // needs to be able to compare across inline (but not block) nodes, e.g. find
 // for "abc" should match a<b>b</b>c. So after we've searched a node, we're not
 // done with it; in the case of a partial match we may need to reset the
 // iterator to go back to a previously visited node, so we always save the
 // "match anchor" node and offset.
 //
 // Text nodes store their text in an nsTextFragment, which is effectively a
@@ -502,16 +521,19 @@ nsFind::Find(const nsAString& aPatText, 
   NS_ENSURE_ARG(root);
 
   *aRangeRet = 0;
 
   nsAutoString patAutoStr(aPatText);
   if (!mCaseSensitive) {
     ToFoldedCase(patAutoStr);
   }
+  if (!mMatchDiacritics) {
+    ToNaked(patAutoStr);
+  }
 
   // Ignore soft hyphens in the pattern
   static const char kShy[] = {char(CH_SHY), 0};
   patAutoStr.StripChars(kShy);
 
   const char16_t* patStr = patAutoStr.get();
   int32_t patLen = patAutoStr.Length() - 1;
 
@@ -680,18 +702,23 @@ nsFind::Find(const nsAString& aPatText, 
       if (OVERFLOW_PINDEX) {
         NS_ASSERTION(false, "Missed a whitespace match");
       }
 #endif
       patc = DecodeChar(patStr, &pindex);
     }
     if (!inWhitespace && IsSpace(patc)) {
       inWhitespace = true;
-    } else if (!inWhitespace && !mCaseSensitive) {
-      c = ToFoldedCase(c);
+    } else if (!inWhitespace) {
+      if (!mCaseSensitive) {
+        c = ToFoldedCase(c);
+      }
+      if (!mMatchDiacritics) {
+        c = ToNaked(c);
+      }
     }
 
     if (c == CH_SHY) {
       // ignore soft hyphens in the document
       continue;
     }
 
     if (!mCaseSensitive) {
--- a/toolkit/components/find/nsFind.h
+++ b/toolkit/components/find/nsFind.h
@@ -37,16 +37,17 @@ class nsFind : public nsIFind {
   nsFind();
 
  protected:
   virtual ~nsFind();
 
   // Parameters set from the interface:
   bool mFindBackward;
   bool mCaseSensitive;
+  bool mMatchDiacritics;
 
   // Use "find entire words" mode by setting to a word breaker or null, to
   // disable "entire words" mode.
   RefPtr<mozilla::intl::WordBreaker> mWordBreaker;
 
   struct State;
   class StateRestorer;
 
--- a/toolkit/components/find/nsFindService.cpp
+++ b/toolkit/components/find/nsFindService.cpp
@@ -74,8 +74,18 @@ NS_IMETHODIMP nsFindService::GetMatchCas
   NS_ENSURE_ARG_POINTER(aMatchCase);
   *aMatchCase = mMatchCase;
   return NS_OK;
 }
 NS_IMETHODIMP nsFindService::SetMatchCase(bool aMatchCase) {
   mMatchCase = aMatchCase;
   return NS_OK;
 }
+
+NS_IMETHODIMP nsFindService::GetMatchDiacritics(bool* aMatchDiacritics) {
+  NS_ENSURE_ARG_POINTER(aMatchDiacritics);
+  *aMatchDiacritics = mMatchDiacritics;
+  return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetMatchDiacritics(bool aMatchDiacritics) {
+  mMatchDiacritics = aMatchDiacritics;
+  return NS_OK;
+}
--- a/toolkit/components/find/nsFindService.h
+++ b/toolkit/components/find/nsFindService.h
@@ -35,9 +35,10 @@ class nsFindService : public nsIFindServ
 
   nsString mSearchString;
   nsString mReplaceString;
 
   bool mFindBackwards;
   bool mWrapFind;
   bool mEntireWord;
   bool mMatchCase;
+  bool mMatchDiacritics;
 };
--- a/toolkit/components/find/nsIFind.idl
+++ b/toolkit/components/find/nsIFind.idl
@@ -10,16 +10,17 @@ interface nsIWordBreaker;
 webidl Range;
 
 [scriptable, uuid(40aba110-2a56-4678-be90-e2c17a9ae7d7)]
 interface nsIFind : nsISupports
 {
   attribute boolean findBackwards;
   attribute boolean caseSensitive;
   attribute boolean entireWord;
+  attribute boolean matchDiacritics;
 
   /**
    * Find some text in the current context. The implementation is
    * responsible for performing the find and highlighting the text.
    *
    * @param aPatText     The text to search for.
    * @param aSearchRange A Range specifying domain of search.
    * @param aStartPoint  A Range specifying search start point.
--- a/toolkit/components/find/nsIFindService.idl
+++ b/toolkit/components/find/nsIFindService.idl
@@ -17,10 +17,11 @@ interface nsIFindService : nsISupports
 
     attribute AString searchString;
     attribute AString replaceString;
 
     attribute boolean findBackwards;
     attribute boolean wrapFind;
     attribute boolean entireWord;
     attribute boolean matchCase;
+    attribute boolean matchDiacritics;
 
 };
--- a/toolkit/components/find/nsIWebBrowserFind.idl
+++ b/toolkit/components/find/nsIWebBrowserFind.idl
@@ -75,16 +75,23 @@ interface nsIWebBrowserFind : nsISupport
     /**
      * matchCase
      *
      * Whether to match case (case sensitive) when searching. Default is false.
      */
     attribute boolean matchCase;
 
     /**
+     * matchDiacritics
+     *
+     * Whether to match diacritics when searching. Default is false.
+     */
+    attribute boolean matchDiacritics;
+
+    /**
      * searchFrames
      *
      * Whether to search through all frames in the content area. Default is true.
      *
      * Note that you can control whether the search propagates into child or
      * parent frames explicitly using nsIWebBrowserFindInFrames, but if one,
      * but not both, of searchSubframes and searchParentFrames are set, this
      * returns false.
--- a/toolkit/components/find/nsWebBrowserFind.cpp
+++ b/toolkit/components/find/nsWebBrowserFind.cpp
@@ -49,16 +49,17 @@ using mozilla::dom::Document;
 using mozilla::dom::Element;
 using mozilla::dom::Selection;
 
 nsWebBrowserFind::nsWebBrowserFind()
     : mFindBackwards(false),
       mWrapFind(false),
       mEntireWord(false),
       mMatchCase(false),
+      mMatchDiacritics(false),
       mSearchSubFrames(true),
       mSearchParentFrames(true) {}
 
 nsWebBrowserFind::~nsWebBrowserFind() {}
 
 NS_IMPL_ISUPPORTS(nsWebBrowserFind, nsIWebBrowserFind,
                   nsIWebBrowserFindInFrames)
 
@@ -285,16 +286,29 @@ nsWebBrowserFind::GetMatchCase(bool* aMa
 }
 
 NS_IMETHODIMP
 nsWebBrowserFind::SetMatchCase(bool aMatchCase) {
   mMatchCase = aMatchCase;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsWebBrowserFind::GetMatchDiacritics(bool* aMatchDiacritics) {
+  NS_ENSURE_ARG_POINTER(aMatchDiacritics);
+  *aMatchDiacritics = mMatchDiacritics;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWebBrowserFind::SetMatchDiacritics(bool aMatchDiacritics) {
+  mMatchDiacritics = aMatchDiacritics;
+  return NS_OK;
+}
+
 void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
                                              nsRange* aRange) {
   RefPtr<Document> doc = aWindow->GetDoc();
   if (!doc) {
     return;
   }
 
   PresShell* presShell = doc->GetPresShell();
@@ -621,16 +635,17 @@ nsresult nsWebBrowserFind::SearchInFrame
     return NS_ERROR_DOM_PROP_ACCESS_DENIED;
   }
 
   nsresult rv;
   nsCOMPtr<nsIFind> find = do_CreateInstance(NS_FIND_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   (void)find->SetCaseSensitive(mMatchCase);
+  (void)find->SetMatchDiacritics(mMatchDiacritics);
   (void)find->SetFindBackwards(mFindBackwards);
 
   (void)find->SetEntireWord(mEntireWord);
 
   // Now make sure the content (for actual finding) and frame (for
   // selection) models are up to date.
   theDoc->FlushPendingNotifications(FlushType::Frames);
 
--- a/toolkit/components/find/nsWebBrowserFind.h
+++ b/toolkit/components/find/nsWebBrowserFind.h
@@ -76,16 +76,17 @@ class nsWebBrowserFind : public nsIWebBr
 
  protected:
   nsString mSearchString;
 
   bool mFindBackwards;
   bool mWrapFind;
   bool mEntireWord;
   bool mMatchCase;
+  bool mMatchDiacritics;
 
   bool mSearchSubFrames;
   bool mSearchParentFrames;
 
   // These are all weak because who knows if windows can go away during our
   // lifetime.
   nsWeakPtr mCurrentSearchFrame;
   nsWeakPtr mRootSearchFrame;
--- a/toolkit/components/typeaheadfind/nsITypeAheadFind.idl
+++ b/toolkit/components/typeaheadfind/nsITypeAheadFind.idl
@@ -65,16 +65,17 @@ interface nsITypeAheadFind : nsISupports
   /* Check if a range is actually rendered (out of viewport always false) */
   boolean isRangeRendered(in Range aRange);
 
   /******************************* Attributes ******************************/
 
   readonly attribute AString searchString;
                                         // Most recent search string
   attribute boolean caseSensitive;      // Searches are case sensitive
+  attribute boolean matchDiacritics;    // Searches preserve diacritics
   attribute boolean entireWord;         // Search for whole words only
   readonly attribute Element foundLink;
                                         // Most recent elem found, if a link
   readonly attribute Element foundEditable;
                                         // Most recent elem found, if editable
   readonly attribute mozIDOMWindow currentWindow;
                                         // Window of most recent match
 
--- a/toolkit/components/typeaheadfind/nsTypeAheadFind.cpp
+++ b/toolkit/components/typeaheadfind/nsTypeAheadFind.cpp
@@ -81,17 +81,18 @@ static NS_DEFINE_CID(kFrameTraversalCID,
 
 nsTypeAheadFind::nsTypeAheadFind()
     : mStartLinksOnlyPref(false),
       mCaretBrowsingOn(false),
       mDidAddObservers(false),
       mLastFindLength(0),
       mIsSoundInitialized(false),
       mCaseSensitive(false),
-      mEntireWord(false) {}
+      mEntireWord(false),
+      mMatchDiacritics(false) {}
 
 nsTypeAheadFind::~nsTypeAheadFind() {
   nsCOMPtr<nsIPrefBranch> prefInternal(
       do_GetService(NS_PREFSERVICE_CONTRACTID));
   if (prefInternal) {
     prefInternal->RemoveObserver("accessibility.typeaheadfind", this);
     prefInternal->RemoveObserver("accessibility.browsewithcaret", this);
   }
@@ -207,16 +208,34 @@ nsTypeAheadFind::SetEntireWord(bool isEn
 NS_IMETHODIMP
 nsTypeAheadFind::GetEntireWord(bool* isEntireWord) {
   *isEntireWord = mEntireWord;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsTypeAheadFind::SetMatchDiacritics(bool matchDiacritics) {
+  mMatchDiacritics = matchDiacritics;
+
+  if (mFind) {
+    mFind->SetMatchDiacritics(mMatchDiacritics);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetMatchDiacritics(bool* matchDiacritics) {
+  *matchDiacritics = mMatchDiacritics;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsTypeAheadFind::SetDocShell(nsIDocShell* aDocShell) {
   mDocShell = do_GetWeakReference(aDocShell);
 
   mWebBrowserFind = do_GetInterface(aDocShell);
   NS_ENSURE_TRUE(mWebBrowserFind, NS_ERROR_FAILURE);
 
   mPresShell = do_GetWeakReference(aDocShell->GetPresShell());
 
--- a/toolkit/components/typeaheadfind/nsTypeAheadFind.h
+++ b/toolkit/components/typeaheadfind/nsTypeAheadFind.h
@@ -115,29 +115,31 @@ class nsTypeAheadFind : public nsITypeAh
   RefPtr<nsRange> mStartPointRange;
   RefPtr<nsRange> mEndPointRange;
 
   // Cached useful interfaces
   nsCOMPtr<nsIFind> mFind;
 
   bool mCaseSensitive;
   bool mEntireWord;
+  bool mMatchDiacritics;
 
   bool EnsureFind() {
     if (mFind) {
       return true;
     }
 
     mFind = do_CreateInstance("@mozilla.org/embedcomp/rangefind;1");
     if (!mFind) {
       return false;
     }
 
     mFind->SetCaseSensitive(mCaseSensitive);
     mFind->SetEntireWord(mEntireWord);
+    mFind->SetMatchDiacritics(mMatchDiacritics);
 
     return true;
   }
 
   nsCOMPtr<nsIWebBrowserFind> mWebBrowserFind;
 
   // The focused content window that we're listening to and its cached objects
   nsWeakPtr mDocShell;
--- a/toolkit/components/windowcreator/test/test_nsFind.html
+++ b/toolkit/components/windowcreator/test/test_nsFind.html
@@ -1,24 +1,24 @@
 <!DOCTYPE HTML>
 <html>
 <!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=202251
 https://bugzilla.mozilla.org/show_bug.cgi?id=450048
 https://bugzilla.mozilla.org/show_bug.cgi?id=969980
 https://bugzilla.mozilla.org/show_bug.cgi?id=1589786
 -->
 <head>
   <meta charset="UTF-8">
   <title>Test for nsFind::Find()</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 
 <script type="application/javascript">
 
-/** Test for Bug 450048 **/
 SimpleTest.waitForExplicitFinish();
 
 async function runTests() {
   // Check nsFind class and its nsIFind interface.
 
   // Inject some text that we'll search for later.
   const NULL_CHARACTER = "\0";
   const INJECTED_NULL_TEXT = "injected null\0";
@@ -42,36 +42,43 @@ async function runTests() {
   var startPt = searchRange;
   var endPt = searchRange;
 
   var searchValue, retRange;
 
   rf.findBackwards = false;
 
   rf.caseSensitive = false;
+  rf.matchDiacritics = false;
 
   searchValue = "TexT";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)");
 
-  searchValue = "λόγος";
+  searchValue = "λογος";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)");
 
   searchValue = "degrees k";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)");
 
   searchValue = "𐐸𐐯𐑊𐐬";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)");
 
+  rf.matchDiacritics = true;
+
+  searchValue = "λογος";
+  retRange = rf.Find(searchValue, searchRange, startPt, endPt);
+  ok(!retRange, "\"" + searchValue + "\" found (not caseSensitive)");
+
   rf.caseSensitive = true;
 
-  // searchValue = "TexT";
+  searchValue = "TexT";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(!retRange, "\"" + searchValue + "\" found (caseSensitive)");
 
   searchValue = "text";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found");
 
   // Matches |i<b>n&shy;t</b>o|.
--- a/toolkit/content/tests/chrome/findbar_events_window.xhtml
+++ b/toolkit/content/tests/chrome/findbar_events_window.xhtml
@@ -55,16 +55,17 @@
 
     async function onDocumentLoaded() {
       gFindBar.open();
       gFindBar.onFindCommand();
 
       await testFind();
       await testFindAgain();
       await testCaseSensitivity();
+      await testDiacriticMatching();
       await testHighlight();
     }
 
     function checkSelection() {
       return new Promise(resolve => {
         SimpleTest.executeSoon(() => {
           ContentTask.spawn(gBrowser, null, async function() {
             let selected = content.getSelection();
@@ -138,16 +139,34 @@
       // Toggle it back to the original setting.
       matchCaseCheckbox.click();
 
       // Changing case sensitivity does the search so clear the selected text
       // before the next test.
       await ContentTask.spawn(gBrowser, null, () => content.getSelection().removeAllRanges());
     }
 
+    async function testDiacriticMatching() {
+      info("Testing normal diacritic matching.");
+      let promise = once(gFindBar, "finddiacriticmatchingchange", false);
+
+      let matchDiacriticsCheckbox = gFindBar.getElement("find-match-diacritics");
+      matchDiacriticsCheckbox.click();
+
+      let e = await promise;
+      ok(e.detail.matchDiacritics, "find should match diacritics");
+
+      // Toggle it back to the original setting.
+      matchDiacriticsCheckbox.click();
+
+      // Changing diacritic matching does the search so clear the selected text
+      // before the next test.
+      await ContentTask.spawn(gBrowser, null, () => content.getSelection().removeAllRanges());
+    }
+
     async function testHighlight() {
       info("Testing find with highlight all.");
       // Update the find state so the highlight button is clickable.
       gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false);
 
       let promise = once(gFindBar, "findhighlightallchange");
 
       let highlightButton = gFindBar.getElement("highlight");
--- a/toolkit/content/tests/chrome/findbar_window.xhtml
+++ b/toolkit/content/tests/chrome/findbar_window.xhtml
@@ -23,17 +23,17 @@
     const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
     const {ContentTask} = ChromeUtils.import("resource://testing-common/ContentTask.jsm");
     ContentTask.setTestScope(window.arguments[0]);
 
     var gPrefsvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
 
     const SAMPLE_URL = "http://www.mozilla.org/";
     const SAMPLE_TEXT = "Some text in a text field.";
-    const SEARCH_TEXT = "Text Test";
+    const SEARCH_TEXT = "Text Test (δοκιμή)";
     const NOT_FOUND_TEXT = "This text is not on the page."
     const ITERATOR_TIMEOUT = gPrefsvc.getIntPref("findbar.iteratorTimeout");
 
     var gFindBar = null;
     var gBrowser;
 
     var gClipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
     var gHasFindClipboard = gClipboard.supportsFindClipboard();
@@ -83,17 +83,17 @@
       info("Starting test with browser '" + browserId + "'");
       gBrowser = document.getElementById(browserId);
       gFindBar.browser = gBrowser;
 
       // Tests delays the loading of a document for one second.
       await new Promise(resolve => setTimeout(resolve, 1000));
 
       let promise = BrowserTestUtils.browserLoaded(gBrowser);
-      BrowserTestUtils.loadURI(gBrowser, "data:text/html,<h2 id='h2'>" + SEARCH_TEXT +
+      BrowserTestUtils.loadURI(gBrowser, "data:text/html;charset=utf-8,<h2 id='h2'>" + SEARCH_TEXT +
         "</h2><h2><a href='" + SAMPLE_URL + "'>Link Test</a></h2><input id='text' type='text' value='" +
         SAMPLE_TEXT + "'></input><input id='button' type='button'></input><img id='img' width='50' height='50'/>",
         { triggeringPrincipal: window.document.nodePrincipal });
       await promise;
       await onDocumentLoaded();
     }
 
     async function onDocumentLoaded() {
@@ -730,16 +730,34 @@
       gFindBar.close();
       ok(gFindBar.hidden, "The findbar is closed.");
       promise = promiseFindResult();
       gFindBar.onFindAgainCommand();
       await promise;
       ok(gFindBar.hidden, "Successful Find Again leaves the find bar closed.");
     }
 
+    async function testToggleDiacriticMatching() {
+      await openFindbar();
+      let promise = promiseFindResult();
+      await enterStringIntoFindField("δοκιμη", false);
+      let result = await promise;
+      is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found");
+
+      await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20));
+      promise = promiseFindResult();
+      let check = gFindBar.getElement("find-match-diacritics");
+      check.click();
+      result = await promise;
+      is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found");
+
+      check.click();
+      gFindBar.close(true);
+    }
+
     async function testToggleEntireWord() {
       await openFindbar();
       let promise = promiseFindResult();
       await enterStringIntoFindField("Tex", false);
       let result = await promise;
       is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found");
 
       await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20));
--- a/toolkit/content/widgets/findbar.js
+++ b/toolkit/content/widgets/findbar.js
@@ -25,16 +25,17 @@
     ["manualFAYT", "accessibility.typeaheadfind.manual"],
     ["typeAheadLinksOnly", "accessibility.typeaheadfind.linksonly"],
     ["entireWord", "findbar.entireword"],
     ["highlightAll", "findbar.highlightAll"],
     ["useModalHighlight", "findbar.modalHighlight"],
   ]);
   const PREFS_TO_OBSERVE_INT = new Map([
     ["typeAheadCaseSensitive", "accessibility.typeaheadfind.casesensitive"],
+    ["matchDiacritics", "findbar.matchdiacritics"],
   ]);
   const PREFS_TO_OBSERVE_ALL = new Map([
     ...PREFS_TO_OBSERVE_BOOL,
     ...PREFS_TO_OBSERVE_INT,
   ]);
   const TOPIC_MAC_APP_ACTIVATE = "mac_app_activate";
 
   class MozFindbar extends XULElement {
@@ -67,19 +68,22 @@
             oncommand="onFindAgainCommand(true);" disabled="true" />
           <toolbarbutton anonid="find-next" class="findbar-find-next tabbable"
             data-l10n-id="findbar-next" oncommand="onFindAgainCommand(false);" disabled="true" />
         </hbox>
         <toolbarbutton anonid="highlight" class="findbar-highlight findbar-button tabbable"
           data-l10n-id="findbar-highlight-all2" oncommand="toggleHighlight(this.checked);" type="checkbox" />
         <toolbarbutton anonid="find-case-sensitive" class="findbar-case-sensitive findbar-button tabbable"
           data-l10n-id="findbar-case-sensitive" oncommand="_setCaseSensitivity(this.checked ? 1 : 0);" type="checkbox" />
+        <toolbarbutton anonid="find-match-diacritics" class="findbar-match-diacritics findbar-button tabbable"
+          data-l10n-id="findbar-match-diacritics" oncommand="_setDiacriticMatching(this.checked ? 1 : 0);" type="checkbox" />
         <toolbarbutton anonid="find-entire-word" class="findbar-entire-word findbar-button tabbable"
           data-l10n-id="findbar-entire-word" oncommand="toggleEntireWord(this.checked);" type="checkbox" />
         <label anonid="match-case-status" class="findbar-find-fast" />
+        <label anonid="match-diacritics-status" class="findbar-find-fast" />
         <label anonid="entire-word-status" class="findbar-find-fast" />
         <label anonid="found-matches" class="findbar-find-fast found-matches" hidden="true" />
         <image anonid="find-status-icon" class="findbar-find-fast find-status-icon" />
         <description anonid="find-status" control="findbar-textbox" class="findbar-find-fast findbar-find-status" />
       </hbox>
       <toolbarbutton anonid="find-closebutton" class="findbar-closebutton close-icon"
         data-l10n-id="findbar-find-button-close" oncommand="close();" />
     `);
@@ -385,16 +389,19 @@
           break;
         case "findbar.entireword":
           this._entireWord = prefsvc.getBoolPref(prefName);
           this.toggleEntireWord(this._entireWord, true);
           break;
         case "findbar.highlightAll":
           this.toggleHighlight(prefsvc.getBoolPref(prefName), true);
           break;
+        case "findbar.matchdiacritics":
+          this._setDiacriticMatching(prefsvc.getIntPref(prefName));
+          break;
         case "findbar.modalHighlight":
           this._useModalHighlight = prefsvc.getBoolPref(prefName);
           if (this.browser.finder) {
             this.browser.finder.onModalHighlightChange(this._useModalHighlight);
           }
           break;
       }
     }
@@ -594,16 +601,65 @@
       this._updateCaseSensitivity();
       this._findFailedString = null;
       this._find();
 
       this._dispatchFindEvent("casesensitivitychange");
     }
 
     /**
+     * Updates the diacritic-matching mode of the findbar and its UI.
+     *
+     * @param {String} [str] The string for which diacritic matching might be
+     *                       turned on. This is only used when diacritic
+     *                       matching is in auto mode, see
+     *                       `_shouldMatchDiacritics`. The default value for
+     *                       this parameter is the find-field value.
+     * @see _shouldMatchDiacritics.
+     */
+    _updateDiacriticMatching(str) {
+      let val = str || this._findField.value;
+
+      let matchDiacritics = this._shouldMatchDiacritics(val);
+      let checkbox = this.getElement("find-match-diacritics");
+      let statusLabel = this.getElement("match-diacritics-status");
+      checkbox.checked = matchDiacritics;
+
+      statusLabel.value = matchDiacritics ? this._matchDiacriticsStr : "";
+
+      // Show the checkbox on the full Find bar in non-auto mode.
+      // Show the label in all other cases.
+      let hideCheckbox =
+        this.findMode != this.FIND_NORMAL ||
+        (this._matchDiacritics != 0 && this._matchDiacritics != 1);
+      checkbox.hidden = hideCheckbox;
+      statusLabel.hidden = !hideCheckbox;
+
+      this.browser.finder.matchDiacritics = matchDiacritics;
+    }
+
+    /**
+     * Sets the findbar diacritic-matching mode
+     * @param {Number} diacriticMatching 0 - ignore diacritics,
+     *                                   1 - match diacritics,
+     *                                   2 - auto = match diacritics if the
+     *                                       matching string contains
+     *                                       diacritics.
+     * @see _shouldMatchDiacritics
+     */
+    _setDiacriticMatching(diacriticMatching) {
+      this._matchDiacritics = diacriticMatching;
+      this._updateDiacriticMatching();
+      this._findFailedString = null;
+      this._find();
+
+      this._dispatchFindEvent("diacriticmatchingchange");
+    }
+
+    /**
      * Updates the entire-word mode of the findbar and its UI.
      */
     _setEntireWord() {
       let entireWord = this._entireWord;
       let checkbox = this.getElement("find-entire-word");
       let statusLabel = this.getElement("entire-word-status");
       checkbox.checked = entireWord;
 
@@ -654,16 +710,17 @@
         var bundle = this.strBundle;
         this._notFoundStr = bundle.GetStringFromName("NotFound");
         this._wrappedToTopStr = bundle.GetStringFromName("WrappedToTop");
         this._wrappedToBottomStr = bundle.GetStringFromName("WrappedToBottom");
         this._normalFindStr = bundle.GetStringFromName("NormalFind");
         this._fastFindStr = bundle.GetStringFromName("FastFind");
         this._fastFindLinksStr = bundle.GetStringFromName("FastFindLinks");
         this._caseSensitiveStr = bundle.GetStringFromName("CaseSensitive");
+        this._matchDiacriticsStr = bundle.GetStringFromName("MatchDiacritics");
         this._entireWordStr = bundle.GetStringFromName("EntireWord");
       }
 
       this._findFailedString = null;
 
       this._updateFindUI();
       if (this.hidden) {
         this.removeAttribute("noanim");
@@ -778,16 +835,27 @@
       }
       if (this._typeAheadCaseSensitive == 1) {
         return true;
       }
 
       return str != str.toLowerCase();
     }
 
+    _shouldMatchDiacritics(str) {
+      if (this._matchDiacritics == 0) {
+        return false;
+      }
+      if (this._matchDiacritics == 1) {
+        return true;
+      }
+
+      return str != str.normalize("NFD");
+    }
+
     onMouseUp() {
       if (!this.hidden && this.findMode != this.FIND_NORMAL) {
         this.close();
       }
     }
 
     /**
      * We get a fake event object through an IPC message when FAYT is being used
@@ -879,16 +947,17 @@
         }
         node.hidden = showMinimalUI;
       }
       this.getElement("find-next").hidden = this.getElement(
         "find-previous"
       ).hidden = showMinimalUI;
       foundMatches.hidden = showMinimalUI || !foundMatches.value;
       this._updateCaseSensitivity();
+      this._updateDiacriticMatching();
       this._setEntireWord();
       this._setHighlightAll();
 
       if (showMinimalUI) {
         this._findField.classList.add("minimal");
       } else {
         this._findField.classList.remove("minimal");
       }
@@ -928,16 +997,17 @@
         // initial prefilling is ignored if it hasn't happened yet.
         if (this._startFindDeferred) {
           this._startFindDeferred.resolve();
           this._startFindDeferred = null;
         }
 
         this._enableFindButtons(val);
         this._updateCaseSensitivity(val);
+        this._updateDiacriticMatching(val);
         this._setEntireWord();
 
         this.browser.finder.fastFind(
           val,
           this.findMode == this.FIND_LINKS,
           this.findMode != this.FIND_NORMAL
         );
       }
@@ -1021,16 +1091,17 @@
       );
     }
 
     _dispatchFindEvent(type, findPrevious) {
       let event = document.createEvent("CustomEvent");
       event.initCustomEvent("find" + type, true, true, {
         query: this._findField.value,
         caseSensitive: !!this._typeAheadCaseSensitive,
+        matchDiacritics: !!this._matchDiacritics,
         entireWord: this._entireWord,
         highlightAll: this._highlightAll,
         findPrevious,
       });
       return this.dispatchEvent(event);
     }
 
     /**
--- a/toolkit/locales/en-US/chrome/global/findbar.properties
+++ b/toolkit/locales/en-US/chrome/global/findbar.properties
@@ -5,16 +5,17 @@
 # strings used by the Find bar, split from browser.properties
 NotFound=Phrase not found
 WrappedToTop=Reached end of page, continued from top
 WrappedToBottom=Reached top of page, continued from bottom
 NormalFind=Find in page
 FastFind=Quick find
 FastFindLinks=Quick find (links only)
 CaseSensitive=(Case sensitive)
+MatchDiacritics=(Matching diacritics)
 EntireWord=(Whole words only)
 # LOCALIZATION NOTE (FoundMatches): Semicolon-separated list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 is currently selected match and #2 the total amount of matches.
 FoundMatches=#1 of #2 match;#1 of #2 matches
 # LOCALIZATION NOTE (FoundMatchesCountLimit): Semicolon-separated list of plural
 # forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
--- a/toolkit/locales/en-US/toolkit/main-window/findbar.ftl
+++ b/toolkit/locales/en-US/toolkit/main-window/findbar.ftl
@@ -20,12 +20,17 @@ findbar-highlight-all2 =
     }
     .tooltiptext = Highlight all occurrences of the phrase
 
 findbar-case-sensitive =
     .label = Match Case
     .accesskey = C
     .tooltiptext = Search with case sensitivity
 
+findbar-match-diacritics =
+    .label = Match Diacritics
+    .accesskey = D
+    .tooltiptext = Distinguish between accented letters and their base letters (for example, when searching for "resume", "résumé" will not be matched)
+
 findbar-entire-word =
     .label = Whole Words
     .accesskey = W
     .tooltiptext = Search whole words only
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -165,16 +165,24 @@ Finder.prototype = {
   set caseSensitive(aSensitive) {
     if (this._fastFind.caseSensitive === aSensitive) {
       return;
     }
     this._fastFind.caseSensitive = aSensitive;
     this.iterator.reset();
   },
 
+  set matchDiacritics(aMatchDiacritics) {
+    if (this._fastFind.matchDiacritics === aMatchDiacritics) {
+      return;
+    }
+    this._fastFind.matchDiacritics = aMatchDiacritics;
+    this.iterator.reset();
+  },
+
   set entireWord(aEntireWord) {
     if (this._fastFind.entireWord === aEntireWord) {
       return;
     }
     this._fastFind.entireWord = aEntireWord;
     this.iterator.reset();
   },
 
@@ -336,16 +344,17 @@ Finder.prototype = {
   async updateHighlightAndMatchCount(aArgs) {
     this._lastFindResult = aArgs;
 
     if (
       !this.iterator.continueRunning({
         caseSensitive: this._fastFind.caseSensitive,
         entireWord: this._fastFind.entireWord,
         linksOnly: aArgs.linksOnly,
+        matchDiacritics: this._fastFind.matchDiacritics,
         word: aArgs.searchString,
         useSubFrames: aArgs.useSubFrames,
       })
     ) {
       this.iterator.stop();
     }
 
     let highlightPromise = this.highlighter.update(
@@ -584,16 +593,17 @@ Finder.prototype = {
     }
 
     this._currentFoundRange = this._fastFind.getFoundRange();
 
     let params = {
       caseSensitive: this._fastFind.caseSensitive,
       entireWord: this._fastFind.entireWord,
       linksOnly: aLinksOnly,
+      matchDiacritics: this._fastFind.matchDiacritics,
       word: aWord,
       useSubFrames: aUseSubFrames,
     };
     if (!this.iterator.continueRunning(params)) {
       this.iterator.stop();
     }
 
     await this.iterator.start(
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -279,16 +279,17 @@ FinderHighlighter.prototype = {
       let params = {
         allowDistance: 1,
         caseSensitive: this.finder._fastFind.caseSensitive,
         entireWord: this.finder._fastFind.entireWord,
         linksOnly,
         word,
         finder: this.finder,
         listener: this,
+        matchDiacritics: this.finder._fastFind.matchDiacritics,
         useCache: true,
         useSubFrames,
         window,
       };
       if (
         this.iterator.isAlreadyRunning(params) ||
         (this.useModal() &&
           this.iterator._areParamsEqual(params, dict.lastIteratorParams))
--- a/toolkit/modules/FinderIterator.jsm
+++ b/toolkit/modules/FinderIterator.jsm
@@ -83,32 +83,35 @@ FinderIterator.prototype = {
    *                                          hyperlink (used by QuickFind).
    *                                          Optional, defaults to `false`.
    * @param {Object}  options.listener        Listener object that implements the
    *                                          following callback functions:
    *                                           - onIteratorRangeFound({Range} range);
    *                                           - onIteratorReset();
    *                                           - onIteratorRestart({Object} iterParams);
    *                                           - onIteratorStart({Object} iterParams);
+   * @param {Boolean} options.matchDiacritics Whether to search in
+   *                                          diacritic-matching mode
    * @param {Boolean} [options.useCache]        Whether to allow results already
    *                                            present in the cache or demand fresh.
    *                                            Optional, defaults to `false`.
    * @param {Boolean} [options.useSubFrames]    Whether to iterate over subframes.
    *                                            Optional, defaults to `false`.
    * @param {String}  options.word              Word to search for
    * @return {Promise}
    */
   start({
     allowDistance,
     caseSensitive,
     entireWord,
     finder,
     limit,
     linksOnly,
     listener,
+    matchDiacritics,
     useCache,
     word,
     useSubFrames,
   }) {
     // Take care of default values for non-required options.
     if (typeof allowDistance != "number") {
       allowDistance = 0;
     }
@@ -127,16 +130,19 @@ FinderIterator.prototype = {
 
     // Validate the options.
     if (typeof caseSensitive != "boolean") {
       throw new Error("Missing required option 'caseSensitive'");
     }
     if (typeof entireWord != "boolean") {
       throw new Error("Missing required option 'entireWord'");
     }
+    if (typeof matchDiacritics != "boolean") {
+      throw new Error("Missing required option 'matchDiacritics'");
+    }
     if (!finder) {
       throw new Error("Missing required option 'finder'");
     }
     if (!word) {
       throw new Error("Missing required option 'word'");
     }
     if (typeof listener != "object" || !listener.onIteratorRangeFound) {
       throw new TypeError("Missing valid, required option 'listener'");
@@ -153,16 +159,17 @@ FinderIterator.prototype = {
 
     let window = finder._getWindow();
     let resolver;
     let promise = new Promise(resolve => (resolver = resolve));
     let iterParams = {
       caseSensitive,
       entireWord,
       linksOnly,
+      matchDiacritics,
       useCache,
       window,
       word,
       useSubFrames,
     };
 
     this._listeners.set(listener, { limit, onEnd: resolver });
 
@@ -299,32 +306,36 @@ FinderIterator.prototype = {
    * 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.caseSensitive Whether to search in case sensitive
    *                                         mode
    * @param {Boolean}  options.entireWord    Whether to search in entire-word mode
    * @param  {Boolean} options.linksOnly     Whether to search for the word to be
    *                                         present in links only
+   * @param {Boolean}  options.matchDiacritics Whether to search in
+   *                                           diacritic-matching mode
    * @param  {String}  options.word          The word being searched for
    * @param  (Boolean) options.useSubFrames  Whether to search subframes
    * @return {Boolean}
    */
   continueRunning({
     caseSensitive,
     entireWord,
     linksOnly,
+    matchDiacritics,
     word,
     useSubFrames,
   }) {
     return (
       this.running &&
       this._currentParams.caseSensitive === caseSensitive &&
       this._currentParams.entireWord === entireWord &&
       this._currentParams.linksOnly === linksOnly &&
+      this._currentParams.matchDiacritics === matchDiacritics &&
       this._currentParams.word == word &&
       this._currentParams.useSubFrames == useSubFrames
     );
   },
 
   /**
    * The default mode of operation of the iterator is to not accept duplicate
    * listeners, resolve the promise of the older listeners and replace it with
@@ -369,34 +380,38 @@ FinderIterator.prototype = {
    * Internal; check if an iteration request is available in the previous result
    * that we cached.
    *
    * @param  {Boolean} options.caseSensitive Whether to search in case sensitive
    *                                         mode
    * @param  {Boolean} options.entireWord    Whether to search in entire-word mode
    * @param  {Boolean} options.linksOnly     Whether to search for the word to be
    *                                         present in links only
+   * @param  {Boolean} options.matchDiacritics Whether to search in
+   *                                           diacritic-matching mode
    * @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({
     caseSensitive,
     entireWord,
     linksOnly,
+    matchDiacritics,
     useCache,
     word,
   }) {
     return !!(
       useCache &&
       this._areParamsEqual(this._previousParams, {
         caseSensitive,
         entireWord,
         linksOnly,
+        matchDiacritics,
         word,
       }) &&
       this._previousRanges.length
     );
   },
 
   /**
    * Internal; compare if two sets of iterator parameters are equivalent.
@@ -410,16 +425,17 @@ FinderIterator.prototype = {
    */
   _areParamsEqual(paramSet1, paramSet2, allowDistance = 0) {
     return (
       !!paramSet1 &&
       !!paramSet2 &&
       paramSet1.caseSensitive === paramSet2.caseSensitive &&
       paramSet1.entireWord === paramSet2.entireWord &&
       paramSet1.linksOnly === paramSet2.linksOnly &&
+      paramSet1.matchDiacritics === paramSet2.matchDiacritics &&
       paramSet1.window === paramSet2.window &&
       paramSet1.useSubFrames === paramSet2.useSubFrames &&
       NLP.levenshtein(paramSet1.word, paramSet2.word) <= allowDistance
     );
   },
 
   /**
    * Internal; iterate over a predefined set of ranges that have been collected
@@ -619,21 +635,26 @@ FinderIterator.prototype = {
   /**
    * Internal; basic wrapper around nsIFind that provides a generator yielding
    * a range each time an occurence of `word` string is found.
    *
    * @param {Boolean}      options.caseSensitive Whether to search in case
    *                                             sensitive mode
    * @param {Boolean}      options.entireWord    Whether to search in entire-word
    *                                             mode
+   * @param {Boolean}      options.matchDiacritics Whether to search in
+   *                                               diacritic-matching mode
    * @param {String}       options.word          The word to search for
    * @param {nsIDOMWindow} window                The window to search in
    * @yield {Range}
    */
-  *_iterateDocument({ caseSensitive, entireWord, word }, window) {
+  *_iterateDocument(
+    { caseSensitive, entireWord, matchDiacritics, word },
+    window
+  ) {
     let doc = window.document;
     let body = doc.body || doc.documentElement;
 
     if (!body) {
       return;
     }
 
     let searchRange = doc.createRange();
@@ -647,16 +668,17 @@ FinderIterator.prototype = {
 
     let retRange = null;
 
     let nsIFind = Cc["@mozilla.org/embedcomp/rangefind;1"]
       .createInstance()
       .QueryInterface(Ci.nsIFind);
     nsIFind.caseSensitive = caseSensitive;
     nsIFind.entireWord = entireWord;
+    nsIFind.matchDiacritics = matchDiacritics;
 
     while ((retRange = nsIFind.Find(word, searchRange, startPt, endPt))) {
       yield retRange;
       startPt = retRange.cloneRange();
       startPt.collapse(false);
     }
   },
 
--- a/toolkit/modules/FinderParent.jsm
+++ b/toolkit/modules/FinderParent.jsm
@@ -212,16 +212,22 @@ FinderParent.prototype = {
   },
 
   set entireWord(aEntireWord) {
     this.sendMessageToAllContexts("Finder:EntireWord", {
       entireWord: aEntireWord,
     });
   },
 
+  set matchDiacritics(aMatchDiacritics) {
+    this.sendMessageToAllContexts("Finder:MatchDiacritics", {
+      matchDiacritics: aMatchDiacritics,
+    });
+  },
+
   async setSearchStringToSelection() {
     return this.setToSelection("Finder:SetSearchStringToSelection", false);
   },
 
   async getInitialSelection() {
     return this.setToSelection("Finder:GetInitialSelection", true);
   },
 
--- a/toolkit/modules/tests/xpcshell/test_FinderIterator.js
+++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
@@ -53,16 +53,17 @@ add_task(async function test_start() {
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
         Assert.equal(range.toString(), findText, "Text content should match");
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   Assert.equal(rangeCount, count, "Amount of ranges yielded should match!");
   Assert.ok(!finderIterator.running, "Running state should match");
   Assert.equal(
     finderIterator._previousRanges.length,
     rangeCount,
@@ -83,16 +84,17 @@ add_task(async function test_subframes()
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
         Assert.equal(range.toString(), findText, "Text content should match");
       },
     },
+    matchDiacritics: false,
     word: findText,
     useSubFrames: true,
   });
 
   Assert.equal(rangeCount, count, "Amount of ranges yielded should match!");
   Assert.ok(!finderIterator.running, "Running state should match");
   Assert.equal(
     finderIterator._previousRanges.length,
@@ -114,16 +116,17 @@ add_task(async function test_valid_argum
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   let params = finderIterator._previousParams;
   Assert.ok(!params.linksOnly, "Default for linksOnly is false");
   Assert.ok(!params.useCache, "Default for useCache is false");
   Assert.equal(params.word, findText, "Words should match");
 
@@ -132,32 +135,51 @@ add_task(async function test_valid_argum
     () =>
       finderIterator.start({
         entireWord: false,
         listener: {
           onIteratorRangeFound(range) {
             ++count;
           },
         },
+        matchDiacritics: false,
         word: findText,
       }),
     /Missing required option 'caseSensitive'/,
     "Should throw when missing an argument"
   );
   finderIterator.reset();
 
   Assert.throws(
     () =>
       finderIterator.start({
         caseSensitive: false,
+        entireWord: false,
+        listener: {
+          onIteratorRangeFound(range) {
+            ++count;
+          },
+        },
+        word: findText,
+      }),
+    /Missing required option 'matchDiacritics'/,
+    "Should throw when missing an argument"
+  );
+  finderIterator.reset();
+
+  Assert.throws(
+    () =>
+      finderIterator.start({
+        caseSensitive: false,
         listener: {
           onIteratorRangeFound(range) {
             ++count;
           },
         },
+        matchDiacritics: false,
         word: findText,
       }),
     /Missing required option 'entireWord'/,
     "Should throw when missing an argument"
   );
   finderIterator.reset();
 
   Assert.throws(
@@ -165,29 +187,31 @@ add_task(async function test_valid_argum
       finderIterator.start({
         caseSensitive: false,
         entireWord: false,
         listener: {
           onIteratorRangeFound(range) {
             ++count;
           },
         },
+        matchDiacritics: false,
         word: findText,
       }),
     /Missing required option 'finder'/,
     "Should throw when missing an argument"
   );
   finderIterator.reset();
 
   Assert.throws(
     () =>
       finderIterator.start({
         caseSensitive: true,
         entireWord: false,
         finder: gMockFinder,
+        matchDiacritics: false,
         word: findText,
       }),
     /Missing valid, required option 'listener'/,
     "Should throw when missing an argument"
   );
   finderIterator.reset();
 
   Assert.throws(
@@ -196,16 +220,17 @@ add_task(async function test_valid_argum
         caseSensitive: false,
         entireWord: true,
         finder: gMockFinder,
         listener: {
           onIteratorRangeFound(range) {
             ++count;
           },
         },
+        matchDiacritics: false,
       }),
     /Missing required option 'word'/,
     "Should throw when missing an argument"
   );
   finderIterator.reset();
 
   Assert.equal(count, 0, "No ranges should've been counted");
 });
@@ -220,16 +245,17 @@ add_task(async function test_stop() {
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   finderIterator.stop();
 
   await whenDone;
 
   Assert.equal(count, 0, "Number of ranges should be 0");
@@ -247,16 +273,17 @@ add_task(async function test_reset() {
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   Assert.ok(finderIterator.running, "Yup, running we are");
   Assert.equal(count, 0, "Number of ranges should match 0");
   Assert.equal(
     finderIterator.ranges.length,
     0,
@@ -289,32 +316,34 @@ add_task(async function test_parallel_st
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
-  await new Promise(resolve => gMockWindow.setTimeout(resolve, 120));
+  await new Promise(resolve => gMockWindow.setTimeout(resolve, 100));
   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;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   // Let the iterator run for a little while longer before we assert the world.
   await new Promise(resolve => gMockWindow.setTimeout(resolve, 10));
   finderIterator.stop();
 
   Assert.ok(!finderIterator.running, "Stop means stop");
@@ -355,43 +384,46 @@ add_task(async function test_allowDistan
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count;
       },
     },
+    matchDiacritics: false,
     word: findText,
   });
 
   let count2 = 0;
   let whenDone2 = finderIterator.start({
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count2;
       },
     },
+    matchDiacritics: false,
     word: "gu",
   });
 
   let count3 = 0;
   let whenDone3 = finderIterator.start({
     allowDistance: 1,
     caseSensitive: false,
     entireWord: false,
     finder: gMockFinder,
     listener: {
       onIteratorRangeFound(range) {
         ++count3;
       },
     },
+    matchDiacritics: false,
     word: "gu",
   });
 
   await Promise.all([whenDone, whenDone2, whenDone3]);
 
   Assert.equal(
     count,
     rangeCount,