diff options
Diffstat (limited to '')
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellChecker.cpp | 2119 |
1 files changed, 2119 insertions, 0 deletions
diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp new file mode 100644 index 0000000000..5d4856d250 --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -0,0 +1,2119 @@ +/* -*- 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" +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif +#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); + ENSURE_SUCCESS(erv, erv.StealNSResult()); + 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.GetRangesForIntervalArray(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--; + } +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->SpellCheckRangeChanged(*aRange); + } +#endif + } + + return rv.StealNSResult(); +} + +struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange { + static bool Equals(const RefPtr<nsRange>& aRange, + const NodeOffsetRange& aNodeOffsetRange) { + return aNodeOffsetRange == *aRange; + } +}; + +void mozInlineSpellChecker::UpdateRangesForMisspelledWords( + const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords, + const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords, + const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) { + MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__)); + + MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length()); + + // When the spellchecker checks text containing words separated by "/", it may + // happen that some words checked in one timeslice, are checked again in a + // following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo" + // and "baz" are checked in one timeslice and two ranges are added for them. + // In the following timeslice "foo" and "baz" are checked again but since + // their corresponding ranges are already in the spellcheck-Selection + // they don't have to be added again and since "foo" and "baz" still contain + // spelling mistakes, they don't have to be removed. + // + // In this case, it's more efficient to keep the existing ranges. + + AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK> + oldRangesMarkedForRemoval; + for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) { + oldRangesMarkedForRemoval.AppendElement(true); + } + + AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK> + nodeOffsetRangesMarkedForAdding; + for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) { + nodeOffsetRangesMarkedForAdding.AppendElement(false); + } + + for (size_t i = 0; i < aIsMisspelled.Length(); i++) { + if (!aIsMisspelled[i]) { + continue; + } + + const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i]; + const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf( + nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{}); + if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex && + aOldRangesForSomeWords[indexOfOldRangeToKeep]->GetSelection() == + &aSpellCheckerSelection /** TODO: warn in case the old range doesn't + belong to the selection. This is not critical, + because other code can always remove them + before the actual spellchecking happens. */) { + MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, + ("%s: reusing old range.", __FUNCTION__)); + + oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false; + } else { + nodeOffsetRangesMarkedForAdding[i] = true; + } + } + + for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) { + if (oldRangesMarkedForRemoval[i]) { + RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]); + } + } + + // Add ranges after removing the marked old ones, so that the Selection can + // become full again. + for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) { + if (nodeOffsetRangesMarkedForAdding[i]) { + RefPtr<nsRange> wordRange = + mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]); + // If we somehow can't make a range for this word, just ignore + // it. + if (wordRange) { + AddRange(&aSpellCheckerSelection, wordRange); + } + } + } +} + +// mozInlineSpellChecker::AddRange +// +// For performance reasons, we have an upper bound on the number of word +// ranges we'll add to the spell check selection. Once we reach that upper +// bound, stop adding the ranges + +nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection, + nsRange* aRange) { + NS_ENSURE_ARG_POINTER(aSpellCheckSelection); + NS_ENSURE_ARG_POINTER(aRange); + + nsresult rv = NS_OK; + + if (!IsSpellCheckSelectionFull()) { + IgnoredErrorResult err; + aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, + err); + if (err.Failed()) { + rv = err.StealNSResult(); + } else { + mNumWordsInSpellSelection++; +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->SpellCheckRangeChanged(*aRange); + } +#endif + } + } + + return rv; +} + +already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() { + if (NS_WARN_IF(!mEditorBase)) { + return nullptr; + } + RefPtr<Selection> selection = + mEditorBase->GetSelection(SelectionType::eSpellCheck); + if (!selection) { + return nullptr; + } + return selection.forget(); +} + +nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() { + if (NS_WARN_IF(!mEditorBase)) { + return NS_OK; // XXX Why NS_OK? + } + + // figure out the old caret position based on the current selection + RefPtr<Selection> selection = mEditorBase->GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + + mCurrentSelectionAnchorNode = selection->GetFocusNode(); + mCurrentSelectionOffset = selection->FocusOffset(); + + return NS_OK; +} + +// mozInlineSpellChecker::HandleNavigationEvent +// +// Acts upon mouse clicks and keyboard navigation changes, spell checking +// the previous word if the new navigation location moves us to another +// word. +// +// This is complicated by the fact that our mouse events are happening after +// selection has been changed to account for the mouse click. But keyboard +// events are happening before the caret selection has changed. Working +// around this by letting keyboard events setting forceWordSpellCheck to +// true. aNewPositionOffset also tries to work around this for the +// DOM_VK_RIGHT and DOM_VK_LEFT cases. + +nsresult mozInlineSpellChecker::HandleNavigationEvent( + bool aForceWordSpellCheck, int32_t aNewPositionOffset) { + nsresult rv; + + // If we already handled the navigation event and there is no possibility + // anything has changed since then, we don't have to do anything. This + // optimization makes a noticeable difference when you hold down a navigation + // key like Page Down. + if (!mNeedsCheckAfterNavigation) return NS_OK; + + nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode; + uint32_t currentAnchorOffset = mCurrentSelectionOffset; + + // now remember the new focus position resulting from the event + rv = SaveCurrentSelectionPosition(); + NS_ENSURE_SUCCESS(rv, rv); + + bool shouldPost; + Result<UniquePtr<mozInlineSpellStatus>, nsresult> res = + mozInlineSpellStatus::CreateForNavigation( + *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode, + currentAnchorOffset, mCurrentSelectionAnchorNode, + mCurrentSelectionOffset, &shouldPost); + + if (NS_WARN_IF(res.isErr())) { + return res.unwrapErr(); + } + + if (shouldPost) { + rv = ScheduleSpellCheck(res.unwrap()); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) { + WidgetEvent* widgetEvent = aEvent->WidgetEventPtr(); + if (MOZ_UNLIKELY(!widgetEvent)) { + return NS_OK; + } + + switch (widgetEvent->mMessage) { + case eBlur: + OnBlur(*aEvent); + return NS_OK; + case eMouseClick: + OnMouseClick(*aEvent); + return NS_OK; + case eKeyDown: + OnKeyDown(*aEvent); + return NS_OK; + default: + MOZ_ASSERT_UNREACHABLE("You must forgot to handle new event type"); + return NS_OK; + } +} + +void mozInlineSpellChecker::OnBlur(Event& aEvent) { + // force spellcheck on blur, for instance when tabbing out of a textbox + HandleNavigationEvent(true); +} + +void mozInlineSpellChecker::OnMouseClick(Event& aMouseEvent) { + MouseEvent* mouseEvent = aMouseEvent.AsMouseEvent(); + if (MOZ_UNLIKELY(!mouseEvent)) { + return; + } + + // ignore any errors from HandleNavigationEvent as we don't want to prevent + // anyone else from seeing this event. + HandleNavigationEvent(mouseEvent->Button() != 0); +} + +void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) { + WidgetKeyboardEvent* widgetKeyboardEvent = + aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); + if (MOZ_UNLIKELY(!widgetKeyboardEvent)) { + return; + } + + // we only care about navigation keys that moved selection + switch (widgetKeyboardEvent->mKeyNameIndex) { + case KEY_NAME_INDEX_ArrowRight: + // XXX Does this work with RTL text? + HandleNavigationEvent(false, 1); + return; + case KEY_NAME_INDEX_ArrowLeft: + // XXX Does this work with RTL text? + HandleNavigationEvent(false, -1); + return; + case KEY_NAME_INDEX_ArrowUp: + case KEY_NAME_INDEX_ArrowDown: + case KEY_NAME_INDEX_Home: + case KEY_NAME_INDEX_End: + case KEY_NAME_INDEX_PageDown: + case KEY_NAME_INDEX_PageUp: + HandleNavigationEvent(true /* force a spelling correction */); + return; + default: + return; + } +} + +// Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback. +class UpdateCurrentDictionaryCallback final + : public nsIEditorSpellCheckCallback { + public: + NS_DECL_ISUPPORTS + + explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker, + uint32_t aDisabledAsyncToken) + : mSpellChecker(aSpellChecker), + mDisabledAsyncToken(aDisabledAsyncToken) {} + + NS_IMETHOD EditorSpellCheckDone() override { + // Ignore this callback if SetEnableRealTimeSpell(false) was called after + // the UpdateCurrentDictionary call that triggered it. + return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken + ? NS_OK + : mSpellChecker->CurrentDictionaryUpdated(); + } + + private: + ~UpdateCurrentDictionaryCallback() {} + + RefPtr<mozInlineSpellChecker> mSpellChecker; + uint32_t mDisabledAsyncToken; +}; +NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback) + +NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() { + // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell + // checker is being initialized. Calling UpdateCurrentDictionary on + // mPendingSpellCheck simply queues the dictionary update after the init. + RefPtr<EditorSpellCheck> spellCheck = + mSpellCheck ? mSpellCheck : mPendingSpellCheck; + if (!spellCheck) { + return NS_OK; + } + + RefPtr<UpdateCurrentDictionaryCallback> cb = + new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken); + NS_ENSURE_STATE(cb); + nsresult rv = spellCheck->UpdateCurrentDictionary(cb); + if (NS_FAILED(rv)) { + cb = nullptr; + return rv; + } + mNumPendingUpdateCurrentDictionary++; + ChangeNumPendingSpellChecks(1); + + return NS_OK; +} + +// Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes. +nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() { + mNumPendingUpdateCurrentDictionary--; + MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0, + "CurrentDictionaryUpdated called without corresponding " + "UpdateCurrentDictionary call!"); + ChangeNumPendingSpellChecks(-1); + + nsresult rv = SpellCheckRange(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) { + *aPending = mNumPendingSpellChecks > 0; + return NS_OK; +} |