Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/extensions/spellcheck/src/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 74 kB image not shown  

Quelle  mozInlineSpellChecker.cpp   Sprache: C

 
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=2 sts=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


/**
 * This class is called by the editor to handle spellchecking after various
 * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
 * when the text is changed.
 *
 * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
 * notifications to be flushed when we are called from the editor. This is
 * because the call might originate from a frame, and flushing the
 * notifications might cause that frame to be deleted.
 *
 * We post an event and do all of the spellchecking in that event handler.
 * We store all DOM pointers in ranges because they are kept up-to-date with
 * DOM changes that may have happened while the event was on the queue.
 *
 * We also allow the spellcheck to be suspended and resumed later. This makes
 * large pastes or initializations with a lot of text not hang the browser UI.
 *
 * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
 * true when we get any change, and false once there is no possibility
 * something changed that we need to check on navigation. Navigation events
 * tend to be a little tricky because we want to check the current word on
 * exit if something has changed. If we navigate inside the word, we don't want
 * to do anything. As a result, this flag is cleared in FinishNavigationEvent
 * when we know that we are checking as a result of navigation.
 */


#include "mozInlineSpellChecker.h"

#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorBase.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorSpellCheck.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/Logging.h"
#include "mozilla/RangeUtils.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/KeyboardEvent.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/Selection.h"
#include "mozInlineSpellWordUtil.h"
#include "nsCOMPtr.h"
#include "nsCRT.h"
#include "nsGenericHTMLElement.h"
#include "nsRange.h"
#include "nsIPrefBranch.h"
#include "nsIPrefService.h"
#include "nsIRunnable.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "nsUnicharUtils.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsRange.h"
#include "nsContentUtils.h"
#include "nsIObserverService.h"
#include "prtime.h"

using mozilla::LogLevel;
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::ipc;

// the number of milliseconds that we will take at once to do spellchecking
#define INLINESPELL_CHECK_TIMEOUT 1

// The number of words to check before we look at the time to see if
// INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
// stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
// be too short to a low-end machine.
#define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5

// The maximum number of words to check word via IPC.
#define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25

// These notifications are broadcast when spell check starts and ends.  STARTED
// must always be followed by ENDED.
#define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
#define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"

static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");

static const PRTime kMaxSpellCheckTimeInUsec =
    INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;

mozInlineSpellStatus::mozInlineSpellStatus(
    mozInlineSpellChecker* aSpellChecker, const Operation aOp,
    RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
    RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
    const int32_t aNewNavigationPositionOffset)
    : mSpellChecker(aSpellChecker),
      mRange(std::move(aRange)),
      mOp(aOp),
      mCreatedRange(std::move(aCreatedRange)),
      mAnchorRange(std::move(aAnchorRange)),
      mForceNavigationWordCheck(aForceNavigationWordCheck),
      mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}

// mozInlineSpellStatus::CreateForEditorChange
//
//    This is the most complicated case. For changes, we need to compute the
//    range of stuff that changed based on the old and new caret positions,
//    as well as use a range possibly provided by the editor (start and end,
//    which are usually nullptr) to get a range with the union of these.

// static
Result<UniquePtr<mozInlineSpellStatus>, nsresult>
mozInlineSpellStatus::CreateForEditorChange(
    mozInlineSpellChecker& aSpellChecker, const EditSubAction aEditSubAction,
    nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
    uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
    nsINode* aEndNode, uint32_t aEndOffset) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));

  if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
    return Err(NS_ERROR_FAILURE);
  }

  bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
  if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
    // IME may remove the previous node if it cancels composition when
    // there is no text around the composition.
    deleted = !aPreviousNode->IsInComposedDoc();
  }

  // save the anchor point as a range so we can find the current word later
  RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
      aAnchorNode, aAnchorOffset);
  if (NS_WARN_IF(!anchorRange)) {
    return Err(NS_ERROR_FAILURE);
  }

  // Deletes are easy, the range is just the current anchor. We set the range
  // to check to be empty, FinishInitOnEvent will fill in the range to be
  // the current word.
  RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);

  // On insert save this range: DoSpellCheck optimizes things in this range.
  // Otherwise, just leave this nullptr.
  RefPtr<nsRange> createdRange =
      (aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;

  UniquePtr<mozInlineSpellStatus> status{
      /* The constructor is `private`, hence the explicit allocation. */
      new mozInlineSpellStatus{&aSpellChecker,
                               deleted ? eOpChangeDelete : eOpChange,
                               std::move(range), std::move(createdRange),
                               std::move(anchorRange), false, 0}};
  if (deleted) {
    return status;
  }

  // ...we need to put the start and end in the correct order
  ErrorResult errorResult;
  int16_t cmpResult = status->mAnchorRange->ComparePoint(
      *aPreviousNode, aPreviousOffset, errorResult);
  if (NS_WARN_IF(errorResult.Failed())) {
    return Err(errorResult.StealNSResult());
  }
  nsresult rv;
  if (cmpResult < 0) {
    // previous anchor node is before the current anchor
    rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
                                        aAnchorNode, aAnchorOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return Err(rv);
    }
  } else {
    // previous anchor node is after (or the same as) the current anchor
    rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
                                        aPreviousNode, aPreviousOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return Err(rv);
    }
  }

  // if we were given a range, we need to expand our range to encompass it
  if (aStartNode && aEndNode) {
    cmpResult =
        status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
    if (NS_WARN_IF(errorResult.Failed())) {
      return Err(errorResult.StealNSResult());
    }
    if (cmpResult < 0) {  // given range starts before
      rv = status->mRange->SetStart(aStartNode, aStartOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }
    }

    cmpResult =
        status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
    if (NS_WARN_IF(errorResult.Failed())) {
      return Err(errorResult.StealNSResult());
    }
    if (cmpResult > 0) {  // given range ends after
      rv = status->mRange->SetEnd(aEndNode, aEndOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }
    }
  }

  return status;
}

// mozInlineSpellStatus::CreateForNavigation
//
//    For navigation events, we just need to store the new and old positions.
//
//    In some cases, we detect that we shouldn't check. If this event should
//    not be processed, *aContinue will be false.

// static
Result<UniquePtr<mozInlineSpellStatus>, nsresult>
mozInlineSpellStatus::CreateForNavigation(
    mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
    int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
    uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
    uint32_t aNewAnchorOffset, bool* aContinue) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));

  RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
      aNewAnchorNode, aNewAnchorOffset);
  if (NS_WARN_IF(!anchorRange)) {
    return Err(NS_ERROR_FAILURE);
  }

  UniquePtr<mozInlineSpellStatus> status{
      /* The constructor is `private`, hence the explicit allocation. */
      new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
                               std::move(anchorRange), aForceCheck,
                               aNewPositionOffset}};

  // get the root node for checking
  EditorBase* editorBase = status->mSpellChecker->mEditorBase;
  if (NS_WARN_IF(!editorBase)) {
    return Err(NS_ERROR_FAILURE);
  }
  Element* root = editorBase->GetRoot();
  if (NS_WARN_IF(!root)) {
    return Err(NS_ERROR_FAILURE);
  }
  // the anchor node might not be in the DOM anymore, check
  if (root && aOldAnchorNode &&
      !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
    *aContinue = false;
    return status;
  }

  status->mOldNavigationAnchorRange =
      mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
                                                     aOldAnchorOffset);
  if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
    return Err(NS_ERROR_FAILURE);
  }

  *aContinue = true;
  return status;
}

// mozInlineSpellStatus::CreateForSelection
//
//    It is easy for selections since we always re-check the spellcheck
//    selection.

// static
UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
    mozInlineSpellChecker& aSpellChecker) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));

  UniquePtr<mozInlineSpellStatus> status{
      /* The constructor is `private`, hence the explicit allocation. */
      new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
                               nullptr, false, 0}};
  return status;
}

// mozInlineSpellStatus::CreateForRange
//
//    Called to cause the spellcheck of the given range. This will look like
//    a change operation over the given range.

// static
UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
    mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
          ("%s: range=%p", __FUNCTION__, aRange));

  UniquePtr<mozInlineSpellStatus> status{
      /* The constructor is `private`, hence the explicit allocation. */
      new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
                               nullptr, false, 0}};

  status->mRange = aRange;
  return status;
}

// mozInlineSpellStatus::FinishInitOnEvent
//
//    Called when the event is triggered to complete initialization that
//    might require the WordUtil. This calls to the operation-specific
//    initializer, and also sets the range to be the entire element if it
//    is nullptr.
//
//    Watch out: the range might still be nullptr if there is nothing to do,
//    the caller will have to check for this.

nsresult mozInlineSpellStatus::FinishInitOnEvent(
    mozInlineSpellWordUtil& aWordUtil) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
          ("%s: mRange=%p", __FUNCTION__, mRange.get()));

  nsresult rv;
  if (!mRange) {
    rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
                                            getter_AddRefs(mRange));
    NS_ENSURE_SUCCESS(rv, rv);
  }

  switch (mOp) {
    case eOpChange:
      if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
      break;
    case eOpChangeDelete:
      if (mAnchorRange) {
        rv = FillNoCheckRangeFromAnchor(aWordUtil);
        NS_ENSURE_SUCCESS(rv, rv);
      }
      // Delete events will have no range for the changed text (because it was
      // deleted), and CreateForEditorChange will set it to nullptr. Here, we
      // select the entire word to cause any underlining to be removed.
      mRange = mNoCheckRange;
      break;
    case eOpNavigation:
      return FinishNavigationEvent(aWordUtil);
    case eOpSelection:
      // this gets special handling in ResumeCheck
      break;
    case eOpResume:
      // everything should be initialized already in this case
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("Bad operation");
      return NS_ERROR_NOT_INITIALIZED;
  }
  return NS_OK;
}

// mozInlineSpellStatus::FinishNavigationEvent
//
//    This verifies that we need to check the word at the previous caret
//    position. Now that we have the word util, we can find the word belonging
//    to the previous caret position. If the new position is inside that word,
//    we don't want to do anything. In this case, we'll nullptr out mRange so
//    that the caller will know not to continue.
//
//    Notice that we don't set mNoCheckRange. We check here whether the cursor
//    is in the word that needs checking, so it isn't necessary. Plus, the
//    spellchecker isn't guaranteed to only check the given word, and it could
//    remove the underline from the new word under the cursor.

nsresult mozInlineSpellStatus::FinishNavigationEvent(
    mozInlineSpellWordUtil& aWordUtil) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));

  RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
  if (!editorBase) {
    return NS_ERROR_FAILURE;  // editor is gone
  }

  MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");

  if (!mOldNavigationAnchorRange->IsPositioned()) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  // get the DOM position of the old caret, the range should be collapsed
  nsCOMPtr<nsINode> oldAnchorNode =
      mOldNavigationAnchorRange->GetStartContainer();
  uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();

  // find the word on the old caret position, this is the one that we MAY need
  // to check
  RefPtr<nsRange> oldWord;
  nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
                                          static_cast<int32_t>(oldAnchorOffset),
                                          getter_AddRefs(oldWord));
  NS_ENSURE_SUCCESS(rv, rv);

  // aWordUtil.GetRangeForWord flushes pending notifications, check editor
  // again.
  if (!mSpellChecker->mEditorBase) {
    return NS_ERROR_FAILURE;  // editor is gone
  }

  // get the DOM position of the new caret, the range should be collapsed
  nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
  uint32_t newAnchorOffset = mAnchorRange->StartOffset();

  // see if the new cursor position is in the word of the old cursor position
  bool isInRange = false;
  if (!mForceNavigationWordCheck) {
    ErrorResult err;
    isInRange = oldWord->IsPointInRange(
        *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
    if (NS_WARN_IF(err.Failed())) {
      return err.StealNSResult();
    }
  }

  if (isInRange) {
    // caller should give up
    mRange = nullptr;
  } else {
    // check the old word
    mRange = oldWord;

    // Once we've spellchecked the current word, we don't need to spellcheck
    // for any more navigation events.
    mSpellChecker->mNeedsCheckAfterNavigation = false;
  }
  return NS_OK;
}

// mozInlineSpellStatus::FillNoCheckRangeFromAnchor
//
//    Given the mAnchorRange object, computes the range of the word it is on
//    (if any) and fills that range into mNoCheckRange. This is used for
//    change and navigation events to know which word we should skip spell
//    checking on

nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
    mozInlineSpellWordUtil& aWordUtil) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));

  if (!mAnchorRange->IsPositioned()) {
    return NS_ERROR_NOT_INITIALIZED;
  }
  nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
  uint32_t anchorOffset = mAnchorRange->StartOffset();
  return aWordUtil.GetRangeForWord(anchorNode,
                                   static_cast<int32_t>(anchorOffset),
                                   getter_AddRefs(mNoCheckRange));
}

// mozInlineSpellStatus::GetDocument
//
//    Returns the Document object for the document for the
//    current spellchecker.

Document* mozInlineSpellStatus::GetDocument() const {
  if (!mSpellChecker->mEditorBase) {
    return nullptr;
  }

  return mSpellChecker->mEditorBase->GetDocument();
}

// mozInlineSpellStatus::PositionToCollapsedRange
//
//    Converts a given DOM position to a collapsed range covering that
//    position. We use ranges to store DOM positions becuase they stay
//    updated as the DOM is changed.

// static
already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
    nsINode* aNode, uint32_t aOffset) {
  if (NS_WARN_IF(!aNode)) {
    return nullptr;
  }
  IgnoredErrorResult ignoredError;
  RefPtr<nsRange> range =
      nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
  NS_WARNING_ASSERTION(!ignoredError.Failed(),
                       "Creating collapsed range failed");
  return range.forget();
}

// mozInlineSpellResume

class mozInlineSpellResume : public Runnable {
 public:
  mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
                       uint32_t aDisabledAsyncToken)
      : Runnable("mozInlineSpellResume"),
        mDisabledAsyncToken(aDisabledAsyncToken),
        mStatus(std::move(aStatus)) {}

  nsresult Post() {
    nsCOMPtr<nsIRunnable> runnable(this);
    return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
                                           EventQueuePriority::Idle);
  }

  NS_IMETHOD Run() override {
    // Discard the resumption if the spell checker was disabled after the
    // resumption was scheduled.
    if (mDisabledAsyncToken ==
        mStatus->mSpellChecker->GetDisabledAsyncToken()) {
      mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
    }
    return NS_OK;
  }

 private:
  uint32_t mDisabledAsyncToken;
  UniquePtr<mozInlineSpellStatus> mStatus;
};

// Used as the nsIEditorSpellCheck::InitSpellChecker callback.
class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
  ~InitEditorSpellCheckCallback() {}

 public:
  NS_DECL_ISUPPORTS

  explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
      : mSpellChecker(aSpellChecker) {}

  NS_IMETHOD EditorSpellCheckDone() override {
    return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
  }

  void Cancel() { mSpellChecker = nullptr; }

 private:
  RefPtr<mozInlineSpellChecker> mSpellChecker;
};
NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)

NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
  NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
  NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)

NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
                              mCurrentSelectionAnchorNode)

mozInlineSpellChecker::SpellCheckingState
    mozInlineSpellChecker::gCanEnableSpellChecking =
        mozInlineSpellChecker::SpellCheck_Uninitialized;

mozInlineSpellChecker::mozInlineSpellChecker()
    : mNumWordsInSpellSelection(0),
      mMaxNumWordsInSpellSelection(
          StaticPrefs::extensions_spellcheck_inline_max_misspellings()),
      mNumPendingSpellChecks(0),
      mNumPendingUpdateCurrentDictionary(0),
      mDisabledAsyncToken(0),
      mNeedsCheckAfterNavigation(false),
      mFullSpellCheckScheduled(false),
      mIsListeningToEditSubActions(false) {}

mozInlineSpellChecker::~mozInlineSpellChecker() {}

EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
  return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
}

NS_IMETHODIMP
mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
  *aSpellCheck = mSpellCheck;
  NS_IF_ADDREF(*aSpellCheck);
  return NS_OK;
}

NS_IMETHODIMP
mozInlineSpellChecker::Init(nsIEditor* aEditor) {
  mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
  return NS_OK;
}

// mozInlineSpellChecker::Cleanup
//
//    Called by the editor when the editor is going away. This is important
//    because we remove listeners. We do NOT clean up anything else in this
//    function, because it can get called while DoSpellCheck is running!
//
//    Getting the style information there can cause DOM notifications to be
//    flushed, which can cause editors to go away which will bring us here.
//    We can not do anything that will cause DoSpellCheck to freak out.

MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
  mNumWordsInSpellSelection = 0;
  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
  nsresult rv = NS_OK;
  if (!spellCheckSelection) {
    // Ensure we still unregister event listeners (but return a failure code)
    UnregisterEventListeners();
    rv = NS_ERROR_FAILURE;
  } else {
    if (!aDestroyingFrames) {
      spellCheckSelection->RemoveAllRanges(IgnoreErrors());
    }

    rv = UnregisterEventListeners();
  }

  // Notify ENDED observers now.  If we wait to notify as we normally do when
  // these async operations finish, then in the meantime the editor may create
  // another inline spell checker and cause more STARTED and ENDED
  // notifications to be broadcast.  Interleaved notifications for the same
  // editor but different inline spell checkers could easily confuse
  // observers.  They may receive two consecutive STARTED notifications for
  // example, which we guarantee will not happen.

  RefPtr<EditorBase> editorBase = std::move(mEditorBase);
  if (mPendingSpellCheck) {
    // Cancel the pending editor spell checker initialization.
    mPendingSpellCheck = nullptr;
    mPendingInitEditorSpellCheckCallback->Cancel();
    mPendingInitEditorSpellCheckCallback = nullptr;
    ChangeNumPendingSpellChecks(-1, editorBase);
  }

  // Increment this token so that pending UpdateCurrentDictionary calls and
  // scheduled spell checks are discarded when they finish.
  mDisabledAsyncToken++;

  if (mNumPendingUpdateCurrentDictionary > 0) {
    // Account for pending UpdateCurrentDictionary calls.
    ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
                                editorBase);
    mNumPendingUpdateCurrentDictionary = 0;
  }
  if (mNumPendingSpellChecks > 0) {
    // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
    // pending scheduled spell checks.
    ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
  }

  mFullSpellCheckScheduled = false;

  return rv;
}

// mozInlineSpellChecker::CanEnableInlineSpellChecking
//
//    This function can be called to see if it seems likely that we can enable
//    spellchecking before actually creating the InlineSpellChecking objects.
//
//    The problem is that we can't get the dictionary list without actually
//    creating a whole bunch of spellchecking objects. This function tries to
//    do that and caches the result so we don't have to keep allocating those
//    objects if there are no dictionaries or spellchecking.
//
//    Whenever dictionaries are added or removed at runtime, this value must be
//    updated before an observer notification is sent out about the change, to
//    avoid editors getting a wrong cached result.

bool  // static
mozInlineSpellChecker::CanEnableInlineSpellChecking() {
  if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
    gCanEnableSpellChecking = SpellCheck_NotAvailable;

    nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();

    bool canSpellCheck = false;
    nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
    NS_ENSURE_SUCCESS(rv, false);

    if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
  }
  return (gCanEnableSpellChecking == SpellCheck_Available);
}

void  // static
mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
  gCanEnableSpellChecking = SpellCheck_Uninitialized;
}

// mozInlineSpellChecker::RegisterEventListeners
//
//    The inline spell checker listens to mouse events and keyboard navigation
//    events.

nsresult mozInlineSpellChecker::RegisterEventListeners() {
  if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
    return NS_ERROR_FAILURE;
  }

  StartToListenToEditSubActions();

  RefPtr<Document> doc = mEditorBase->GetDocument();
  if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
    return NS_ERROR_FAILURE;
  }
  EventListenerManager* eventListenerManager =
      doc->GetOrCreateListenerManager();
  if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
    return NS_ERROR_FAILURE;
  }
  eventListenerManager->AddEventListenerByType(
      this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
  eventListenerManager->AddEventListenerByType(
      this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
  eventListenerManager->AddEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
  return NS_OK;
}

// mozInlineSpellChecker::UnregisterEventListeners

nsresult mozInlineSpellChecker::UnregisterEventListeners() {
  if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
    return NS_ERROR_FAILURE;
  }

  EndListeningToEditSubActions();

  RefPtr<Document> doc = mEditorBase->GetDocument();
  if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
    return NS_ERROR_FAILURE;
  }
  EventListenerManager* eventListenerManager =
      doc->GetOrCreateListenerManager();
  if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
    return NS_ERROR_FAILURE;
  }
  eventListenerManager->RemoveEventListenerByType(
      this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
  eventListenerManager->RemoveEventListenerByType(
      this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
  eventListenerManager->RemoveEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
  return NS_OK;
}

// mozInlineSpellChecker::GetEnableRealTimeSpell

NS_IMETHODIMP
mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
  NS_ENSURE_ARG_POINTER(aEnabled);
  *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
  return NS_OK;
}

// mozInlineSpellChecker::SetEnableRealTimeSpell

NS_IMETHODIMP
mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
  if (!aEnabled) {
    mSpellCheck = nullptr;
    return Cleanup(false);
  }

  if (mSpellCheck) {
    // spellcheck the current contents. SpellCheckRange doesn't supply a created
    // range to DoSpellCheck, which in our case is the entire range. But this
    // optimization doesn't matter because there is nothing in the spellcheck
    // selection when starting, which triggers a better optimization.
    return SpellCheckRange(nullptr);
  }

  if (mPendingSpellCheck) {
    // The editor spell checker is already being initialized.
    return NS_OK;
  }

  mPendingSpellCheck = new EditorSpellCheck();
  mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);

  mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
  nsresult rv = mPendingSpellCheck->InitSpellChecker(
      mEditorBase, false, mPendingInitEditorSpellCheckCallback);
  if (NS_FAILED(rv)) {
    mPendingSpellCheck = nullptr;
    mPendingInitEditorSpellCheckCallback = nullptr;
    NS_ENSURE_SUCCESS(rv, rv);
  }

  ChangeNumPendingSpellChecks(1);

  return NS_OK;
}

// Called when nsIEditorSpellCheck::InitSpellChecker completes.
nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
  MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");

  // spell checking is enabled, register our event listeners to track navigation
  RegisterEventListeners();

  mSpellCheck = mPendingSpellCheck;
  mPendingSpellCheck = nullptr;
  mPendingInitEditorSpellCheckCallback = nullptr;
  ChangeNumPendingSpellChecks(-1);

  // spellcheck the current contents. SpellCheckRange doesn't supply a created
  // range to DoSpellCheck, which in our case is the entire range. But this
  // optimization doesn't matter because there is nothing in the spellcheck
  // selection when starting, which triggers a better optimization.
  return SpellCheckRange(nullptr);
}

// Changes the number of pending spell checks by the given delta.  If the number
// becomes zero or nonzero, observers are notified.  See NotifyObservers for
// info on the aEditor parameter.
void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
    int32_t aDelta, EditorBase* aEditorBase) {
  int8_t oldNumPending = mNumPendingSpellChecks;
  mNumPendingSpellChecks += aDelta;
  MOZ_ASSERT(mNumPendingSpellChecks >= 0,
             "Unbalanced ChangeNumPendingSpellChecks calls!");
  if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
    NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
  } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
    NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
  }
}

// Broadcasts the given topic to observers.  aEditor is passed to observers if
// nonnull; otherwise mEditorBase is passed.
void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
                                            EditorBase* aEditorBase) {
  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  if (!os) return;
  // XXX Do we need to grab the editor here?  If it's necessary, each observer
  //     should do it instead.
  RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
  os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
                      nullptr);
}

// mozInlineSpellChecker::SpellCheckAfterEditorChange
//
//    Called by the editor when nearly anything happens to change the content.
//
//    The start and end positions specify a range for the thing that happened,
//    but these are usually nullptr, even when you'd think they would be useful
//    because you want the range (for example, pasting). We ignore them in
//    this case.

nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
    EditSubAction aEditSubAction, Selection& aSelection,
    nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
    nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
    uint32_t aEndOffset) {
  nsresult rv;
  if (!mSpellCheck) return NS_OK;  // disabling spell checking is not an error

  // this means something has changed, and we never check the current word,
  // therefore, we should spellcheck for subsequent caret navigations
  mNeedsCheckAfterNavigation = true;

  // the anchor node is the position of the caret
  Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
      mozInlineSpellStatus::CreateForEditorChange(
          *this, aEditSubAction, aSelection.GetAnchorNode(),
          aSelection.AnchorOffset(), aPreviousSelectedNode,
          aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
          aEndOffset);
  if (NS_WARN_IF(res.isErr())) {
    return res.unwrapErr();
  }

  rv = ScheduleSpellCheck(res.unwrap());
  NS_ENSURE_SUCCESS(rv, rv);

  // remember the current caret position after every change
  SaveCurrentSelectionPosition();
  return NS_OK;
}

// mozInlineSpellChecker::SpellCheckRange
//
//    Spellchecks all the words in the given range.
//    Supply a nullptr range and this will check the entire editor.

nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
  if (!mSpellCheck) {
    NS_WARNING_ASSERTION(
        mPendingSpellCheck,
        "Trying to spellcheck, but checking seems to be disabled");
    return NS_ERROR_NOT_INITIALIZED;
  }

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForRange(*this, aRange);
  return ScheduleSpellCheck(std::move(status));
}

// mozInlineSpellChecker::GetMisspelledWord

NS_IMETHODIMP
mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, uint32_t aOffset,
                                         nsRange** newword) {
  if (NS_WARN_IF(!aNode)) {
    return NS_ERROR_INVALID_ARG;
  }
  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
  if (NS_WARN_IF(!spellCheckSelection)) {
    return NS_ERROR_FAILURE;
  }
  return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
}

// mozInlineSpellChecker::ReplaceWord

NS_IMETHODIMP
mozInlineSpellChecker::ReplaceWord(nsINode* aNode, uint32_t aOffset,
                                   const nsAString& aNewWord) {
  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<nsRange> range;
  nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
  NS_ENSURE_SUCCESS(res, res);

  if (!range) {
    return NS_OK;
  }

  // In usual cases, any words shouldn't include line breaks, but technically,
  // they may include and we need to avoid `HTMLTextAreaElement.value` returns
  // \r.  Therefore, we need to handle it here.
  nsString newWord(aNewWord);
  if (mEditorBase->IsTextEditor()) {
    nsContentUtils::PlatformToDOMLineBreaks(newWord);
  }

  // Blink dispatches cancelable `beforeinput` event at collecting misspelled
  // word so that we should allow to dispatch cancelable event.
  RefPtr<EditorBase> editorBase(mEditorBase);
  DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
      newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
  return NS_OK;
}

// mozInlineSpellChecker::AddWordToDictionary

NS_IMETHODIMP
mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);

  nsresult rv = mSpellCheck->AddWordToDictionary(word);
  NS_ENSURE_SUCCESS(rv, rv);

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForSelection(*this);
  return ScheduleSpellCheck(std::move(status));
}

//  mozInlineSpellChecker::RemoveWordFromDictionary

NS_IMETHODIMP
mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);

  nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
  NS_ENSURE_SUCCESS(rv, rv);

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForRange(*this, nullptr);
  return ScheduleSpellCheck(std::move(status));
}

// mozInlineSpellChecker::IgnoreWord

NS_IMETHODIMP
mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);

  nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
  NS_ENSURE_SUCCESS(rv, rv);

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForSelection(*this);
  return ScheduleSpellCheck(std::move(status));
}

// mozInlineSpellChecker::IgnoreWords

NS_IMETHODIMP
mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);

  // add each word to the ignore list and then recheck the document
  for (auto& word : aWordsToIgnore) {
    mSpellCheck->IgnoreWordAllOccurrences(word);
  }

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForSelection(*this);
  return ScheduleSpellCheck(std::move(status));
}

// mozInlineSpellChecker::MakeSpellCheckRange
//
//    Given begin and end positions, this function constructs a range as
//    required for ScheduleSpellCheck. If the start and end nodes are nullptr,
//    then the entire range will be selected, and you can supply -1 as the
//    offset to the end range to select all of that node.
//
//    If the resulting range would be empty, nullptr is put into *aRange and the
//    function succeeds.

nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
                                                    int32_t aStartOffset,
                                                    nsINode* aEndNode,
                                                    int32_t aEndOffset,
                                                    nsRange** aRange) const {
  nsresult rv;
  *aRange = nullptr;

  if (NS_WARN_IF(!mEditorBase)) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<Document> doc = mEditorBase->GetDocument();
  if (NS_WARN_IF(!doc)) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<nsRange> range = nsRange::Create(doc);

  // possibly use full range of the editor
  if (!aStartNode || !aEndNode) {
    Element* domRootElement = mEditorBase->GetRoot();
    if (NS_WARN_IF(!domRootElement)) {
      return NS_ERROR_FAILURE;
    }
    aStartNode = aEndNode = domRootElement;
    aStartOffset = 0;
    aEndOffset = -1;
  }

  if (aEndOffset == -1) {
    // It's hard to say whether it's better to just do nsINode::GetChildCount or
    // get the ChildNodes() and then its length.  The latter is faster if we
    // keep going through this code for the same nodes (because it caches the
    // length).  The former is faster if we keep getting different nodes here...
    //
    // Let's do the thing which can't end up with bad O(N^2) behavior.
    aEndOffset = aEndNode->ChildNodes()->Length();
  }

  // sometimes we are are requested to check an empty range (possibly an empty
  // document). This will result in assertions later.
  if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;

  if (aEndOffset) {
    rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  } else {
    rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
                               RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

  range.swap(*aRange);
  return NS_OK;
}

nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
                                                       int32_t aStartOffset,
                                                       nsINode* aEndNode,
                                                       int32_t aEndOffset) {
  RefPtr<nsRange> range;
  nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
                                    aEndOffset, getter_AddRefs(range));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!range) return NS_OK;  // range is empty: nothing to do

  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForRange(*this, range);
  return ScheduleSpellCheck(std::move(status));
}

// mozInlineSpellChecker::ShouldSpellCheckNode
//
//    There are certain conditions when we don't want to spell check a node. In
//    particular quotations, moz signatures, etc. This routine returns false
//    for these cases.

// static
bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
                                                 nsINode* aNode) {
  MOZ_ASSERT(aNode);
  if (!aNode->IsContent()) return false;

  nsIContent* content = aNode->AsContent();

  if (aEditorBase->IsMailEditor()) {
    nsIContent* parent = content->GetParent();
    while (parent) {
      if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
                                           nsGkAtoms::cite, eIgnoreCase)) {
        return false;
      }
      if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
                                           nsGkAtoms::mozsignature,
                                           eIgnoreCase)) {
        return false;
      }
      if (parent->IsHTMLElement(nsGkAtoms::div) &&
          parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
                                           nsGkAtoms::mozfwcontainer,
                                           eIgnoreCase)) {
        return false;
      }

      parent = parent->GetParent();
    }
  } else {
    // Check spelling only if the node is editable, and GetSpellcheck() is true
    // on the nearest HTMLElement ancestor.
    if (!content->IsEditable()) {
      return false;
    }

    // Make sure that we can always turn on spell checking for inputs/textareas.
    // Note that because of the previous check, at this point we know that the
    // node is editable.
    if (content->IsInNativeAnonymousSubtree()) {
      nsIContent* node = content->GetParent();
      while (node && node->IsInNativeAnonymousSubtree()) {
        node = node->GetParent();
      }
      if (node && node->IsTextControlElement()) {
        return true;
      }
    }

    // Get HTML element ancestor (might be aNode itself, although probably that
    // has to be a text node in real life here)
    nsIContent* parent = content;
    while (!parent->IsHTMLElement()) {
      parent = parent->GetParent();
      if (!parent) {
        return true;
      }
    }

    // See if it's spellcheckable
    return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
  }

  return true;
}

// mozInlineSpellChecker::ScheduleSpellCheck
//
//    This is called by code to do the actual spellchecking. We will set up
//    the proper structures for calls to DoSpellCheck.

nsresult mozInlineSpellChecker::ScheduleSpellCheck(
    UniquePtr<mozInlineSpellStatus>&& aStatus) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
          ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
           mFullSpellCheckScheduled));

  if (mFullSpellCheckScheduled) {
    // Just ignore this; we're going to spell-check everything anyway
    return NS_OK;
  }
  // Cache the value because we are going to move aStatus's ownership to
  // the new created mozInlineSpellResume instance.
  bool isFullSpellCheck = aStatus->IsFullSpellCheck();

  RefPtr<mozInlineSpellResume> resume =
      new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
  NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);

  nsresult rv = resume->Post();
  if (NS_SUCCEEDED(rv)) {
    if (isFullSpellCheck) {
      // We're going to check everything.  Suppress further spell-check attempts
      // until that happens.
      mFullSpellCheckScheduled = true;
    }
    ChangeNumPendingSpellChecks(1);
  }
  return rv;
}

// mozInlineSpellChecker::DoSpellCheckSelection
//
//    Called to re-check all misspelled words. We iterate over all ranges in
//    the selection and call DoSpellCheck on them. This is used when a word
//    is ignored or added to the dictionary: all instances of that word should
//    be removed from the selection.
//
//    FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
//    Typically, checking this small amount of text is relatively fast, but
//    for large numbers of words, a lag may be noticeable.

nsresult mozInlineSpellChecker::DoSpellCheckSelection(
    mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
  nsresult rv;

  // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
  mNumWordsInSpellSelection = 0;

  // Since we could be modifying the ranges for the spellCheckSelection while
  // looping on the spell check selection, keep a separate array of range
  // elements inside the selection
  nsTArray<RefPtr<nsRange>> ranges;

  const uint32_t rangeCount = aSpellCheckSelection->RangeCount();
  for (const uint32_t idx : IntegerRange(rangeCount)) {
    MOZ_ASSERT(aSpellCheckSelection->RangeCount() == rangeCount);
    nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
    MOZ_ASSERT(range);
    if (MOZ_LIKELY(range)) {
      ranges.AppendElement(range);
    }
  }

  // We have saved the ranges above. Clearing the spellcheck selection here
  // isn't necessary (rechecking each word will modify it as necessary) but
  // provides better performance. By ensuring that no ranges need to be
  // removed in DoSpellCheck, we can save checking range inclusion which is
  // slow.
  aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());

  // We use this state object for all calls, and just update its range. Note
  // that we don't need to call FinishInit since we will be filling in the
  // necessary information.
  UniquePtr<mozInlineSpellStatus> status =
      mozInlineSpellStatus::CreateForRange(*this, nullptr);

  bool doneChecking;
  for (uint32_t idx : IntegerRange(rangeCount)) {
    // We can consider this word as "added" since we know it has no spell
    // check range over it that needs to be deleted. All the old ranges
    // were cleared above. We also need to clear the word count so that we
    // check all words instead of stopping early.
    status->mRange = ranges[idx];
    rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
    NS_ENSURE_SUCCESS(rv, rv);
    MOZ_ASSERT(
        doneChecking,
        "We gave the spellchecker one word, but it didn't finish checking?!?!");
  }

  return NS_OK;
}

class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
 public:
  /**
   * @param aStatus must be non-nullptr.
   */

  SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
                    mozInlineSpellWordUtil& aWordUtil,
                    mozilla::dom::Selection& aSpellCheckSelection,
                    const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
                    bool& aDoneChecking)
      : mInlineSpellChecker{aInlineSpellChecker},
        mWordUtil{aWordUtil},
        mSpellCheckSelection{aSpellCheckSelection},
        mStatus{aStatus},
        mDoneChecking{aDoneChecking} {
    MOZ_ASSERT(aStatus);
  }

  [[nodiscard]] nsresult Execute();

 private:
  // Creates an async request to check the words and update the ranges for the
  // misspellings.
  //
  // @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
  // @param aOldRangesForSomeWords ranges from previous spellcheckings which
  //                               might need to be removed. Its length might
  //                               differ from `aWords.Length()`.
  // @param aNodeOffsetRangesForWords One range for each word in aWords. So
  //                                  `aNodeOffsetRangesForWords.Length() ==
  //                                  aWords.Length()`.
  void CheckWordsAndUpdateRangesForMisspellings(
      const nsTArray<nsString>& aWords,
      nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
      nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);

  void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);

  bool ShouldSpellCheckRange(const nsRange& aRange) const;

  bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;

  mozInlineSpellChecker& mInlineSpellChecker;
  mozInlineSpellWordUtil& mWordUtil;
  mozilla::dom::Selection& mSpellCheckSelection;
  const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
  bool& mDoneChecking;
};

bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
    const nsRange& aRange) const {
  if (aRange.Collapsed()) {
    return false;
  }

  nsINode* beginNode = aRange.GetStartContainer();
  nsINode* endNode = aRange.GetEndContainer();

  const nsINode* rootNode = mWordUtil.GetRootNode();
  return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
         beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
         endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
}

bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
    const nsINode& aNode, int32_t aOffset) const {
  ErrorResult erv;
  return mStatus->GetNoCheckRange() &&
         mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
}

void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
    const nsTArray<RefPtr<nsRange>>& aRanges) {
  for (uint32_t i = 0; i < aRanges.Length(); i++) {
    mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
  }
}

// mozInlineSpellChecker::SpellCheckerSlice::Execute
//
//    This function checks words intersecting the given range, excluding those
//    inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
//    will have any spell selection removed (this is used to hide the
//    underlining for the word that the caret is in). aNoCheckRange should be
//    on word boundaries.
//
//    mResume->mCreatedRange is a possibly nullptr range of new text that was
//    inserted.  Inside this range, we don't bother to check whether things are
//    inside the spellcheck selection, which speeds up large paste operations
//    considerably.
//
//    Normal case when editing text by typing
//       h e l l o   w o r k d   h o w   a r e   y o u
//                            ^ caret
//                   [-------] mRange
//                   [-------] mNoCheckRange
//      -> does nothing (range is the same as the no check range)
//
//    Case when pasting:
//             [---------- pasted text ----------]
//       h e l l o   w o r k d   h o w   a r e   y o u
//                                                ^ caret
//                                               [---] aNoCheckRange
//      -> recheck all words in range except those in aNoCheckRange
//
//    If checking is complete, *aDoneChecking will be set. If there is more
//    but we ran out of time, this will be false and the range will be
//    updated with the stuff that still needs checking.

nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));

  mDoneChecking = true;

  if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
    return NS_OK;
  }

  // get the editor for ShouldSpellCheckNode, this may fail in reasonable
  // circumstances since the editor could have gone away
  RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
  if (!editorBase || editorBase->Destroyed()) {
    return NS_ERROR_FAILURE;
  }

  if (!ShouldSpellCheckRange(*mStatus->mRange)) {
    // Just bail out and don't try to spell-check this
    return NS_OK;
  }

  // see if the selection has any ranges, if not, then we can optimize checking
  // range inclusion later (we have no ranges when we are initially checking or
  // when there are no misspelled words yet).
  const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();

  // set the starting DOM position to be the beginning of our range
  if (nsresult rv = mWordUtil.SetPositionAndEnd(
          mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
          mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
      NS_FAILED(rv)) {
    // Just bail out and don't try to spell-check this
    return NS_OK;
  }

  // aWordUtil.SetPosition flushes pending notifications, check editor again.
  if (!mInlineSpellChecker.mEditorBase) {
    return NS_ERROR_FAILURE;
  }

  int32_t wordsChecked = 0;
  PRTime beginTime = PR_Now();

  nsTArray<nsString> normalizedWords;
  nsTArray<RefPtr<nsRange>> oldRangesToRemove;
  nsTArray<NodeOffsetRange> checkRanges;
  mozInlineSpellWordUtil::Word word;
  static const size_t requestChunkSize =
      INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;

  while (mWordUtil.GetNextWord(word)) {
    // get the range for the current word.
    nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
    nsINode* const endNode = word.mNodeOffsetRange.End().Node();
    // TODO: Make them `uint32_t`
    const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
    const int32_t endOffset = word.mNodeOffsetRange.End().Offset();

    // see if we've done enough words in this round and run out of time.
    if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
        PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
      // stop checking, our time limit has been exceeded.
      MOZ_LOG(
          sInlineSpellCheckerLog, LogLevel::Verbose,
          ("%s: we have run out of time, schedule next round.", __FUNCTION__));

      CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
                                               std::move(oldRangesToRemove),
                                               std::move(checkRanges));

      // move the range to encompass the stuff that needs checking.
      nsresult rv = mStatus->mRange->SetStart(
          beginNode, AssertedCast<uint32_t>(beginOffset));
      if (NS_FAILED(rv)) {
        // The range might be unhappy because the beginning is after the
        // end. This is possible when the requested end was in the middle
        // of a word, just ignore this situation and assume we're done.
        return NS_OK;
      }
      mDoneChecking = false;
      return NS_OK;
    }

    MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
            ("%s: got word \"%s\"%s", __FUNCTION__,
             NS_ConvertUTF16toUTF8(word.mText).get(),
             word.mSkipChecking ? " (not checking)" : ""));

    // see if there is a spellcheck range that already intersects the word
    // and remove it. We only need to remove old ranges, so don't bother if
    // there were no ranges when we started out.
    if (originalRangeCount > 0) {
      ErrorResult erv;
      // likewise, if this word is inside new text, we won't bother testing
      if (!mStatus->GetCreatedRange() ||
          !mStatus->GetCreatedRange()->IsPointInRange(
              *beginNode, AssertedCast<uint32_t>(beginOffset), erv)) {
        MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
                ("%s: removing ranges for some interval.", __FUNCTION__));

        nsTArray<RefPtr<nsRange>> ranges;
        mSpellCheckSelection.GetRangesForInterval(
            *beginNode, AssertedCast<uint32_t>(beginOffset), *endNode,
            AssertedCast<uint32_t>(endOffset), true, ranges, erv);
        RETURN_NSRESULT_ON_FAILURE(erv);
        oldRangesToRemove.AppendElements(std::move(ranges));
      }
    }

    // some words are special and don't need checking
    if (word.mSkipChecking) {
      continue;
    }

    // some nodes we don't spellcheck
    if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
      continue;
    }

    // Don't check spelling if we're inside the noCheckRange. This needs to
    // be done after we clear any old selection because the excluded word
    // might have been previously marked.
    //
    // We do a simple check to see if the beginning of our word is in the
    // exclusion range. Because the exclusion range is a multiple of a word,
    // this is sufficient.
    if (IsInNoCheckRange(*beginNode, beginOffset)) {
      continue;
    }

    // check spelling and add to selection if misspelled
    mozInlineSpellWordUtil::NormalizeWord(word.mText);
    normalizedWords.AppendElement(word.mText);
    checkRanges.AppendElement(word.mNodeOffsetRange);
    wordsChecked++;
    if (normalizedWords.Length() >= requestChunkSize) {
      CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
                                               std::move(oldRangesToRemove),
                                               std::move(checkRanges));
      normalizedWords.Clear();
      oldRangesToRemove = {};
      // Set new empty data for spellcheck range in DOM to avoid
      // clang-tidy detection.
      checkRanges = nsTArray<NodeOffsetRange>();
    }
  }

  CheckWordsAndUpdateRangesForMisspellings(
      normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));

  return NS_OK;
}

nsresult mozInlineSpellChecker::DoSpellCheck(
    mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
    const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
  MOZ_ASSERT(aDoneChecking);

  SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
                                      aStatus, *aDoneChecking};

  return spellCheckerSlice.Execute();
}

// An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
class MOZ_RAII AutoChangeNumPendingSpellChecks final {
 public:
  explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
                                           int32_t aDelta)
      : mSpellChecker(aSpellChecker), mDelta(aDelta) {}

  ~AutoChangeNumPendingSpellChecks() {
    mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
  }

 private:
  RefPtr<mozInlineSpellChecker> mSpellChecker;
  int32_t mDelta;
};

void mozInlineSpellChecker::SpellCheckerSlice::
    CheckWordsAndUpdateRangesForMisspellings(
        const nsTArray<nsString>& aWords,
        nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
        nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
          ("%s: aWords.Length()=%i", __FUNCTION__,
           static_cast<int>(aWords.Length())));

  MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());

  // TODO:
  // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
  // to remove ranges more efficiently.

  if (aWords.IsEmpty()) {
    RemoveRanges(aOldRangesForSomeWords);
    return;
  }

  mInlineSpellChecker.ChangeNumPendingSpellChecks(1);

  RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
  RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
  uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
  mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [inlineSpellChecker, spellCheckerSelection,
       nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
       oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
       token](const nsTArray<bool>& aIsMisspelled) {
        if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
          // This result is never used
          return;
        }

        if (!inlineSpellChecker->mEditorBase ||
            inlineSpellChecker->mEditorBase->Destroyed()) {
          return;
        }

        AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);

        if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
          return;
        }

        inlineSpellChecker->UpdateRangesForMisspelledWords(
            nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
            *spellCheckerSelection);
      },
      [inlineSpellChecker, token](nsresult aRv) {
        if (!inlineSpellChecker->mEditorBase ||
            inlineSpellChecker->mEditorBase->Destroyed()) {
          return;
        }

        if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
          // This result is never used
          return;
        }

        inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
      });
}

// mozInlineSpellChecker::ResumeCheck
//
//    Called by the resume event when it fires. We will try to pick up where
//    the last resume left off.

nsresult mozInlineSpellChecker::ResumeCheck(
    UniquePtr<mozInlineSpellStatus>&& aStatus) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));

  // Observers should be notified that spell check has ended only after spell
  // check is done below, but since there are many early returns in this method
  // and the number of pending spell checks must be decremented regardless of
  // whether the spell check actually happens, use this RAII object.
  AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);

  if (aStatus->IsFullSpellCheck()) {
    // Allow posting new spellcheck resume events from inside
    // ResumeCheck, now that we're actually firing.
    MOZ_ASSERT(mFullSpellCheckScheduled,
               "How could this be false? The full spell check is "
               "calling us!!");
    mFullSpellCheckScheduled = false;
  }

  if (!mSpellCheck) return NS_OK;  // spell checking has been turned off

  if (!mEditorBase) {
    return NS_OK;
  }

  Maybe<mozInlineSpellWordUtil> wordUtil{
      mozInlineSpellWordUtil::Create(*mEditorBase)};
  if (!wordUtil) {
    return NS_OK;  // editor doesn't like us, don't assert
  }

  RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
  if (NS_WARN_IF(!spellCheckSelection)) {
    return NS_ERROR_FAILURE;
  }

  nsTArray<nsCString> currentDictionaries;
  nsresult rv = mSpellCheck->GetCurrentDictionaries(currentDictionaries);
  if (NS_FAILED(rv)) {
    MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
            ("%s: no active dictionary.", __FUNCTION__));

    // no active dictionary
    for (const uint32_t index :
         Reversed(IntegerRange(spellCheckSelection->RangeCount()))) {
      RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
      if (MOZ_LIKELY(checkRange)) {
        RemoveRange(spellCheckSelection, checkRange);
      }
    }
    return NS_OK;
  }

  CleanupRangesInSelection(spellCheckSelection);

  rv = aStatus->FinishInitOnEvent(*wordUtil);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!aStatus->mRange) return NS_OK;  // empty range, nothing to do

  bool doneChecking = true;
  if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
    rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
  else
    rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
  return rv;
}

// mozInlineSpellChecker::IsPointInSelection
//
//    Determines if a given (node,offset) point is inside the given
//    selection. If so, the specific range of the selection that
//    intersects is places in *aRange. (There may be multiple disjoint
//    ranges in a selection.)
//
//    If there is no intersection, *aRange will be nullptr.

// static
nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
                                                   nsINode* aNode,
                                                   uint32_t aOffset,
                                                   nsRange** aRange) {
  *aRange = nullptr;

  nsTArray<nsRange*> ranges;
  nsresult rv = aSelection.GetDynamicRangesForIntervalArray(
      aNode, aOffset, aNode, aOffset, true, &ranges);
  NS_ENSURE_SUCCESS(rv, rv);

  if (ranges.Length() == 0) return NS_OK;  // no matches

  // there may be more than one range returned, and we don't know what do
  // do with that, so just get the first one
  NS_ADDREF(*aRange = ranges[0]);
  return NS_OK;
}

nsresult mozInlineSpellChecker::CleanupRangesInSelection(
    Selection* aSelection) {
  // integrity check - remove ranges that have collapsed to nothing. This
  // can happen if the node containing a highlighted word was removed.
  if (!aSelection) return NS_ERROR_FAILURE;

  // TODO: Rewrite this with reversed ranged-loop, it might make this simpler.
  int64_t count = aSelection->RangeCount();
  for (int64_t index = 0; index < count; index++) {
    nsRange* checkRange = aSelection->GetRangeAt(static_cast<uint32_t>(index));
    if (MOZ_LIKELY(checkRange)) {
      if (checkRange->Collapsed()) {
        RemoveRange(aSelection, checkRange);
        index--;
        count--;
      }
    }
  }

  return NS_OK;
}

// mozInlineSpellChecker::RemoveRange
//
//    For performance reasons, we have an upper bound on the number of word
//    ranges  in the spell check selection. When removing a range from the
//    selection, we need to decrement mNumWordsInSpellSelection

nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
                                            nsRange* aRange) {
  MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));

  NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
  NS_ENSURE_ARG_POINTER(aRange);

  ErrorResult rv;
  RefPtr<nsRange> range{aRange};
  RefPtr<Selection> selection{aSpellCheckSelection};
  selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
  if (!rv.Failed()) {
    if (mNumWordsInSpellSelection) {
      mNumWordsInSpellSelection--;
    }
  }

  return rv.StealNSResult();
}

--> --------------------

--> maximum size reached

--> --------------------

96%


¤ Dauer der Verarbeitung: 0.29 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung ist noch experimentell.