/* -*- 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. * * Using the WordUtil class to find words causes DOM notifications to be * flushed because it asks for style information. As a result, we post an event * and do all of the spellchecking in that event handler, which occurs later. * 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/EditAction.h" #include "mozilla/EditorSpellCheck.h" #include "mozilla/EditorUtils.h" #include "mozilla/RangeUtils.h" #include "mozilla/Services.h" #include "mozilla/TextEditor.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 namespace mozilla; using namespace mozilla::dom; using namespace mozilla::ipc; // Set to spew messages to the console about what is happening. //#define DEBUG_INLINESPELL // 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 const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; static const PRTime kMaxSpellCheckTimeInUsec = INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC; mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker) : mSpellChecker(aSpellChecker) {} // mozInlineSpellStatus::InitForEditorChange // // 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. nsresult mozInlineSpellStatus::InitForEditorChange( EditSubAction aEditSubAction, nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode, uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode, uint32_t aEndOffset) { if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) { return NS_ERROR_FAILURE; } // save the anchor point as a range so we can find the current word later mAnchorRange = PositionToCollapsedRange(aAnchorNode, aAnchorOffset); if (NS_WARN_IF(!mAnchorRange)) { return 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(); } if (deleted) { // 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. mOp = eOpChangeDelete; mRange = nullptr; return NS_OK; } mOp = eOpChange; // range to check mRange = nsRange::Create(aPreviousNode); // ...we need to put the start and end in the correct order ErrorResult errorResult; int16_t cmpResult = mAnchorRange->ComparePoint(*aPreviousNode, aPreviousOffset, errorResult); if (NS_WARN_IF(errorResult.Failed())) { return errorResult.StealNSResult(); } nsresult rv; if (cmpResult < 0) { // previous anchor node is before the current anchor rv = mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset, aAnchorNode, aAnchorOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { // previous anchor node is after (or the same as) the current anchor rv = mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset, aPreviousNode, aPreviousOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // On insert save this range: DoSpellCheck optimizes things in this range. // Otherwise, just leave this nullptr. if (aEditSubAction == EditSubAction::eInsertText) { mCreatedRange = mRange; } // if we were given a range, we need to expand our range to encompass it if (aStartNode && aEndNode) { cmpResult = mRange->ComparePoint(*aStartNode, aStartOffset, errorResult); if (NS_WARN_IF(errorResult.Failed())) { return errorResult.StealNSResult(); } if (cmpResult < 0) { // given range starts before rv = mRange->SetStart(aStartNode, aStartOffset); NS_ENSURE_SUCCESS(rv, rv); } cmpResult = mRange->ComparePoint(*aEndNode, aEndOffset, errorResult); if (NS_WARN_IF(errorResult.Failed())) { return errorResult.StealNSResult(); } if (cmpResult > 0) { // given range ends after rv = mRange->SetEnd(aEndNode, aEndOffset); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } // mozInlineSpellStatis::InitForNavigation // // 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. nsresult mozInlineSpellStatus::InitForNavigation( bool aForceCheck, int32_t aNewPositionOffset, nsINode* aOldAnchorNode, uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode, uint32_t aNewAnchorOffset, bool* aContinue) { mOp = eOpNavigation; mForceNavigationWordCheck = aForceCheck; mNewNavigationPositionOffset = aNewPositionOffset; // get the root node for checking TextEditor* textEditor = mSpellChecker->mTextEditor; if (NS_WARN_IF(!textEditor)) { return NS_ERROR_FAILURE; } Element* root = textEditor->GetRoot(); if (NS_WARN_IF(!root)) { return NS_ERROR_FAILURE; } // the anchor node might not be in the DOM anymore, check if (root && aOldAnchorNode && !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) { *aContinue = false; return NS_OK; } mOldNavigationAnchorRange = PositionToCollapsedRange(aOldAnchorNode, aOldAnchorOffset); if (NS_WARN_IF(!mOldNavigationAnchorRange)) { return NS_ERROR_FAILURE; } mAnchorRange = PositionToCollapsedRange(aNewAnchorNode, aNewAnchorOffset); if (NS_WARN_IF(!mAnchorRange)) { return NS_ERROR_FAILURE; } *aContinue = true; return NS_OK; } // mozInlineSpellStatus::InitForSelection // // It is easy for selections since we always re-check the spellcheck // selection. nsresult mozInlineSpellStatus::InitForSelection() { mOp = eOpSelection; return NS_OK; } // mozInlineSpellStatus::InitForRange // // Called to cause the spellcheck of the given range. This will look like // a change operation over the given range. nsresult mozInlineSpellStatus::InitForRange(nsRange* aRange) { mOp = eOpChange; mRange = aRange; return NS_OK; } // 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) { 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 InitForEditorChange 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) { RefPtr textEditor = mSpellChecker->mTextEditor; if (!textEditor) { return NS_ERROR_FAILURE; // editor is gone } NS_ASSERTION(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 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 oldWord; nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode, static_cast(oldAnchorOffset), getter_AddRefs(oldWord)); NS_ENSURE_SUCCESS(rv, rv); // aWordUtil.GetRangeForWord flushes pending notifications, check editor // again. if (!mSpellChecker->mTextEditor) { return NS_ERROR_FAILURE; // editor is gone } // get the DOM position of the new caret, the range should be collapsed nsCOMPtr 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) { if (!mAnchorRange->IsPositioned()) { return NS_ERROR_NOT_INITIALIZED; } nsCOMPtr anchorNode = mAnchorRange->GetStartContainer(); uint32_t anchorOffset = mAnchorRange->StartOffset(); return aWordUtil.GetRangeForWord(anchorNode, static_cast(anchorOffset), getter_AddRefs(mNoCheckRange)); } // mozInlineSpellStatus::GetDocument // // Returns the Document object for the document for the // current spellchecker. Document* mozInlineSpellStatus::GetDocument() const { if (!mSpellChecker->mTextEditor) { return nullptr; } return mSpellChecker->mTextEditor->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. already_AddRefed mozInlineSpellStatus::PositionToCollapsedRange( nsINode* aNode, uint32_t aOffset) { if (NS_WARN_IF(!aNode) || NS_WARN_IF(!GetDocument())) { return nullptr; } IgnoredErrorResult ignoredError; RefPtr 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&& aStatus, uint32_t aDisabledAsyncToken) : Runnable("mozInlineSpellResume"), mDisabledAsyncToken(aDisabledAsyncToken), mStatus(std::move(aStatus)) {} nsresult Post() { nsCOMPtr 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->mDisabledAsyncToken) { mStatus->mSpellChecker->ResumeCheck(std::move(mStatus)); } return NS_OK; } private: uint32_t mDisabledAsyncToken; UniquePtr 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 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, mTextEditor, mSpellCheck, mCurrentSelectionAnchorNode) mozInlineSpellChecker::SpellCheckingState mozInlineSpellChecker::gCanEnableSpellChecking = mozInlineSpellChecker::SpellCheck_Uninitialized; mozInlineSpellChecker::mozInlineSpellChecker() : mNumWordsInSpellSelection(0), mMaxNumWordsInSpellSelection(250), mNumPendingSpellChecks(0), mNumPendingUpdateCurrentDictionary(0), mDisabledAsyncToken(0), mNeedsCheckAfterNavigation(false), mFullSpellCheckScheduled(false), mIsListeningToEditSubActions(false) { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (prefs) prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); } 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) { mTextEditor = aEditor ? aEditor->AsTextEditor() : 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. nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) { mNumWordsInSpellSelection = 0; RefPtr 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 textEditor = std::move(mTextEditor); if (mPendingSpellCheck) { // Cancel the pending editor spell checker initialization. mPendingSpellCheck = nullptr; mPendingInitEditorSpellCheckCallback->Cancel(); mPendingInitEditorSpellCheckCallback = nullptr; ChangeNumPendingSpellChecks(-1, textEditor); } // 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, textEditor); mNumPendingUpdateCurrentDictionary = 0; } if (mNumPendingSpellChecks > 0) { // If mNumPendingSpellChecks is still > 0 at this point, the remainder is // pending scheduled spell checks. ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, textEditor); } 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 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 (NS_WARN_IF(!mTextEditor)) { return NS_ERROR_FAILURE; } StartToListenToEditSubActions(); RefPtr doc = mTextEditor->GetDocument(); if (NS_WARN_IF(!doc)) { return NS_ERROR_FAILURE; } doc->AddEventListener(u"blur"_ns, this, true, false); doc->AddEventListener(u"click"_ns, this, false, false); doc->AddEventListener(u"keypress"_ns, this, false, false); return NS_OK; } // mozInlineSpellChecker::UnregisterEventListeners nsresult mozInlineSpellChecker::UnregisterEventListeners() { if (NS_WARN_IF(!mTextEditor)) { return NS_ERROR_FAILURE; } EndListeningToEditSubActions(); RefPtr doc = mTextEditor->GetDocument(); if (NS_WARN_IF(!doc)) { return NS_ERROR_FAILURE; } doc->RemoveEventListener(u"blur"_ns, this, true); doc->RemoveEventListener(u"click"_ns, this, false); doc->RemoveEventListener(u"keypress"_ns, this, false); 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( mTextEditor, 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() { NS_ASSERTION(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, TextEditor* aTextEditor) { int8_t oldNumPending = mNumPendingSpellChecks; mNumPendingSpellChecks += aDelta; NS_ASSERTION(mNumPendingSpellChecks >= 0, "Unbalanced ChangeNumPendingSpellChecks calls!"); if (oldNumPending == 0 && mNumPendingSpellChecks > 0) { NotifyObservers(INLINESPELL_STARTED_TOPIC, aTextEditor); } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) { NotifyObservers(INLINESPELL_ENDED_TOPIC, aTextEditor); } } // Broadcasts the given topic to observers. aEditor is passed to observers if // nonnull; otherwise mTextEditor is passed. void mozInlineSpellChecker::NotifyObservers(const char* aTopic, TextEditor* aTextEditor) { nsCOMPtr 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 textEditor = aTextEditor ? aTextEditor : mTextEditor.get(); os->NotifyObservers(static_cast(textEditor.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 auto status = MakeUnique(this); rv = status->InitForEditorChange( aEditSubAction, aSelection.GetAnchorNode(), aSelection.AnchorOffset(), aPreviousSelectedNode, aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode, aEndOffset); NS_ENSURE_SUCCESS(rv, rv); rv = ScheduleSpellCheck(std::move(status)); 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; } auto status = MakeUnique(this); nsresult rv = status->InitForRange(aRange); NS_ENSURE_SUCCESS(rv, rv); return ScheduleSpellCheck(std::move(status)); } // mozInlineSpellChecker::GetMisspelledWord NS_IMETHODIMP mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, int32_t aOffset, nsRange** newword) { if (NS_WARN_IF(!aNode)) { return NS_ERROR_INVALID_ARG; } RefPtr 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, int32_t aOffset, const nsAString& aNewWord) { if (NS_WARN_IF(!mTextEditor) || NS_WARN_IF(aNewWord.IsEmpty())) { return NS_ERROR_FAILURE; } RefPtr 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 (!mTextEditor->AsHTMLEditor()) { nsContentUtils::PlatformToDOMLineBreaks(newWord); } // Blink dispatches cancelable `beforeinput` event at collecting misspelled // word so that we should allow to dispatch cancelable event. RefPtr textEditor(mTextEditor); DebugOnly rv = textEditor->ReplaceTextAsAction( newWord, range, TextEditor::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); auto status = MakeUnique(this); rv = status->InitForSelection(); NS_ENSURE_SUCCESS(rv, rv); 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); auto status = MakeUnique(this); rv = status->InitForRange(nullptr); NS_ENSURE_SUCCESS(rv, rv); 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); auto status = MakeUnique(this); rv = status->InitForSelection(); NS_ENSURE_SUCCESS(rv, rv); return ScheduleSpellCheck(std::move(status)); } // mozInlineSpellChecker::IgnoreWords NS_IMETHODIMP mozInlineSpellChecker::IgnoreWords(const nsTArray& 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); } auto status = MakeUnique(this); nsresult rv = status->InitForSelection(); NS_ENSURE_SUCCESS(rv, rv); return ScheduleSpellCheck(std::move(status)); } void mozInlineSpellChecker::DidSplitNode(nsINode* aExistingRightNode, nsINode* aNewLeftNode) { if (!mIsListeningToEditSubActions) { return; } SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0); } void mozInlineSpellChecker::DidJoinNodes(nsINode& aLeftNode, nsINode& aRightNode) { if (!mIsListeningToEditSubActions) { return; } SpellCheckBetweenNodes(&aRightNode, 0, &aRightNode, 0); } // 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) { nsresult rv; *aRange = nullptr; if (NS_WARN_IF(!mTextEditor)) { return NS_ERROR_FAILURE; } RefPtr doc = mTextEditor->GetDocument(); if (NS_WARN_IF(!doc)) { return NS_ERROR_FAILURE; } RefPtr range = nsRange::Create(doc); // possibly use full range of the editor if (!aStartNode || !aEndNode) { Element* domRootElement = mTextEditor->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 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 auto status = MakeUnique(this); rv = status->InitForRange(range); NS_ENSURE_SUCCESS(rv, rv); 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. bool mozInlineSpellChecker::ShouldSpellCheckNode(TextEditor* aTextEditor, nsINode* aNode) { MOZ_ASSERT(aNode); if (!aNode->IsContent()) return false; nsIContent* content = aNode->AsContent(); if (aTextEditor->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(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&& aStatus) { 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 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> ranges; int32_t count = aSpellCheckSelection->RangeCount(); for (int32_t idx = 0; idx < count; idx++) { nsRange* range = aSpellCheckSelection->GetRangeAt(idx); if (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. auto status = MakeUnique(this); rv = status->InitForRange(nullptr); NS_ENSURE_SUCCESS(rv, rv); bool doneChecking; for (int32_t idx = 0; idx < count; idx++) { // 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; } // mozInlineSpellChecker::DoSpellCheck // // 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::DoSpellCheck( mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection, const UniquePtr& aStatus, bool* aDoneChecking) { *aDoneChecking = true; if (NS_WARN_IF(!mSpellCheck)) { return NS_ERROR_NOT_INITIALIZED; } if (SpellCheckSelectionIsFull()) { return NS_OK; } // get the editor for ShouldSpellCheckNode, this may fail in reasonable // circumstances since the editor could have gone away RefPtr textEditor = mTextEditor; if (!textEditor || textEditor->Destroyed()) { return NS_ERROR_FAILURE; } if (aStatus->mRange->Collapsed()) 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). int32_t originalRangeCount = aSpellCheckSelection->RangeCount(); // set the starting DOM position to be the beginning of our range { // Scope for the node/offset pairs here so they don't get // accidentally used later nsINode* beginNode = aStatus->mRange->GetStartContainer(); int32_t beginOffset = aStatus->mRange->StartOffset(); nsINode* endNode = aStatus->mRange->GetEndContainer(); int32_t endOffset = aStatus->mRange->EndOffset(); // Now check that we're still looking at a range that's under // aWordUtil.GetRootNode() nsINode* rootNode = aWordUtil.GetRootNode(); if (!beginNode->IsInComposedDoc() || !endNode->IsInComposedDoc() || !beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) || !endNode->IsShadowIncludingInclusiveDescendantOf(rootNode)) { // Just bail out and don't try to spell-check this return NS_OK; } nsresult rv = aWordUtil.SetPositionAndEnd(beginNode, beginOffset, endNode, endOffset); if (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 (!mTextEditor) { return NS_ERROR_FAILURE; } int32_t wordsChecked = 0; PRTime beginTime = PR_Now(); nsTArray words; nsTArray checkRanges; nsAutoString wordText; NodeOffsetRange wordNodeOffsetRange; bool dontCheckWord; // XXX Spellchecker API isn't async on chrome process. static const size_t requestChunkSize = XRE_IsContentProcess() ? INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK : 1; while ( aWordUtil.GetNextWord(wordText, &wordNodeOffsetRange, &dontCheckWord)) { // get the range for the current word. nsINode* beginNode = wordNodeOffsetRange.Begin().Node(); nsINode* endNode = wordNodeOffsetRange.End().Node(); int32_t beginOffset = wordNodeOffsetRange.Begin().Offset(); int32_t endOffset = wordNodeOffsetRange.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. #ifdef DEBUG_INLINESPELL printf("We have run out of the time, schedule next round.\n"); #endif CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), std::move(checkRanges)); // move the range to encompass the stuff that needs checking. nsresult rv = aStatus->mRange->SetStart(beginNode, 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; } *aDoneChecking = false; return NS_OK; } #ifdef DEBUG_INLINESPELL printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get()); if (dontCheckWord) printf(" (not checking)"); printf("\n"); #endif ErrorResult erv; // 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) { // likewise, if this word is inside new text, we won't bother testing if (!aStatus->mCreatedRange || !aStatus->mCreatedRange->IsPointInRange( *beginNode, beginOffset, erv)) { nsTArray> ranges; aSpellCheckSelection->GetRangesForInterval( *beginNode, beginOffset, *endNode, endOffset, true, ranges, erv); ENSURE_SUCCESS(erv, erv.StealNSResult()); for (uint32_t i = 0; i < ranges.Length(); i++) RemoveRange(aSpellCheckSelection, ranges[i]); } } // some words are special and don't need checking if (dontCheckWord) continue; // some nodes we don't spellcheck if (!ShouldSpellCheckNode(textEditor, 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 (aStatus->mNoCheckRange && aStatus->mNoCheckRange->IsPointInRange(*beginNode, beginOffset, erv)) { continue; } // check spelling and add to selection if misspelled mozInlineSpellWordUtil::NormalizeWord(wordText); words.AppendElement(wordText); checkRanges.AppendElement(wordNodeOffsetRange); wordsChecked++; if (words.Length() >= requestChunkSize) { CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), std::move(checkRanges)); // Set new empty data for spellcheck words and range in DOM to avoid // clang-tidy detection. words = nsTArray(); checkRanges = nsTArray(); } } CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), std::move(checkRanges)); return NS_OK; } // 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 mSpellChecker; int32_t mDelta; }; void mozInlineSpellChecker::CheckCurrentWordsNoSuggest( Selection* aSpellCheckSelection, nsTArray&& aWords, nsTArray&& aRanges) { MOZ_ASSERT(aWords.Length() == aRanges.Length()); if (aWords.IsEmpty()) { return; } ChangeNumPendingSpellChecks(1); RefPtr self = this; RefPtr spellCheckerSelection = aSpellCheckSelection; uint32_t token = mDisabledAsyncToken; nsTArray words = std::move(aWords); mSpellCheck->CheckCurrentWordsNoSuggest(words)->Then( GetMainThreadSerialEventTarget(), __func__, [self, spellCheckerSelection, ranges = std::move(aRanges), token](const nsTArray& aIsMisspelled) { if (token != self->mDisabledAsyncToken) { // This result is never used return; } if (!self->mTextEditor || self->mTextEditor->Destroyed()) { return; } AutoChangeNumPendingSpellChecks pendingChecks(self, -1); if (self->SpellCheckSelectionIsFull()) { return; } for (size_t i = 0; i < aIsMisspelled.Length(); i++) { if (!aIsMisspelled[i]) { continue; } RefPtr wordRange = mozInlineSpellWordUtil::MakeRange(ranges[i]); // If we somehow can't make a range for this word, just ignore // it. if (wordRange) { self->AddRange(spellCheckerSelection, wordRange); } } }, [self, token](nsresult aRv) { if (!self->mTextEditor || self->mTextEditor->Destroyed()) { return; } if (token != self->mDisabledAsyncToken) { // This result is never used return; } self->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&& aStatus) { // 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. NS_ASSERTION(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 (!mTextEditor) { return NS_OK; } mozInlineSpellWordUtil wordUtil; nsresult rv = wordUtil.Init(mTextEditor); if (NS_FAILED(rv)) return NS_OK; // editor doesn't like us, don't assert RefPtr spellCheckSelection = GetSpellCheckSelection(); if (NS_WARN_IF(!spellCheckSelection)) { return NS_ERROR_FAILURE; } nsAutoCString currentDictionary; rv = mSpellCheck->GetCurrentDictionary(currentDictionary); if (NS_FAILED(rv)) { // no active dictionary int32_t count = spellCheckSelection->RangeCount(); for (int32_t index = count - 1; index >= 0; index--) { RefPtr checkRange = spellCheckSelection->GetRangeAt(index); if (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->mOp == 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. nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection, nsINode* aNode, int32_t aOffset, nsRange** aRange) { *aRange = nullptr; nsTArray 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; int32_t count = aSelection->RangeCount(); for (int32_t index = 0; index < count; index++) { nsRange* checkRange = aSelection->GetRangeAt(index); if (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) { NS_ENSURE_ARG_POINTER(aSpellCheckSelection); NS_ENSURE_ARG_POINTER(aRange); ErrorResult rv; RefPtr range{aRange}; RefPtr selection{aSpellCheckSelection}; selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv); if (!rv.Failed() && mNumWordsInSpellSelection) mNumWordsInSpellSelection--; return rv.StealNSResult(); } // 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 (!SpellCheckSelectionIsFull()) { IgnoredErrorResult err; aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, err); if (err.Failed()) { rv = err.StealNSResult(); } else { mNumWordsInSpellSelection++; } } return rv; } already_AddRefed mozInlineSpellChecker::GetSpellCheckSelection() { if (NS_WARN_IF(!mTextEditor)) { return nullptr; } RefPtr selection = mTextEditor->GetSelection(SelectionType::eSpellCheck); if (!selection) { return nullptr; } return selection.forget(); } nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() { if (NS_WARN_IF(!mTextEditor)) { return NS_OK; // XXX Why NS_OK? } // figure out the old caret position based on the current selection RefPtr selection = mTextEditor->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 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; auto status = MakeUnique(this); rv = status->InitForNavigation(aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode, currentAnchorOffset, mCurrentSelectionAnchorNode, mCurrentSelectionOffset, &shouldPost); NS_ENSURE_SUCCESS(rv, rv); if (shouldPost) { rv = ScheduleSpellCheck(std::move(status)); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) { nsAutoString eventType; aEvent->GetType(eventType); if (eventType.EqualsLiteral("blur")) { return OnBlur(aEvent); } if (eventType.EqualsLiteral("click")) { return OnMouseClick(aEvent); } if (eventType.EqualsLiteral("keypress")) { return OnKeyPress(aEvent); } return NS_OK; } nsresult mozInlineSpellChecker::OnBlur(Event* aEvent) { // force spellcheck on blur, for instance when tabbing out of a textbox HandleNavigationEvent(true); return NS_OK; } nsresult mozInlineSpellChecker::OnMouseClick(Event* aMouseEvent) { MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); NS_ENSURE_TRUE(mouseEvent, NS_OK); // ignore any errors from HandleNavigationEvent as we don't want to prevent // anyone else from seeing this event. HandleNavigationEvent(mouseEvent->Button() != 0); return NS_OK; } nsresult mozInlineSpellChecker::OnKeyPress(Event* aKeyEvent) { RefPtr keyEvent = aKeyEvent->AsKeyboardEvent(); NS_ENSURE_TRUE(keyEvent, NS_OK); uint32_t keyCode = keyEvent->KeyCode(); // we only care about navigation keys that moved selection switch (keyCode) { case KeyboardEvent_Binding::DOM_VK_RIGHT: case KeyboardEvent_Binding::DOM_VK_LEFT: HandleNavigationEvent( false, keyCode == KeyboardEvent_Binding::DOM_VK_RIGHT ? 1 : -1); break; case KeyboardEvent_Binding::DOM_VK_UP: case KeyboardEvent_Binding::DOM_VK_DOWN: case KeyboardEvent_Binding::DOM_VK_HOME: case KeyboardEvent_Binding::DOM_VK_END: case KeyboardEvent_Binding::DOM_VK_PAGE_UP: case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN: HandleNavigationEvent(true /* force a spelling correction */); break; } return NS_OK; } // 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->mDisabledAsyncToken > mDisabledAsyncToken ? NS_OK : mSpellChecker->CurrentDictionaryUpdated(); } private: ~UpdateCurrentDictionaryCallback() {} RefPtr 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 spellCheck = mSpellCheck ? mSpellCheck : mPendingSpellCheck; if (!spellCheck) { return NS_OK; } RefPtr 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--; NS_ASSERTION(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; }