tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mozInlineSpellChecker.cpp (76667B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set sw=2 ts=2 sts=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /**
      8 * This class is called by the editor to handle spellchecking after various
      9 * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
     10 * when the text is changed.
     11 *
     12 * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
     13 * notifications to be flushed when we are called from the editor. This is
     14 * because the call might originate from a frame, and flushing the
     15 * notifications might cause that frame to be deleted.
     16 *
     17 * We post an event and do all of the spellchecking in that event handler.
     18 * We store all DOM pointers in ranges because they are kept up-to-date with
     19 * DOM changes that may have happened while the event was on the queue.
     20 *
     21 * We also allow the spellcheck to be suspended and resumed later. This makes
     22 * large pastes or initializations with a lot of text not hang the browser UI.
     23 *
     24 * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
     25 * true when we get any change, and false once there is no possibility
     26 * something changed that we need to check on navigation. Navigation events
     27 * tend to be a little tricky because we want to check the current word on
     28 * exit if something has changed. If we navigate inside the word, we don't want
     29 * to do anything. As a result, this flag is cleared in FinishNavigationEvent
     30 * when we know that we are checking as a result of navigation.
     31 */
     32 
     33 #include "mozInlineSpellChecker.h"
     34 
     35 #include "mozilla/Assertions.h"
     36 #include "mozilla/Attributes.h"
     37 #include "mozilla/EditAction.h"
     38 #include "mozilla/EditorBase.h"
     39 #include "mozilla/EditorDOMPoint.h"
     40 #include "mozilla/EditorSpellCheck.h"
     41 #include "mozilla/EventListenerManager.h"
     42 #include "mozilla/HTMLEditor.h"
     43 #include "mozilla/IntegerRange.h"
     44 #include "mozilla/Logging.h"
     45 #include "mozilla/RangeUtils.h"
     46 #include "mozilla/Services.h"
     47 #include "mozilla/StaticPrefs_extensions.h"
     48 #include "mozilla/TextEvents.h"
     49 #include "mozilla/dom/Event.h"
     50 #include "mozilla/dom/KeyboardEvent.h"
     51 #include "mozilla/dom/KeyboardEventBinding.h"
     52 #include "mozilla/dom/MouseEvent.h"
     53 #include "mozilla/dom/Selection.h"
     54 #include "mozInlineSpellWordUtil.h"
     55 #include "nsCOMPtr.h"
     56 #include "nsCRT.h"
     57 #include "nsGenericHTMLElement.h"
     58 #include "nsRange.h"
     59 #include "nsIPrefBranch.h"
     60 #include "nsIPrefService.h"
     61 #include "nsIRunnable.h"
     62 #include "nsServiceManagerUtils.h"
     63 #include "nsString.h"
     64 #include "nsThreadUtils.h"
     65 #include "nsUnicharUtils.h"
     66 #include "nsIContent.h"
     67 #include "nsIContentInlines.h"
     68 #include "nsRange.h"
     69 #include "nsContentUtils.h"
     70 #include "nsIObserverService.h"
     71 #include "prtime.h"
     72 
     73 using mozilla::LogLevel;
     74 using namespace mozilla;
     75 using namespace mozilla::dom;
     76 using namespace mozilla::ipc;
     77 
     78 // the number of milliseconds that we will take at once to do spellchecking
     79 #define INLINESPELL_CHECK_TIMEOUT 1
     80 
     81 // The number of words to check before we look at the time to see if
     82 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
     83 // stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
     84 // be too short to a low-end machine.
     85 #define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
     86 
     87 // The maximum number of words to check word via IPC.
     88 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
     89 
     90 // These notifications are broadcast when spell check starts and ends.  STARTED
     91 // must always be followed by ENDED.
     92 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
     93 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
     94 
     95 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
     96 
     97 static const PRTime kMaxSpellCheckTimeInUsec =
     98    INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
     99 
    100 mozInlineSpellStatus::mozInlineSpellStatus(
    101    mozInlineSpellChecker* aSpellChecker, const Operation aOp,
    102    RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
    103    RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
    104    const int32_t aNewNavigationPositionOffset)
    105    : mSpellChecker(aSpellChecker),
    106      mRange(std::move(aRange)),
    107      mOp(aOp),
    108      mCreatedRange(std::move(aCreatedRange)),
    109      mAnchorRange(std::move(aAnchorRange)),
    110      mForceNavigationWordCheck(aForceNavigationWordCheck),
    111      mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}
    112 
    113 // mozInlineSpellStatus::CreateForEditorChange
    114 //
    115 //    This is the most complicated case. For changes, we need to compute the
    116 //    range of stuff that changed based on the old and new caret positions,
    117 //    as well as use a range possibly provided by the editor (start and end,
    118 //    which are usually nullptr) to get a range with the union of these.
    119 
    120 // static
    121 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
    122 mozInlineSpellStatus::CreateForEditorChange(
    123    mozInlineSpellChecker& aSpellChecker, const EditSubAction aEditSubAction,
    124    nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
    125    uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
    126    nsINode* aEndNode, uint32_t aEndOffset) {
    127  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
    128 
    129  if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
    130    return Err(NS_ERROR_FAILURE);
    131  }
    132 
    133  bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
    134  if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
    135    // IME may remove the previous node if it cancels composition when
    136    // there is no text around the composition.
    137    deleted = !aPreviousNode->IsInComposedDoc();
    138  }
    139 
    140  // save the anchor point as a range so we can find the current word later
    141  RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
    142      aAnchorNode, aAnchorOffset);
    143  if (NS_WARN_IF(!anchorRange)) {
    144    return Err(NS_ERROR_FAILURE);
    145  }
    146 
    147  // Deletes are easy, the range is just the current anchor. We set the range
    148  // to check to be empty, FinishInitOnEvent will fill in the range to be
    149  // the current word.
    150  RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);
    151 
    152  // On insert save this range: DoSpellCheck optimizes things in this range.
    153  // Otherwise, just leave this nullptr.
    154  RefPtr<nsRange> createdRange =
    155      (aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;
    156 
    157  UniquePtr<mozInlineSpellStatus> status{
    158      /* The constructor is `private`, hence the explicit allocation. */
    159      new mozInlineSpellStatus{&aSpellChecker,
    160                               deleted ? eOpChangeDelete : eOpChange,
    161                               std::move(range), std::move(createdRange),
    162                               std::move(anchorRange), false, 0}};
    163  if (deleted) {
    164    return status;
    165  }
    166 
    167  // ...we need to put the start and end in the correct order
    168  ErrorResult errorResult;
    169  int16_t cmpResult = status->mAnchorRange->ComparePoint(
    170      *aPreviousNode, aPreviousOffset, errorResult);
    171  if (NS_WARN_IF(errorResult.Failed())) {
    172    return Err(errorResult.StealNSResult());
    173  }
    174  nsresult rv;
    175  if (cmpResult < 0) {
    176    // previous anchor node is before the current anchor
    177    rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
    178                                        aAnchorNode, aAnchorOffset);
    179    if (NS_WARN_IF(NS_FAILED(rv))) {
    180      return Err(rv);
    181    }
    182  } else {
    183    // previous anchor node is after (or the same as) the current anchor
    184    rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
    185                                        aPreviousNode, aPreviousOffset);
    186    if (NS_WARN_IF(NS_FAILED(rv))) {
    187      return Err(rv);
    188    }
    189  }
    190 
    191  // if we were given a range, we need to expand our range to encompass it
    192  if (aStartNode && aEndNode) {
    193    cmpResult =
    194        status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
    195    if (NS_WARN_IF(errorResult.Failed())) {
    196      return Err(errorResult.StealNSResult());
    197    }
    198    if (cmpResult < 0) {  // given range starts before
    199      rv = status->mRange->SetStart(aStartNode, aStartOffset);
    200      if (NS_WARN_IF(NS_FAILED(rv))) {
    201        return Err(rv);
    202      }
    203    }
    204 
    205    cmpResult =
    206        status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
    207    if (NS_WARN_IF(errorResult.Failed())) {
    208      return Err(errorResult.StealNSResult());
    209    }
    210    if (cmpResult > 0) {  // given range ends after
    211      rv = status->mRange->SetEnd(aEndNode, aEndOffset);
    212      if (NS_WARN_IF(NS_FAILED(rv))) {
    213        return Err(rv);
    214      }
    215    }
    216  }
    217 
    218  return status;
    219 }
    220 
    221 // mozInlineSpellStatus::CreateForNavigation
    222 //
    223 //    For navigation events, we just need to store the new and old positions.
    224 //
    225 //    In some cases, we detect that we shouldn't check. If this event should
    226 //    not be processed, *aContinue will be false.
    227 
    228 // static
    229 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
    230 mozInlineSpellStatus::CreateForNavigation(
    231    mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
    232    int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
    233    uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
    234    uint32_t aNewAnchorOffset, bool* aContinue) {
    235  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
    236 
    237  RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
    238      aNewAnchorNode, aNewAnchorOffset);
    239  if (NS_WARN_IF(!anchorRange)) {
    240    return Err(NS_ERROR_FAILURE);
    241  }
    242 
    243  UniquePtr<mozInlineSpellStatus> status{
    244      /* The constructor is `private`, hence the explicit allocation. */
    245      new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
    246                               std::move(anchorRange), aForceCheck,
    247                               aNewPositionOffset}};
    248 
    249  // get the root node for checking
    250  EditorBase* editorBase = status->mSpellChecker->mEditorBase;
    251  if (NS_WARN_IF(!editorBase)) {
    252    return Err(NS_ERROR_FAILURE);
    253  }
    254  Element* root = editorBase->GetRoot();
    255  if (NS_WARN_IF(!root)) {
    256    return Err(NS_ERROR_FAILURE);
    257  }
    258  // the anchor node might not be in the DOM anymore, check
    259  if (root && aOldAnchorNode &&
    260      !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
    261    *aContinue = false;
    262    return status;
    263  }
    264 
    265  status->mOldNavigationAnchorRange =
    266      mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
    267                                                     aOldAnchorOffset);
    268  if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
    269    return Err(NS_ERROR_FAILURE);
    270  }
    271 
    272  *aContinue = true;
    273  return status;
    274 }
    275 
    276 // mozInlineSpellStatus::CreateForSelection
    277 //
    278 //    It is easy for selections since we always re-check the spellcheck
    279 //    selection.
    280 
    281 // static
    282 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
    283    mozInlineSpellChecker& aSpellChecker) {
    284  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
    285 
    286  UniquePtr<mozInlineSpellStatus> status{
    287      /* The constructor is `private`, hence the explicit allocation. */
    288      new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
    289                               nullptr, false, 0}};
    290  return status;
    291 }
    292 
    293 // mozInlineSpellStatus::CreateForRange
    294 //
    295 //    Called to cause the spellcheck of the given range. This will look like
    296 //    a change operation over the given range.
    297 
    298 // static
    299 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
    300    mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
    301  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
    302          ("%s: range=%p", __FUNCTION__, aRange));
    303 
    304  UniquePtr<mozInlineSpellStatus> status{
    305      /* The constructor is `private`, hence the explicit allocation. */
    306      new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
    307                               nullptr, false, 0}};
    308 
    309  status->mRange = aRange;
    310  return status;
    311 }
    312 
    313 // mozInlineSpellStatus::FinishInitOnEvent
    314 //
    315 //    Called when the event is triggered to complete initialization that
    316 //    might require the WordUtil. This calls to the operation-specific
    317 //    initializer, and also sets the range to be the entire element if it
    318 //    is nullptr.
    319 //
    320 //    Watch out: the range might still be nullptr if there is nothing to do,
    321 //    the caller will have to check for this.
    322 
    323 nsresult mozInlineSpellStatus::FinishInitOnEvent(
    324    mozInlineSpellWordUtil& aWordUtil) {
    325  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
    326          ("%s: mRange=%p", __FUNCTION__, mRange.get()));
    327 
    328  nsresult rv;
    329  if (!mRange) {
    330    rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
    331                                            getter_AddRefs(mRange));
    332    NS_ENSURE_SUCCESS(rv, rv);
    333  }
    334 
    335  switch (mOp) {
    336    case eOpChange:
    337      if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
    338      break;
    339    case eOpChangeDelete:
    340      if (mAnchorRange) {
    341        rv = FillNoCheckRangeFromAnchor(aWordUtil);
    342        NS_ENSURE_SUCCESS(rv, rv);
    343      }
    344      // Delete events will have no range for the changed text (because it was
    345      // deleted), and CreateForEditorChange will set it to nullptr. Here, we
    346      // select the entire word to cause any underlining to be removed.
    347      mRange = mNoCheckRange;
    348      break;
    349    case eOpNavigation:
    350      return FinishNavigationEvent(aWordUtil);
    351    case eOpSelection:
    352      // this gets special handling in ResumeCheck
    353      break;
    354    case eOpResume:
    355      // everything should be initialized already in this case
    356      break;
    357    default:
    358      MOZ_ASSERT_UNREACHABLE("Bad operation");
    359      return NS_ERROR_NOT_INITIALIZED;
    360  }
    361  return NS_OK;
    362 }
    363 
    364 // mozInlineSpellStatus::FinishNavigationEvent
    365 //
    366 //    This verifies that we need to check the word at the previous caret
    367 //    position. Now that we have the word util, we can find the word belonging
    368 //    to the previous caret position. If the new position is inside that word,
    369 //    we don't want to do anything. In this case, we'll nullptr out mRange so
    370 //    that the caller will know not to continue.
    371 //
    372 //    Notice that we don't set mNoCheckRange. We check here whether the cursor
    373 //    is in the word that needs checking, so it isn't necessary. Plus, the
    374 //    spellchecker isn't guaranteed to only check the given word, and it could
    375 //    remove the underline from the new word under the cursor.
    376 
    377 nsresult mozInlineSpellStatus::FinishNavigationEvent(
    378    mozInlineSpellWordUtil& aWordUtil) {
    379  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
    380 
    381  RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
    382  if (!editorBase) {
    383    return NS_ERROR_FAILURE;  // editor is gone
    384  }
    385 
    386  MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");
    387 
    388  if (!mOldNavigationAnchorRange->IsPositioned()) {
    389    return NS_ERROR_NOT_INITIALIZED;
    390  }
    391 
    392  // get the DOM position of the old caret, the range should be collapsed
    393  nsCOMPtr<nsINode> oldAnchorNode =
    394      mOldNavigationAnchorRange->GetStartContainer();
    395  uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
    396 
    397  // find the word on the old caret position, this is the one that we MAY need
    398  // to check
    399  RefPtr<nsRange> oldWord;
    400  nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
    401                                          static_cast<int32_t>(oldAnchorOffset),
    402                                          getter_AddRefs(oldWord));
    403  NS_ENSURE_SUCCESS(rv, rv);
    404 
    405  // aWordUtil.GetRangeForWord flushes pending notifications, check editor
    406  // again.
    407  if (!mSpellChecker->mEditorBase) {
    408    return NS_ERROR_FAILURE;  // editor is gone
    409  }
    410 
    411  // get the DOM position of the new caret, the range should be collapsed
    412  nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
    413  uint32_t newAnchorOffset = mAnchorRange->StartOffset();
    414 
    415  // see if the new cursor position is in the word of the old cursor position
    416  bool isInRange = false;
    417  if (!mForceNavigationWordCheck) {
    418    ErrorResult err;
    419    isInRange = oldWord->IsPointInRange(
    420        *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
    421    if (NS_WARN_IF(err.Failed())) {
    422      return err.StealNSResult();
    423    }
    424  }
    425 
    426  if (isInRange) {
    427    // caller should give up
    428    mRange = nullptr;
    429  } else {
    430    // check the old word
    431    mRange = oldWord;
    432 
    433    // Once we've spellchecked the current word, we don't need to spellcheck
    434    // for any more navigation events.
    435    mSpellChecker->mNeedsCheckAfterNavigation = false;
    436  }
    437  return NS_OK;
    438 }
    439 
    440 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
    441 //
    442 //    Given the mAnchorRange object, computes the range of the word it is on
    443 //    (if any) and fills that range into mNoCheckRange. This is used for
    444 //    change and navigation events to know which word we should skip spell
    445 //    checking on
    446 
    447 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
    448    mozInlineSpellWordUtil& aWordUtil) {
    449  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
    450 
    451  if (!mAnchorRange->IsPositioned()) {
    452    return NS_ERROR_NOT_INITIALIZED;
    453  }
    454  nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
    455  uint32_t anchorOffset = mAnchorRange->StartOffset();
    456  return aWordUtil.GetRangeForWord(anchorNode,
    457                                   static_cast<int32_t>(anchorOffset),
    458                                   getter_AddRefs(mNoCheckRange));
    459 }
    460 
    461 // mozInlineSpellStatus::GetDocument
    462 //
    463 //    Returns the Document object for the document for the
    464 //    current spellchecker.
    465 
    466 Document* mozInlineSpellStatus::GetDocument() const {
    467  if (!mSpellChecker->mEditorBase) {
    468    return nullptr;
    469  }
    470 
    471  return mSpellChecker->mEditorBase->GetDocument();
    472 }
    473 
    474 // mozInlineSpellStatus::PositionToCollapsedRange
    475 //
    476 //    Converts a given DOM position to a collapsed range covering that
    477 //    position. We use ranges to store DOM positions becuase they stay
    478 //    updated as the DOM is changed.
    479 
    480 // static
    481 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
    482    nsINode* aNode, uint32_t aOffset) {
    483  if (NS_WARN_IF(!aNode)) {
    484    return nullptr;
    485  }
    486  IgnoredErrorResult ignoredError;
    487  RefPtr<nsRange> range =
    488      nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
    489  NS_WARNING_ASSERTION(!ignoredError.Failed(),
    490                       "Creating collapsed range failed");
    491  return range.forget();
    492 }
    493 
    494 // mozInlineSpellResume
    495 
    496 class mozInlineSpellResume : public Runnable {
    497 public:
    498  mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
    499                       uint32_t aDisabledAsyncToken)
    500      : Runnable("mozInlineSpellResume"),
    501        mDisabledAsyncToken(aDisabledAsyncToken),
    502        mStatus(std::move(aStatus)) {}
    503 
    504  nsresult Post() {
    505    nsCOMPtr<nsIRunnable> runnable(this);
    506    return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
    507                                           EventQueuePriority::Idle);
    508  }
    509 
    510  NS_IMETHOD Run() override {
    511    // Discard the resumption if the spell checker was disabled after the
    512    // resumption was scheduled.
    513    if (mDisabledAsyncToken ==
    514        mStatus->mSpellChecker->GetDisabledAsyncToken()) {
    515      mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
    516    }
    517    return NS_OK;
    518  }
    519 
    520 private:
    521  uint32_t mDisabledAsyncToken;
    522  UniquePtr<mozInlineSpellStatus> mStatus;
    523 };
    524 
    525 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
    526 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
    527  ~InitEditorSpellCheckCallback() {}
    528 
    529 public:
    530  NS_DECL_ISUPPORTS
    531 
    532  explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
    533      : mSpellChecker(aSpellChecker) {}
    534 
    535  NS_IMETHOD EditorSpellCheckDone() override {
    536    return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
    537  }
    538 
    539  void Cancel() { mSpellChecker = nullptr; }
    540 
    541 private:
    542  RefPtr<mozInlineSpellChecker> mSpellChecker;
    543 };
    544 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
    545 
    546 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
    547  NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
    548  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
    549  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
    550  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
    551  NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
    552 NS_INTERFACE_MAP_END
    553 
    554 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
    555 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
    556 
    557 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
    558                              mCurrentSelectionAnchorNode)
    559 
    560 mozInlineSpellChecker::SpellCheckingState
    561    mozInlineSpellChecker::gCanEnableSpellChecking =
    562        mozInlineSpellChecker::SpellCheck_Uninitialized;
    563 
    564 mozInlineSpellChecker::mozInlineSpellChecker()
    565    : mNumWordsInSpellSelection(0),
    566      mMaxNumWordsInSpellSelection(
    567          StaticPrefs::extensions_spellcheck_inline_max_misspellings()),
    568      mNumPendingSpellChecks(0),
    569      mNumPendingUpdateCurrentDictionary(0),
    570      mDisabledAsyncToken(0),
    571      mNeedsCheckAfterNavigation(false),
    572      mFullSpellCheckScheduled(false),
    573      mIsListeningToEditSubActions(false) {}
    574 
    575 mozInlineSpellChecker::~mozInlineSpellChecker() {}
    576 
    577 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
    578  return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
    579 }
    580 
    581 NS_IMETHODIMP
    582 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
    583  *aSpellCheck = mSpellCheck;
    584  NS_IF_ADDREF(*aSpellCheck);
    585  return NS_OK;
    586 }
    587 
    588 NS_IMETHODIMP
    589 mozInlineSpellChecker::Init(nsIEditor* aEditor) {
    590  mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
    591  return NS_OK;
    592 }
    593 
    594 // mozInlineSpellChecker::Cleanup
    595 //
    596 //    Called by the editor when the editor is going away. This is important
    597 //    because we remove listeners. We do NOT clean up anything else in this
    598 //    function, because it can get called while DoSpellCheck is running!
    599 //
    600 //    Getting the style information there can cause DOM notifications to be
    601 //    flushed, which can cause editors to go away which will bring us here.
    602 //    We can not do anything that will cause DoSpellCheck to freak out.
    603 
    604 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
    605 mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
    606  mNumWordsInSpellSelection = 0;
    607  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
    608  nsresult rv = NS_OK;
    609  if (!spellCheckSelection) {
    610    // Ensure we still unregister event listeners (but return a failure code)
    611    UnregisterEventListeners();
    612    rv = NS_ERROR_FAILURE;
    613  } else {
    614    if (!aDestroyingFrames) {
    615      spellCheckSelection->RemoveAllRanges(IgnoreErrors());
    616    }
    617 
    618    rv = UnregisterEventListeners();
    619  }
    620 
    621  // Notify ENDED observers now.  If we wait to notify as we normally do when
    622  // these async operations finish, then in the meantime the editor may create
    623  // another inline spell checker and cause more STARTED and ENDED
    624  // notifications to be broadcast.  Interleaved notifications for the same
    625  // editor but different inline spell checkers could easily confuse
    626  // observers.  They may receive two consecutive STARTED notifications for
    627  // example, which we guarantee will not happen.
    628 
    629  RefPtr<EditorBase> editorBase = std::move(mEditorBase);
    630  if (mPendingSpellCheck) {
    631    // Cancel the pending editor spell checker initialization.
    632    mPendingSpellCheck = nullptr;
    633    mPendingInitEditorSpellCheckCallback->Cancel();
    634    mPendingInitEditorSpellCheckCallback = nullptr;
    635    ChangeNumPendingSpellChecks(-1, editorBase);
    636  }
    637 
    638  // Increment this token so that pending UpdateCurrentDictionary calls and
    639  // scheduled spell checks are discarded when they finish.
    640  mDisabledAsyncToken++;
    641 
    642  if (mNumPendingUpdateCurrentDictionary > 0) {
    643    // Account for pending UpdateCurrentDictionary calls.
    644    ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
    645                                editorBase);
    646    mNumPendingUpdateCurrentDictionary = 0;
    647  }
    648  if (mNumPendingSpellChecks > 0) {
    649    // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
    650    // pending scheduled spell checks.
    651    ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
    652  }
    653 
    654  mFullSpellCheckScheduled = false;
    655 
    656  return rv;
    657 }
    658 
    659 // mozInlineSpellChecker::CanEnableInlineSpellChecking
    660 //
    661 //    This function can be called to see if it seems likely that we can enable
    662 //    spellchecking before actually creating the InlineSpellChecking objects.
    663 //
    664 //    The problem is that we can't get the dictionary list without actually
    665 //    creating a whole bunch of spellchecking objects. This function tries to
    666 //    do that and caches the result so we don't have to keep allocating those
    667 //    objects if there are no dictionaries or spellchecking.
    668 //
    669 //    Whenever dictionaries are added or removed at runtime, this value must be
    670 //    updated before an observer notification is sent out about the change, to
    671 //    avoid editors getting a wrong cached result.
    672 
    673 bool  // static
    674 mozInlineSpellChecker::CanEnableInlineSpellChecking() {
    675  if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
    676    gCanEnableSpellChecking = SpellCheck_NotAvailable;
    677 
    678    nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
    679 
    680    bool canSpellCheck = false;
    681    nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
    682    NS_ENSURE_SUCCESS(rv, false);
    683 
    684    if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
    685  }
    686  return (gCanEnableSpellChecking == SpellCheck_Available);
    687 }
    688 
    689 void  // static
    690 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
    691  gCanEnableSpellChecking = SpellCheck_Uninitialized;
    692 }
    693 
    694 // mozInlineSpellChecker::RegisterEventListeners
    695 //
    696 //    The inline spell checker listens to mouse events and keyboard navigation
    697 //    events.
    698 
    699 nsresult mozInlineSpellChecker::RegisterEventListeners() {
    700  if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
    701    return NS_ERROR_FAILURE;
    702  }
    703 
    704  StartToListenToEditSubActions();
    705 
    706  RefPtr<Document> doc = mEditorBase->GetDocument();
    707  if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
    708    return NS_ERROR_FAILURE;
    709  }
    710  EventListenerManager* eventListenerManager =
    711      doc->GetOrCreateListenerManager();
    712  if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
    713    return NS_ERROR_FAILURE;
    714  }
    715  eventListenerManager->AddEventListenerByType(
    716      this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
    717  eventListenerManager->AddEventListenerByType(
    718      this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
    719  eventListenerManager->AddEventListenerByType(
    720      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
    721  return NS_OK;
    722 }
    723 
    724 // mozInlineSpellChecker::UnregisterEventListeners
    725 
    726 nsresult mozInlineSpellChecker::UnregisterEventListeners() {
    727  if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
    728    return NS_ERROR_FAILURE;
    729  }
    730 
    731  EndListeningToEditSubActions();
    732 
    733  RefPtr<Document> doc = mEditorBase->GetDocument();
    734  if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
    735    return NS_ERROR_FAILURE;
    736  }
    737  EventListenerManager* eventListenerManager =
    738      doc->GetOrCreateListenerManager();
    739  if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
    740    return NS_ERROR_FAILURE;
    741  }
    742  eventListenerManager->RemoveEventListenerByType(
    743      this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
    744  eventListenerManager->RemoveEventListenerByType(
    745      this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
    746  eventListenerManager->RemoveEventListenerByType(
    747      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
    748  return NS_OK;
    749 }
    750 
    751 // mozInlineSpellChecker::GetEnableRealTimeSpell
    752 
    753 NS_IMETHODIMP
    754 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
    755  NS_ENSURE_ARG_POINTER(aEnabled);
    756  *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
    757  return NS_OK;
    758 }
    759 
    760 // mozInlineSpellChecker::SetEnableRealTimeSpell
    761 
    762 NS_IMETHODIMP
    763 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
    764  if (!aEnabled) {
    765    mSpellCheck = nullptr;
    766    return Cleanup(false);
    767  }
    768 
    769  if (mSpellCheck) {
    770    // spellcheck the current contents. SpellCheckRange doesn't supply a created
    771    // range to DoSpellCheck, which in our case is the entire range. But this
    772    // optimization doesn't matter because there is nothing in the spellcheck
    773    // selection when starting, which triggers a better optimization.
    774    return SpellCheckRange(nullptr);
    775  }
    776 
    777  if (mPendingSpellCheck) {
    778    // The editor spell checker is already being initialized.
    779    return NS_OK;
    780  }
    781 
    782  mPendingSpellCheck = new EditorSpellCheck();
    783  mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
    784 
    785  mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
    786  nsresult rv = mPendingSpellCheck->InitSpellChecker(
    787      mEditorBase, false, mPendingInitEditorSpellCheckCallback);
    788  if (NS_FAILED(rv)) {
    789    mPendingSpellCheck = nullptr;
    790    mPendingInitEditorSpellCheckCallback = nullptr;
    791    NS_ENSURE_SUCCESS(rv, rv);
    792  }
    793 
    794  ChangeNumPendingSpellChecks(1);
    795 
    796  return NS_OK;
    797 }
    798 
    799 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
    800 nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
    801  MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");
    802 
    803  // spell checking is enabled, register our event listeners to track navigation
    804  RegisterEventListeners();
    805 
    806  mSpellCheck = mPendingSpellCheck;
    807  mPendingSpellCheck = nullptr;
    808  mPendingInitEditorSpellCheckCallback = nullptr;
    809  ChangeNumPendingSpellChecks(-1);
    810 
    811  // spellcheck the current contents. SpellCheckRange doesn't supply a created
    812  // range to DoSpellCheck, which in our case is the entire range. But this
    813  // optimization doesn't matter because there is nothing in the spellcheck
    814  // selection when starting, which triggers a better optimization.
    815  return SpellCheckRange(nullptr);
    816 }
    817 
    818 // Changes the number of pending spell checks by the given delta.  If the number
    819 // becomes zero or nonzero, observers are notified.  See NotifyObservers for
    820 // info on the aEditor parameter.
    821 void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
    822    int32_t aDelta, EditorBase* aEditorBase) {
    823  int8_t oldNumPending = mNumPendingSpellChecks;
    824  mNumPendingSpellChecks += aDelta;
    825  MOZ_ASSERT(mNumPendingSpellChecks >= 0,
    826             "Unbalanced ChangeNumPendingSpellChecks calls!");
    827  if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
    828    NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
    829  } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
    830    NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
    831  }
    832 }
    833 
    834 // Broadcasts the given topic to observers.  aEditor is passed to observers if
    835 // nonnull; otherwise mEditorBase is passed.
    836 void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
    837                                            EditorBase* aEditorBase) {
    838  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
    839  if (!os) return;
    840  // XXX Do we need to grab the editor here?  If it's necessary, each observer
    841  //     should do it instead.
    842  RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
    843  os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
    844                      nullptr);
    845 }
    846 
    847 // mozInlineSpellChecker::SpellCheckAfterEditorChange
    848 //
    849 //    Called by the editor when nearly anything happens to change the content.
    850 //
    851 //    The start and end positions specify a range for the thing that happened,
    852 //    but these are usually nullptr, even when you'd think they would be useful
    853 //    because you want the range (for example, pasting). We ignore them in
    854 //    this case.
    855 
    856 nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
    857    EditSubAction aEditSubAction, Selection& aSelection,
    858    nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
    859    nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
    860    uint32_t aEndOffset) {
    861  nsresult rv;
    862  if (!mSpellCheck) return NS_OK;  // disabling spell checking is not an error
    863 
    864  // this means something has changed, and we never check the current word,
    865  // therefore, we should spellcheck for subsequent caret navigations
    866  mNeedsCheckAfterNavigation = true;
    867 
    868  // the anchor node is the position of the caret
    869  Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
    870      mozInlineSpellStatus::CreateForEditorChange(
    871          *this, aEditSubAction, aSelection.GetAnchorNode(),
    872          aSelection.AnchorOffset(), aPreviousSelectedNode,
    873          aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
    874          aEndOffset);
    875  if (NS_WARN_IF(res.isErr())) {
    876    return res.unwrapErr();
    877  }
    878 
    879  rv = ScheduleSpellCheck(res.unwrap());
    880  NS_ENSURE_SUCCESS(rv, rv);
    881 
    882  // remember the current caret position after every change
    883  SaveCurrentSelectionPosition();
    884  return NS_OK;
    885 }
    886 
    887 // mozInlineSpellChecker::SpellCheckRange
    888 //
    889 //    Spellchecks all the words in the given range.
    890 //    Supply a nullptr range and this will check the entire editor.
    891 
    892 nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
    893  if (!mSpellCheck) {
    894    NS_WARNING_ASSERTION(
    895        mPendingSpellCheck,
    896        "Trying to spellcheck, but checking seems to be disabled");
    897    return NS_ERROR_NOT_INITIALIZED;
    898  }
    899 
    900  UniquePtr<mozInlineSpellStatus> status =
    901      mozInlineSpellStatus::CreateForRange(*this, aRange);
    902  return ScheduleSpellCheck(std::move(status));
    903 }
    904 
    905 // mozInlineSpellChecker::GetMisspelledWord
    906 
    907 NS_IMETHODIMP
    908 mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, uint32_t aOffset,
    909                                         nsRange** newword) {
    910  if (NS_WARN_IF(!aNode)) {
    911    return NS_ERROR_INVALID_ARG;
    912  }
    913  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
    914  if (NS_WARN_IF(!spellCheckSelection)) {
    915    return NS_ERROR_FAILURE;
    916  }
    917  return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
    918 }
    919 
    920 // mozInlineSpellChecker::ReplaceWord
    921 
    922 NS_IMETHODIMP
    923 mozInlineSpellChecker::ReplaceWord(nsINode* aNode, uint32_t aOffset,
    924                                   const nsAString& aNewWord) {
    925  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
    926    return NS_ERROR_FAILURE;
    927  }
    928 
    929  RefPtr<nsRange> range;
    930  nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
    931  NS_ENSURE_SUCCESS(res, res);
    932 
    933  if (!range) {
    934    return NS_OK;
    935  }
    936 
    937  // In usual cases, any words shouldn't include line breaks, but technically,
    938  // they may include and we need to avoid `HTMLTextAreaElement.value` returns
    939  // \r.  Therefore, we need to handle it here.
    940  nsString newWord(aNewWord);
    941  if (mEditorBase->IsTextEditor()) {
    942    nsContentUtils::PlatformToDOMLineBreaks(newWord);
    943  }
    944 
    945  // Blink dispatches cancelable `beforeinput` event at collecting misspelled
    946  // word so that we should allow to dispatch cancelable event.
    947  RefPtr<EditorBase> editorBase(mEditorBase);
    948  DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
    949      newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
    950  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
    951  return NS_OK;
    952 }
    953 
    954 // mozInlineSpellChecker::AddWordToDictionary
    955 
    956 NS_IMETHODIMP
    957 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
    958  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
    959 
    960  nsresult rv = mSpellCheck->AddWordToDictionary(word);
    961  NS_ENSURE_SUCCESS(rv, rv);
    962 
    963  UniquePtr<mozInlineSpellStatus> status =
    964      mozInlineSpellStatus::CreateForSelection(*this);
    965  return ScheduleSpellCheck(std::move(status));
    966 }
    967 
    968 //  mozInlineSpellChecker::RemoveWordFromDictionary
    969 
    970 NS_IMETHODIMP
    971 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
    972  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
    973 
    974  nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
    975  NS_ENSURE_SUCCESS(rv, rv);
    976 
    977  UniquePtr<mozInlineSpellStatus> status =
    978      mozInlineSpellStatus::CreateForRange(*this, nullptr);
    979  return ScheduleSpellCheck(std::move(status));
    980 }
    981 
    982 // mozInlineSpellChecker::IgnoreWord
    983 
    984 NS_IMETHODIMP
    985 mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
    986  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
    987 
    988  nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
    989  NS_ENSURE_SUCCESS(rv, rv);
    990 
    991  UniquePtr<mozInlineSpellStatus> status =
    992      mozInlineSpellStatus::CreateForSelection(*this);
    993  return ScheduleSpellCheck(std::move(status));
    994 }
    995 
    996 // mozInlineSpellChecker::IgnoreWords
    997 
    998 NS_IMETHODIMP
    999 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
   1000  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
   1001 
   1002  // add each word to the ignore list and then recheck the document
   1003  for (auto& word : aWordsToIgnore) {
   1004    mSpellCheck->IgnoreWordAllOccurrences(word);
   1005  }
   1006 
   1007  UniquePtr<mozInlineSpellStatus> status =
   1008      mozInlineSpellStatus::CreateForSelection(*this);
   1009  return ScheduleSpellCheck(std::move(status));
   1010 }
   1011 
   1012 // mozInlineSpellChecker::MakeSpellCheckRange
   1013 //
   1014 //    Given begin and end positions, this function constructs a range as
   1015 //    required for ScheduleSpellCheck. If the start and end nodes are nullptr,
   1016 //    then the entire range will be selected, and you can supply -1 as the
   1017 //    offset to the end range to select all of that node.
   1018 //
   1019 //    If the resulting range would be empty, nullptr is put into *aRange and the
   1020 //    function succeeds.
   1021 
   1022 nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
   1023                                                    int32_t aStartOffset,
   1024                                                    nsINode* aEndNode,
   1025                                                    int32_t aEndOffset,
   1026                                                    nsRange** aRange) const {
   1027  nsresult rv;
   1028  *aRange = nullptr;
   1029 
   1030  if (NS_WARN_IF(!mEditorBase)) {
   1031    return NS_ERROR_FAILURE;
   1032  }
   1033 
   1034  RefPtr<Document> doc = mEditorBase->GetDocument();
   1035  if (NS_WARN_IF(!doc)) {
   1036    return NS_ERROR_FAILURE;
   1037  }
   1038 
   1039  RefPtr<nsRange> range = nsRange::Create(doc);
   1040 
   1041  // possibly use full range of the editor
   1042  if (!aStartNode || !aEndNode) {
   1043    Element* domRootElement = mEditorBase->GetRoot();
   1044    if (NS_WARN_IF(!domRootElement)) {
   1045      return NS_ERROR_FAILURE;
   1046    }
   1047    aStartNode = aEndNode = domRootElement;
   1048    aStartOffset = 0;
   1049    aEndOffset = -1;
   1050  }
   1051 
   1052  if (aEndOffset == -1) {
   1053    // It's hard to say whether it's better to just do nsINode::GetChildCount or
   1054    // get the ChildNodes() and then its length.  The latter is faster if we
   1055    // keep going through this code for the same nodes (because it caches the
   1056    // length).  The former is faster if we keep getting different nodes here...
   1057    //
   1058    // Let's do the thing which can't end up with bad O(N^2) behavior.
   1059    aEndOffset = aEndNode->ChildNodes()->Length();
   1060  }
   1061 
   1062  // sometimes we are are requested to check an empty range (possibly an empty
   1063  // document). This will result in assertions later.
   1064  if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
   1065 
   1066  if (aEndOffset) {
   1067    rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
   1068    if (NS_WARN_IF(NS_FAILED(rv))) {
   1069      return rv;
   1070    }
   1071  } else {
   1072    rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
   1073                               RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
   1074    if (NS_WARN_IF(NS_FAILED(rv))) {
   1075      return rv;
   1076    }
   1077  }
   1078 
   1079  range.swap(*aRange);
   1080  return NS_OK;
   1081 }
   1082 
   1083 nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
   1084                                                       int32_t aStartOffset,
   1085                                                       nsINode* aEndNode,
   1086                                                       int32_t aEndOffset) {
   1087  RefPtr<nsRange> range;
   1088  nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
   1089                                    aEndOffset, getter_AddRefs(range));
   1090  NS_ENSURE_SUCCESS(rv, rv);
   1091 
   1092  if (!range) return NS_OK;  // range is empty: nothing to do
   1093 
   1094  UniquePtr<mozInlineSpellStatus> status =
   1095      mozInlineSpellStatus::CreateForRange(*this, range);
   1096  return ScheduleSpellCheck(std::move(status));
   1097 }
   1098 
   1099 // mozInlineSpellChecker::ShouldSpellCheckNode
   1100 //
   1101 //    There are certain conditions when we don't want to spell check a node. In
   1102 //    particular quotations, moz signatures, etc. This routine returns false
   1103 //    for these cases.
   1104 
   1105 // static
   1106 bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
   1107                                                 nsINode* aNode) {
   1108  MOZ_ASSERT(aNode);
   1109  if (!aNode->IsContent()) return false;
   1110 
   1111  nsIContent* content = aNode->AsContent();
   1112 
   1113  if (aEditorBase->IsMailEditor()) {
   1114    nsIContent* parent = content->GetParent();
   1115    while (parent) {
   1116      if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
   1117          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
   1118                                           nsGkAtoms::cite, eIgnoreCase)) {
   1119        return false;
   1120      }
   1121      if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
   1122          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
   1123                                           nsGkAtoms::mozsignature,
   1124                                           eIgnoreCase)) {
   1125        return false;
   1126      }
   1127      if (parent->IsHTMLElement(nsGkAtoms::div) &&
   1128          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
   1129                                           nsGkAtoms::mozfwcontainer,
   1130                                           eIgnoreCase)) {
   1131        return false;
   1132      }
   1133 
   1134      parent = parent->GetParent();
   1135    }
   1136  } else {
   1137    // Check spelling only if the node is editable, and GetSpellcheck() is true
   1138    // on the nearest HTMLElement ancestor.
   1139    if (!content->IsEditable()) {
   1140      return false;
   1141    }
   1142 
   1143    // Make sure that we can always turn on spell checking for inputs/textareas.
   1144    // Note that because of the previous check, at this point we know that the
   1145    // node is editable.
   1146    if (content->IsInNativeAnonymousSubtree()) {
   1147      nsIContent* node = content->GetParent();
   1148      while (node && node->IsInNativeAnonymousSubtree()) {
   1149        node = node->GetParent();
   1150      }
   1151      if (node && node->IsTextControlElement()) {
   1152        return true;
   1153      }
   1154    }
   1155 
   1156    // Get HTML element ancestor (might be aNode itself, although probably that
   1157    // has to be a text node in real life here)
   1158    nsIContent* parent = content;
   1159    while (!parent->IsHTMLElement()) {
   1160      parent = parent->GetParent();
   1161      if (!parent) {
   1162        return true;
   1163      }
   1164    }
   1165 
   1166    // See if it's spellcheckable
   1167    return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
   1168  }
   1169 
   1170  return true;
   1171 }
   1172 
   1173 // mozInlineSpellChecker::ScheduleSpellCheck
   1174 //
   1175 //    This is called by code to do the actual spellchecking. We will set up
   1176 //    the proper structures for calls to DoSpellCheck.
   1177 
   1178 nsresult mozInlineSpellChecker::ScheduleSpellCheck(
   1179    UniquePtr<mozInlineSpellStatus>&& aStatus) {
   1180  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
   1181          ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
   1182           mFullSpellCheckScheduled));
   1183 
   1184  if (mFullSpellCheckScheduled) {
   1185    // Just ignore this; we're going to spell-check everything anyway
   1186    return NS_OK;
   1187  }
   1188  // Cache the value because we are going to move aStatus's ownership to
   1189  // the new created mozInlineSpellResume instance.
   1190  bool isFullSpellCheck = aStatus->IsFullSpellCheck();
   1191 
   1192  RefPtr<mozInlineSpellResume> resume =
   1193      new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
   1194  NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
   1195 
   1196  nsresult rv = resume->Post();
   1197  if (NS_SUCCEEDED(rv)) {
   1198    if (isFullSpellCheck) {
   1199      // We're going to check everything.  Suppress further spell-check attempts
   1200      // until that happens.
   1201      mFullSpellCheckScheduled = true;
   1202    }
   1203    ChangeNumPendingSpellChecks(1);
   1204  }
   1205  return rv;
   1206 }
   1207 
   1208 // mozInlineSpellChecker::DoSpellCheckSelection
   1209 //
   1210 //    Called to re-check all misspelled words. We iterate over all ranges in
   1211 //    the selection and call DoSpellCheck on them. This is used when a word
   1212 //    is ignored or added to the dictionary: all instances of that word should
   1213 //    be removed from the selection.
   1214 //
   1215 //    FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
   1216 //    Typically, checking this small amount of text is relatively fast, but
   1217 //    for large numbers of words, a lag may be noticeable.
   1218 
   1219 nsresult mozInlineSpellChecker::DoSpellCheckSelection(
   1220    mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
   1221  nsresult rv;
   1222 
   1223  // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
   1224  mNumWordsInSpellSelection = 0;
   1225 
   1226  // Since we could be modifying the ranges for the spellCheckSelection while
   1227  // looping on the spell check selection, keep a separate array of range
   1228  // elements inside the selection
   1229  nsTArray<RefPtr<nsRange>> ranges;
   1230 
   1231  const uint32_t rangeCount = aSpellCheckSelection->RangeCount();
   1232  for (const uint32_t idx : IntegerRange(rangeCount)) {
   1233    MOZ_ASSERT(aSpellCheckSelection->RangeCount() == rangeCount);
   1234    nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
   1235    MOZ_ASSERT(range);
   1236    if (MOZ_LIKELY(range)) {
   1237      ranges.AppendElement(range);
   1238    }
   1239  }
   1240 
   1241  // We have saved the ranges above. Clearing the spellcheck selection here
   1242  // isn't necessary (rechecking each word will modify it as necessary) but
   1243  // provides better performance. By ensuring that no ranges need to be
   1244  // removed in DoSpellCheck, we can save checking range inclusion which is
   1245  // slow.
   1246  aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
   1247 
   1248  // We use this state object for all calls, and just update its range. Note
   1249  // that we don't need to call FinishInit since we will be filling in the
   1250  // necessary information.
   1251  UniquePtr<mozInlineSpellStatus> status =
   1252      mozInlineSpellStatus::CreateForRange(*this, nullptr);
   1253 
   1254  bool doneChecking;
   1255  for (uint32_t idx : IntegerRange(rangeCount)) {
   1256    // We can consider this word as "added" since we know it has no spell
   1257    // check range over it that needs to be deleted. All the old ranges
   1258    // were cleared above. We also need to clear the word count so that we
   1259    // check all words instead of stopping early.
   1260    status->mRange = ranges[idx];
   1261    rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
   1262    NS_ENSURE_SUCCESS(rv, rv);
   1263    MOZ_ASSERT(
   1264        doneChecking,
   1265        "We gave the spellchecker one word, but it didn't finish checking?!?!");
   1266  }
   1267 
   1268  return NS_OK;
   1269 }
   1270 
   1271 class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
   1272 public:
   1273  /**
   1274   * @param aStatus must be non-nullptr.
   1275   */
   1276  SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
   1277                    mozInlineSpellWordUtil& aWordUtil,
   1278                    mozilla::dom::Selection& aSpellCheckSelection,
   1279                    const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
   1280                    bool& aDoneChecking)
   1281      : mInlineSpellChecker{aInlineSpellChecker},
   1282        mWordUtil{aWordUtil},
   1283        mSpellCheckSelection{aSpellCheckSelection},
   1284        mStatus{aStatus},
   1285        mDoneChecking{aDoneChecking} {
   1286    MOZ_ASSERT(aStatus);
   1287  }
   1288 
   1289  [[nodiscard]] nsresult Execute();
   1290 
   1291 private:
   1292  // Creates an async request to check the words and update the ranges for the
   1293  // misspellings.
   1294  //
   1295  // @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
   1296  // @param aOldRangesForSomeWords ranges from previous spellcheckings which
   1297  //                               might need to be removed. Its length might
   1298  //                               differ from `aWords.Length()`.
   1299  // @param aNodeOffsetRangesForWords One range for each word in aWords. So
   1300  //                                  `aNodeOffsetRangesForWords.Length() ==
   1301  //                                  aWords.Length()`.
   1302  void CheckWordsAndUpdateRangesForMisspellings(
   1303      const nsTArray<nsString>& aWords,
   1304      nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
   1305      nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);
   1306 
   1307  void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);
   1308 
   1309  bool ShouldSpellCheckRange(const nsRange& aRange) const;
   1310 
   1311  bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;
   1312 
   1313  mozInlineSpellChecker& mInlineSpellChecker;
   1314  mozInlineSpellWordUtil& mWordUtil;
   1315  mozilla::dom::Selection& mSpellCheckSelection;
   1316  const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
   1317  bool& mDoneChecking;
   1318 };
   1319 
   1320 bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
   1321    const nsRange& aRange) const {
   1322  if (aRange.Collapsed()) {
   1323    return false;
   1324  }
   1325 
   1326  nsINode* beginNode = aRange.GetStartContainer();
   1327  nsINode* endNode = aRange.GetEndContainer();
   1328 
   1329  const nsINode* rootNode = mWordUtil.GetRootNode();
   1330  return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
   1331         beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
   1332         endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
   1333 }
   1334 
   1335 bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
   1336    const nsINode& aNode, int32_t aOffset) const {
   1337  ErrorResult erv;
   1338  return mStatus->GetNoCheckRange() &&
   1339         mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
   1340 }
   1341 
   1342 void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
   1343    const nsTArray<RefPtr<nsRange>>& aRanges) {
   1344  for (uint32_t i = 0; i < aRanges.Length(); i++) {
   1345    mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
   1346  }
   1347 }
   1348 
   1349 // mozInlineSpellChecker::SpellCheckerSlice::Execute
   1350 //
   1351 //    This function checks words intersecting the given range, excluding those
   1352 //    inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
   1353 //    will have any spell selection removed (this is used to hide the
   1354 //    underlining for the word that the caret is in). aNoCheckRange should be
   1355 //    on word boundaries.
   1356 //
   1357 //    mResume->mCreatedRange is a possibly nullptr range of new text that was
   1358 //    inserted.  Inside this range, we don't bother to check whether things are
   1359 //    inside the spellcheck selection, which speeds up large paste operations
   1360 //    considerably.
   1361 //
   1362 //    Normal case when editing text by typing
   1363 //       h e l l o   w o r k d   h o w   a r e   y o u
   1364 //                            ^ caret
   1365 //                   [-------] mRange
   1366 //                   [-------] mNoCheckRange
   1367 //      -> does nothing (range is the same as the no check range)
   1368 //
   1369 //    Case when pasting:
   1370 //             [---------- pasted text ----------]
   1371 //       h e l l o   w o r k d   h o w   a r e   y o u
   1372 //                                                ^ caret
   1373 //                                               [---] aNoCheckRange
   1374 //      -> recheck all words in range except those in aNoCheckRange
   1375 //
   1376 //    If checking is complete, *aDoneChecking will be set. If there is more
   1377 //    but we ran out of time, this will be false and the range will be
   1378 //    updated with the stuff that still needs checking.
   1379 
   1380 nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
   1381  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
   1382 
   1383  mDoneChecking = true;
   1384 
   1385  if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
   1386    return NS_ERROR_NOT_INITIALIZED;
   1387  }
   1388 
   1389  if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
   1390    return NS_OK;
   1391  }
   1392 
   1393  // get the editor for ShouldSpellCheckNode, this may fail in reasonable
   1394  // circumstances since the editor could have gone away
   1395  RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
   1396  if (!editorBase || editorBase->Destroyed()) {
   1397    return NS_ERROR_FAILURE;
   1398  }
   1399 
   1400  if (!ShouldSpellCheckRange(*mStatus->mRange)) {
   1401    // Just bail out and don't try to spell-check this
   1402    return NS_OK;
   1403  }
   1404 
   1405  // see if the selection has any ranges, if not, then we can optimize checking
   1406  // range inclusion later (we have no ranges when we are initially checking or
   1407  // when there are no misspelled words yet).
   1408  const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();
   1409 
   1410  // set the starting DOM position to be the beginning of our range
   1411  if (nsresult rv = mWordUtil.SetPositionAndEnd(
   1412          mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
   1413          mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
   1414      NS_FAILED(rv)) {
   1415    // Just bail out and don't try to spell-check this
   1416    return NS_OK;
   1417  }
   1418 
   1419  // aWordUtil.SetPosition flushes pending notifications, check editor again.
   1420  if (!mInlineSpellChecker.mEditorBase) {
   1421    return NS_ERROR_FAILURE;
   1422  }
   1423 
   1424  int32_t wordsChecked = 0;
   1425  PRTime beginTime = PR_Now();
   1426 
   1427  nsTArray<nsString> normalizedWords;
   1428  nsTArray<RefPtr<nsRange>> oldRangesToRemove;
   1429  nsTArray<NodeOffsetRange> checkRanges;
   1430  mozInlineSpellWordUtil::Word word;
   1431  static const size_t requestChunkSize =
   1432      INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
   1433 
   1434  while (mWordUtil.GetNextWord(word)) {
   1435    // get the range for the current word.
   1436    nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
   1437    nsINode* const endNode = word.mNodeOffsetRange.End().Node();
   1438    // TODO: Make them `uint32_t`
   1439    const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
   1440    const int32_t endOffset = word.mNodeOffsetRange.End().Offset();
   1441 
   1442    // see if we've done enough words in this round and run out of time.
   1443    if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
   1444        PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
   1445      // stop checking, our time limit has been exceeded.
   1446      MOZ_LOG(
   1447          sInlineSpellCheckerLog, LogLevel::Verbose,
   1448          ("%s: we have run out of time, schedule next round.", __FUNCTION__));
   1449 
   1450      CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
   1451                                               std::move(oldRangesToRemove),
   1452                                               std::move(checkRanges));
   1453 
   1454      // move the range to encompass the stuff that needs checking.
   1455      nsresult rv = mStatus->mRange->SetStart(
   1456          beginNode, AssertedCast<uint32_t>(beginOffset));
   1457      if (NS_FAILED(rv)) {
   1458        // The range might be unhappy because the beginning is after the
   1459        // end. This is possible when the requested end was in the middle
   1460        // of a word, just ignore this situation and assume we're done.
   1461        return NS_OK;
   1462      }
   1463      mDoneChecking = false;
   1464      return NS_OK;
   1465    }
   1466 
   1467    MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
   1468            ("%s: got word \"%s\"%s", __FUNCTION__,
   1469             NS_ConvertUTF16toUTF8(word.mText).get(),
   1470             word.mSkipChecking ? " (not checking)" : ""));
   1471 
   1472    // see if there is a spellcheck range that already intersects the word
   1473    // and remove it. We only need to remove old ranges, so don't bother if
   1474    // there were no ranges when we started out.
   1475    if (originalRangeCount > 0) {
   1476      ErrorResult erv;
   1477      // likewise, if this word is inside new text, we won't bother testing
   1478      if (!mStatus->GetCreatedRange() ||
   1479          !mStatus->GetCreatedRange()->IsPointInRange(
   1480              *beginNode, AssertedCast<uint32_t>(beginOffset), erv)) {
   1481        MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
   1482                ("%s: removing ranges for some interval.", __FUNCTION__));
   1483 
   1484        nsTArray<RefPtr<nsRange>> ranges;
   1485        mSpellCheckSelection.GetRangesForInterval(
   1486            *beginNode, AssertedCast<uint32_t>(beginOffset), *endNode,
   1487            AssertedCast<uint32_t>(endOffset), true, ranges, erv);
   1488        RETURN_NSRESULT_ON_FAILURE(erv);
   1489        oldRangesToRemove.AppendElements(std::move(ranges));
   1490      }
   1491    }
   1492 
   1493    // some words are special and don't need checking
   1494    if (word.mSkipChecking) {
   1495      continue;
   1496    }
   1497 
   1498    // some nodes we don't spellcheck
   1499    if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
   1500      continue;
   1501    }
   1502 
   1503    // Don't check spelling if we're inside the noCheckRange. This needs to
   1504    // be done after we clear any old selection because the excluded word
   1505    // might have been previously marked.
   1506    //
   1507    // We do a simple check to see if the beginning of our word is in the
   1508    // exclusion range. Because the exclusion range is a multiple of a word,
   1509    // this is sufficient.
   1510    if (IsInNoCheckRange(*beginNode, beginOffset)) {
   1511      continue;
   1512    }
   1513 
   1514    // check spelling and add to selection if misspelled
   1515    mozInlineSpellWordUtil::NormalizeWord(word.mText);
   1516    normalizedWords.AppendElement(word.mText);
   1517    checkRanges.AppendElement(word.mNodeOffsetRange);
   1518    wordsChecked++;
   1519    if (normalizedWords.Length() >= requestChunkSize) {
   1520      CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
   1521                                               std::move(oldRangesToRemove),
   1522                                               std::move(checkRanges));
   1523      normalizedWords.Clear();
   1524      oldRangesToRemove = {};
   1525      // Set new empty data for spellcheck range in DOM to avoid
   1526      // clang-tidy detection.
   1527      checkRanges = nsTArray<NodeOffsetRange>();
   1528    }
   1529  }
   1530 
   1531  CheckWordsAndUpdateRangesForMisspellings(
   1532      normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));
   1533 
   1534  return NS_OK;
   1535 }
   1536 
   1537 nsresult mozInlineSpellChecker::DoSpellCheck(
   1538    mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
   1539    const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
   1540  MOZ_ASSERT(aDoneChecking);
   1541 
   1542  SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
   1543                                      aStatus, *aDoneChecking};
   1544 
   1545  return spellCheckerSlice.Execute();
   1546 }
   1547 
   1548 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
   1549 class MOZ_RAII AutoChangeNumPendingSpellChecks final {
   1550 public:
   1551  explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
   1552                                           int32_t aDelta)
   1553      : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
   1554 
   1555  ~AutoChangeNumPendingSpellChecks() {
   1556    mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
   1557  }
   1558 
   1559 private:
   1560  RefPtr<mozInlineSpellChecker> mSpellChecker;
   1561  int32_t mDelta;
   1562 };
   1563 
   1564 void mozInlineSpellChecker::SpellCheckerSlice::
   1565    CheckWordsAndUpdateRangesForMisspellings(
   1566        const nsTArray<nsString>& aWords,
   1567        nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
   1568        nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
   1569  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
   1570          ("%s: aWords.Length()=%i", __FUNCTION__,
   1571           static_cast<int>(aWords.Length())));
   1572 
   1573  MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());
   1574 
   1575  // TODO:
   1576  // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
   1577  // to remove ranges more efficiently.
   1578 
   1579  if (aWords.IsEmpty()) {
   1580    RemoveRanges(aOldRangesForSomeWords);
   1581    return;
   1582  }
   1583 
   1584  mInlineSpellChecker.ChangeNumPendingSpellChecks(1);
   1585 
   1586  RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
   1587  RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
   1588  uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
   1589  mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
   1590      GetMainThreadSerialEventTarget(), __func__,
   1591      [inlineSpellChecker, spellCheckerSelection,
   1592       nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
   1593       oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
   1594       token](const nsTArray<bool>& aIsMisspelled) {
   1595        if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
   1596          // This result is never used
   1597          return;
   1598        }
   1599 
   1600        if (!inlineSpellChecker->mEditorBase ||
   1601            inlineSpellChecker->mEditorBase->Destroyed()) {
   1602          return;
   1603        }
   1604 
   1605        AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);
   1606 
   1607        if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
   1608          return;
   1609        }
   1610 
   1611        inlineSpellChecker->UpdateRangesForMisspelledWords(
   1612            nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
   1613            *spellCheckerSelection);
   1614      },
   1615      [inlineSpellChecker, token](nsresult aRv) {
   1616        if (!inlineSpellChecker->mEditorBase ||
   1617            inlineSpellChecker->mEditorBase->Destroyed()) {
   1618          return;
   1619        }
   1620 
   1621        if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
   1622          // This result is never used
   1623          return;
   1624        }
   1625 
   1626        inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
   1627      });
   1628 }
   1629 
   1630 // mozInlineSpellChecker::ResumeCheck
   1631 //
   1632 //    Called by the resume event when it fires. We will try to pick up where
   1633 //    the last resume left off.
   1634 
   1635 nsresult mozInlineSpellChecker::ResumeCheck(
   1636    UniquePtr<mozInlineSpellStatus>&& aStatus) {
   1637  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
   1638 
   1639  // Observers should be notified that spell check has ended only after spell
   1640  // check is done below, but since there are many early returns in this method
   1641  // and the number of pending spell checks must be decremented regardless of
   1642  // whether the spell check actually happens, use this RAII object.
   1643  AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
   1644 
   1645  if (aStatus->IsFullSpellCheck()) {
   1646    // Allow posting new spellcheck resume events from inside
   1647    // ResumeCheck, now that we're actually firing.
   1648    MOZ_ASSERT(mFullSpellCheckScheduled,
   1649               "How could this be false?  The full spell check is "
   1650               "calling us!!");
   1651    mFullSpellCheckScheduled = false;
   1652  }
   1653 
   1654  if (!mSpellCheck) return NS_OK;  // spell checking has been turned off
   1655 
   1656  if (!mEditorBase) {
   1657    return NS_OK;
   1658  }
   1659 
   1660  Maybe<mozInlineSpellWordUtil> wordUtil{
   1661      mozInlineSpellWordUtil::Create(*mEditorBase)};
   1662  if (!wordUtil) {
   1663    return NS_OK;  // editor doesn't like us, don't assert
   1664  }
   1665 
   1666  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
   1667  if (NS_WARN_IF(!spellCheckSelection)) {
   1668    return NS_ERROR_FAILURE;
   1669  }
   1670 
   1671  nsTArray<nsCString> currentDictionaries;
   1672  nsresult rv = mSpellCheck->GetCurrentDictionaries(currentDictionaries);
   1673  if (NS_FAILED(rv)) {
   1674    MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
   1675            ("%s: no active dictionary.", __FUNCTION__));
   1676 
   1677    // no active dictionary
   1678    for (const uint32_t index :
   1679         Reversed(IntegerRange(spellCheckSelection->RangeCount()))) {
   1680      RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
   1681      if (MOZ_LIKELY(checkRange)) {
   1682        RemoveRange(spellCheckSelection, checkRange);
   1683      }
   1684    }
   1685    return NS_OK;
   1686  }
   1687 
   1688  CleanupRangesInSelection(spellCheckSelection);
   1689 
   1690  rv = aStatus->FinishInitOnEvent(*wordUtil);
   1691  NS_ENSURE_SUCCESS(rv, rv);
   1692  if (!aStatus->mRange) return NS_OK;  // empty range, nothing to do
   1693 
   1694  bool doneChecking = true;
   1695  if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
   1696    rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
   1697  else
   1698    rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
   1699  NS_ENSURE_SUCCESS(rv, rv);
   1700 
   1701  if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
   1702  return rv;
   1703 }
   1704 
   1705 // mozInlineSpellChecker::IsPointInSelection
   1706 //
   1707 //    Determines if a given (node,offset) point is inside the given
   1708 //    selection. If so, the specific range of the selection that
   1709 //    intersects is places in *aRange. (There may be multiple disjoint
   1710 //    ranges in a selection.)
   1711 //
   1712 //    If there is no intersection, *aRange will be nullptr.
   1713 
   1714 // static
   1715 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
   1716                                                   nsINode* aNode,
   1717                                                   uint32_t aOffset,
   1718                                                   nsRange** aRange) {
   1719  *aRange = nullptr;
   1720 
   1721  nsTArray<nsRange*> ranges;
   1722  nsresult rv = aSelection.GetDynamicRangesForIntervalArray(
   1723      aNode, aOffset, aNode, aOffset, true, &ranges);
   1724  NS_ENSURE_SUCCESS(rv, rv);
   1725 
   1726  if (ranges.Length() == 0) return NS_OK;  // no matches
   1727 
   1728  // there may be more than one range returned, and we don't know what do
   1729  // do with that, so just get the first one
   1730  NS_ADDREF(*aRange = ranges[0]);
   1731  return NS_OK;
   1732 }
   1733 
   1734 nsresult mozInlineSpellChecker::CleanupRangesInSelection(
   1735    Selection* aSelection) {
   1736  // integrity check - remove ranges that have collapsed to nothing. This
   1737  // can happen if the node containing a highlighted word was removed.
   1738  if (!aSelection) return NS_ERROR_FAILURE;
   1739 
   1740  // TODO: Rewrite this with reversed ranged-loop, it might make this simpler.
   1741  int64_t count = aSelection->RangeCount();
   1742  for (int64_t index = 0; index < count; index++) {
   1743    nsRange* checkRange = aSelection->GetRangeAt(static_cast<uint32_t>(index));
   1744    if (MOZ_LIKELY(checkRange)) {
   1745      if (checkRange->Collapsed()) {
   1746        RemoveRange(aSelection, checkRange);
   1747        index--;
   1748        count--;
   1749      }
   1750    }
   1751  }
   1752 
   1753  return NS_OK;
   1754 }
   1755 
   1756 // mozInlineSpellChecker::RemoveRange
   1757 //
   1758 //    For performance reasons, we have an upper bound on the number of word
   1759 //    ranges  in the spell check selection. When removing a range from the
   1760 //    selection, we need to decrement mNumWordsInSpellSelection
   1761 
   1762 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
   1763                                            nsRange* aRange) {
   1764  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
   1765 
   1766  NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
   1767  NS_ENSURE_ARG_POINTER(aRange);
   1768 
   1769  ErrorResult rv;
   1770  RefPtr<nsRange> range{aRange};
   1771  RefPtr<Selection> selection{aSpellCheckSelection};
   1772  selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
   1773  if (!rv.Failed()) {
   1774    if (mNumWordsInSpellSelection) {
   1775      mNumWordsInSpellSelection--;
   1776    }
   1777  }
   1778 
   1779  return rv.StealNSResult();
   1780 }
   1781 
   1782 struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange {
   1783  static bool Equals(const RefPtr<nsRange>& aRange,
   1784                     const NodeOffsetRange& aNodeOffsetRange) {
   1785    return aNodeOffsetRange == *aRange;
   1786  }
   1787 };
   1788 
   1789 void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
   1790    const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords,
   1791    const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords,
   1792    const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) {
   1793  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
   1794 
   1795  MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length());
   1796 
   1797  // When the spellchecker checks text containing words separated by "/", it may
   1798  // happen that some words checked in one timeslice, are checked again in a
   1799  // following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo"
   1800  // and "baz" are checked in one timeslice and two ranges are added for them.
   1801  // In the following timeslice "foo" and "baz" are checked again but since
   1802  // their corresponding ranges are already in the spellcheck-Selection
   1803  // they don't have to be added again and since "foo" and "baz" still contain
   1804  // spelling mistakes, they don't have to be removed.
   1805  //
   1806  // In this case, it's more efficient to keep the existing ranges.
   1807 
   1808  AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
   1809      oldRangesMarkedForRemoval;
   1810  for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) {
   1811    oldRangesMarkedForRemoval.AppendElement(true);
   1812  }
   1813 
   1814  AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
   1815      nodeOffsetRangesMarkedForAdding;
   1816  for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) {
   1817    nodeOffsetRangesMarkedForAdding.AppendElement(false);
   1818  }
   1819 
   1820  for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
   1821    if (!aIsMisspelled[i]) {
   1822      continue;
   1823    }
   1824 
   1825    const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i];
   1826    const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
   1827        nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
   1828    if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
   1829        aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
   1830            aSpellCheckerSelection)) {
   1831      /** TODO: warn in case the old range doesn't
   1832        belong to the selection. This is not critical,
   1833        because other code can always remove them
   1834        before the actual spellchecking happens. */
   1835      MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
   1836              ("%s: reusing old range.", __FUNCTION__));
   1837 
   1838      oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false;
   1839    } else {
   1840      nodeOffsetRangesMarkedForAdding[i] = true;
   1841    }
   1842  }
   1843 
   1844  for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) {
   1845    if (oldRangesMarkedForRemoval[i]) {
   1846      RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]);
   1847    }
   1848  }
   1849 
   1850  // Add ranges after removing the marked old ones, so that the Selection can
   1851  // become full again.
   1852  for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) {
   1853    if (nodeOffsetRangesMarkedForAdding[i]) {
   1854      RefPtr<nsRange> wordRange =
   1855          mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]);
   1856      // If we somehow can't make a range for this word, just ignore
   1857      // it.
   1858      if (wordRange) {
   1859        AddRange(&aSpellCheckerSelection, wordRange);
   1860      }
   1861    }
   1862  }
   1863 }
   1864 
   1865 // mozInlineSpellChecker::AddRange
   1866 //
   1867 //    For performance reasons, we have an upper bound on the number of word
   1868 //    ranges we'll add to the spell check selection. Once we reach that upper
   1869 //    bound, stop adding the ranges
   1870 
   1871 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
   1872                                         nsRange* aRange) {
   1873  NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
   1874  NS_ENSURE_ARG_POINTER(aRange);
   1875 
   1876  nsresult rv = NS_OK;
   1877 
   1878  if (!IsSpellCheckSelectionFull()) {
   1879    IgnoredErrorResult err;
   1880    aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
   1881                                                                    err);
   1882    if (err.Failed()) {
   1883      rv = err.StealNSResult();
   1884    } else {
   1885      mNumWordsInSpellSelection++;
   1886    }
   1887  }
   1888 
   1889  return rv;
   1890 }
   1891 
   1892 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
   1893  if (NS_WARN_IF(!mEditorBase)) {
   1894    return nullptr;
   1895  }
   1896  RefPtr<Selection> selection =
   1897      mEditorBase->GetSelection(SelectionType::eSpellCheck);
   1898  if (!selection) {
   1899    return nullptr;
   1900  }
   1901  return selection.forget();
   1902 }
   1903 
   1904 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
   1905  if (NS_WARN_IF(!mEditorBase)) {
   1906    return NS_OK;  // XXX Why NS_OK?
   1907  }
   1908 
   1909  // figure out the old caret position based on the current selection
   1910  RefPtr<Selection> selection = mEditorBase->GetSelection();
   1911  if (NS_WARN_IF(!selection)) {
   1912    return NS_ERROR_FAILURE;
   1913  }
   1914 
   1915  mCurrentSelectionAnchorNode = selection->GetFocusNode();
   1916  mCurrentSelectionOffset = selection->FocusOffset();
   1917 
   1918  return NS_OK;
   1919 }
   1920 
   1921 // mozInlineSpellChecker::HandleNavigationEvent
   1922 //
   1923 //    Acts upon mouse clicks and keyboard navigation changes, spell checking
   1924 //    the previous word if the new navigation location moves us to another
   1925 //    word.
   1926 //
   1927 //    This is complicated by the fact that our mouse events are happening after
   1928 //    selection has been changed to account for the mouse click. But keyboard
   1929 //    events are happening before the caret selection has changed. Working
   1930 //    around this by letting keyboard events setting forceWordSpellCheck to
   1931 //    true. aNewPositionOffset also tries to work around this for the
   1932 //    DOM_VK_RIGHT and DOM_VK_LEFT cases.
   1933 
   1934 nsresult mozInlineSpellChecker::HandleNavigationEvent(
   1935    bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
   1936  nsresult rv;
   1937 
   1938  // If we already handled the navigation event and there is no possibility
   1939  // anything has changed since then, we don't have to do anything. This
   1940  // optimization makes a noticeable difference when you hold down a navigation
   1941  // key like Page Down.
   1942  if (!mNeedsCheckAfterNavigation) return NS_OK;
   1943 
   1944  nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
   1945  uint32_t currentAnchorOffset = mCurrentSelectionOffset;
   1946 
   1947  // now remember the new focus position resulting from the event
   1948  rv = SaveCurrentSelectionPosition();
   1949  NS_ENSURE_SUCCESS(rv, rv);
   1950 
   1951  bool shouldPost;
   1952  Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
   1953      mozInlineSpellStatus::CreateForNavigation(
   1954          *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
   1955          currentAnchorOffset, mCurrentSelectionAnchorNode,
   1956          mCurrentSelectionOffset, &shouldPost);
   1957 
   1958  if (NS_WARN_IF(res.isErr())) {
   1959    return res.unwrapErr();
   1960  }
   1961 
   1962  if (shouldPost) {
   1963    rv = ScheduleSpellCheck(res.unwrap());
   1964    NS_ENSURE_SUCCESS(rv, rv);
   1965  }
   1966 
   1967  return NS_OK;
   1968 }
   1969 
   1970 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) {
   1971  WidgetEvent* widgetEvent = aEvent->WidgetEventPtr();
   1972  if (MOZ_UNLIKELY(!widgetEvent)) {
   1973    return NS_OK;
   1974  }
   1975 
   1976  switch (widgetEvent->mMessage) {
   1977    case eBlur:
   1978      OnBlur(*aEvent);
   1979      return NS_OK;
   1980    case ePointerClick:
   1981      OnPointerClick(*aEvent);
   1982      return NS_OK;
   1983    case eKeyDown:
   1984      OnKeyDown(*aEvent);
   1985      return NS_OK;
   1986    default:
   1987      MOZ_ASSERT_UNREACHABLE("You must forgot to handle new event type");
   1988      return NS_OK;
   1989  }
   1990 }
   1991 
   1992 void mozInlineSpellChecker::OnBlur(Event& aEvent) {
   1993  // force spellcheck on blur, for instance when tabbing out of a textbox
   1994  HandleNavigationEvent(true);
   1995 }
   1996 
   1997 void mozInlineSpellChecker::OnPointerClick(Event& aPointerEvent) {
   1998  MouseEvent* const mouseEvent = aPointerEvent.AsMouseEvent();
   1999  if (MOZ_UNLIKELY(!mouseEvent)) {
   2000    return;
   2001  }
   2002 
   2003  // ignore any errors from HandleNavigationEvent as we don't want to prevent
   2004  // anyone else from seeing this event.
   2005  HandleNavigationEvent(mouseEvent->Button() != 0);
   2006 }
   2007 
   2008 void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) {
   2009  WidgetKeyboardEvent* widgetKeyboardEvent =
   2010      aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
   2011  if (MOZ_UNLIKELY(!widgetKeyboardEvent)) {
   2012    return;
   2013  }
   2014 
   2015  // we only care about navigation keys that moved selection
   2016  switch (widgetKeyboardEvent->mKeyNameIndex) {
   2017    case KEY_NAME_INDEX_ArrowRight:
   2018      // XXX Does this work with RTL text?
   2019      HandleNavigationEvent(false, 1);
   2020      return;
   2021    case KEY_NAME_INDEX_ArrowLeft:
   2022      // XXX Does this work with RTL text?
   2023      HandleNavigationEvent(false, -1);
   2024      return;
   2025    case KEY_NAME_INDEX_ArrowUp:
   2026    case KEY_NAME_INDEX_ArrowDown:
   2027    case KEY_NAME_INDEX_Home:
   2028    case KEY_NAME_INDEX_End:
   2029    case KEY_NAME_INDEX_PageDown:
   2030    case KEY_NAME_INDEX_PageUp:
   2031      HandleNavigationEvent(true /* force a spelling correction */);
   2032      return;
   2033    default:
   2034      return;
   2035  }
   2036 }
   2037 
   2038 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
   2039 class UpdateCurrentDictionaryCallback final
   2040    : public nsIEditorSpellCheckCallback {
   2041 public:
   2042  NS_DECL_ISUPPORTS
   2043 
   2044  explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
   2045                                           uint32_t aDisabledAsyncToken)
   2046      : mSpellChecker(aSpellChecker),
   2047        mDisabledAsyncToken(aDisabledAsyncToken) {}
   2048 
   2049  NS_IMETHOD EditorSpellCheckDone() override {
   2050    // Ignore this callback if SetEnableRealTimeSpell(false) was called after
   2051    // the UpdateCurrentDictionary call that triggered it.
   2052    return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
   2053               ? NS_OK
   2054               : mSpellChecker->CurrentDictionaryUpdated();
   2055  }
   2056 
   2057 private:
   2058  ~UpdateCurrentDictionaryCallback() {}
   2059 
   2060  RefPtr<mozInlineSpellChecker> mSpellChecker;
   2061  uint32_t mDisabledAsyncToken;
   2062 };
   2063 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
   2064 
   2065 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
   2066  // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
   2067  // checker is being initialized.  Calling UpdateCurrentDictionary on
   2068  // mPendingSpellCheck simply queues the dictionary update after the init.
   2069  RefPtr<EditorSpellCheck> spellCheck =
   2070      mSpellCheck ? mSpellCheck : mPendingSpellCheck;
   2071  if (!spellCheck) {
   2072    return NS_OK;
   2073  }
   2074 
   2075  RefPtr<UpdateCurrentDictionaryCallback> cb =
   2076      new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
   2077  NS_ENSURE_STATE(cb);
   2078  nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
   2079  if (NS_FAILED(rv)) {
   2080    cb = nullptr;
   2081    return rv;
   2082  }
   2083  mNumPendingUpdateCurrentDictionary++;
   2084  ChangeNumPendingSpellChecks(1);
   2085 
   2086  return NS_OK;
   2087 }
   2088 
   2089 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
   2090 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
   2091  mNumPendingUpdateCurrentDictionary--;
   2092  MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0,
   2093             "CurrentDictionaryUpdated called without corresponding "
   2094             "UpdateCurrentDictionary call!");
   2095  ChangeNumPendingSpellChecks(-1);
   2096 
   2097  nsresult rv = SpellCheckRange(nullptr);
   2098  NS_ENSURE_SUCCESS(rv, rv);
   2099 
   2100  return NS_OK;
   2101 }
   2102 
   2103 NS_IMETHODIMP
   2104 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
   2105  *aPending = mNumPendingSpellChecks > 0;
   2106  return NS_OK;
   2107 }