michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * This class is called by the editor to handle spellchecking after various michael@0: * events. The main entrypoint is SpellCheckAfterEditorChange, which is called michael@0: * when the text is changed. michael@0: * michael@0: * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM michael@0: * notifications to be flushed when we are called from the editor. This is michael@0: * because the call might originate from a frame, and flushing the michael@0: * notifications might cause that frame to be deleted. michael@0: * michael@0: * Using the WordUtil class to find words causes DOM notifications to be michael@0: * flushed because it asks for style information. As a result, we post an event michael@0: * and do all of the spellchecking in that event handler, which occurs later. michael@0: * We store all DOM pointers in ranges because they are kept up-to-date with michael@0: * DOM changes that may have happened while the event was on the queue. michael@0: * michael@0: * We also allow the spellcheck to be suspended and resumed later. This makes michael@0: * large pastes or initializations with a lot of text not hang the browser UI. michael@0: * michael@0: * An optimization is the mNeedsCheckAfterNavigation flag. This is set to michael@0: * true when we get any change, and false once there is no possibility michael@0: * something changed that we need to check on navigation. Navigation events michael@0: * tend to be a little tricky because we want to check the current word on michael@0: * exit if something has changed. If we navigate inside the word, we don't want michael@0: * to do anything. As a result, this flag is cleared in FinishNavigationEvent michael@0: * when we know that we are checking as a result of navigation. michael@0: */ michael@0: michael@0: #include "mozInlineSpellChecker.h" michael@0: #include "mozInlineSpellWordUtil.h" michael@0: #include "mozISpellI18NManager.h" michael@0: #include "nsCOMPtr.h" michael@0: #include "nsCRT.h" michael@0: #include "nsIDOMNode.h" michael@0: #include "nsIDOMDocument.h" michael@0: #include "nsIDOMElement.h" michael@0: #include "nsIDOMHTMLElement.h" michael@0: #include "nsIDOMMouseEvent.h" michael@0: #include "nsIDOMKeyEvent.h" michael@0: #include "nsIDOMNode.h" michael@0: #include "nsIDOMNodeList.h" michael@0: #include "nsRange.h" michael@0: #include "nsIPlaintextEditor.h" michael@0: #include "nsIPrefBranch.h" michael@0: #include "nsIPrefService.h" michael@0: #include "nsIRunnable.h" michael@0: #include "nsISelection.h" michael@0: #include "nsISelectionPrivate.h" michael@0: #include "nsISelectionController.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsITextServicesFilter.h" michael@0: #include "nsString.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "nsUnicharUtils.h" michael@0: #include "nsIContent.h" michael@0: #include "nsRange.h" michael@0: #include "nsContentUtils.h" michael@0: #include "nsEditor.h" michael@0: #include "mozilla/Services.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsITextControlElement.h" michael@0: #include "prtime.h" michael@0: michael@0: using namespace mozilla::dom; michael@0: michael@0: // Set to spew messages to the console about what is happening. michael@0: //#define DEBUG_INLINESPELL michael@0: michael@0: // the number of milliseconds that we will take at once to do spellchecking michael@0: #define INLINESPELL_CHECK_TIMEOUT 50 michael@0: michael@0: // The number of words to check before we look at the time to see if michael@0: // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending michael@0: // too much time checking the clock. Note that misspelled words count for michael@0: // more than one word in this calculation. michael@0: #define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50 michael@0: michael@0: // This number is the number of checked words a misspelled word counts for michael@0: // when we're checking the time to see if the alloted time is up for michael@0: // spellchecking. Misspelled words take longer to process since we have to michael@0: // create a range, so they count more. The exact number isn't very important michael@0: // since this just controls how often we check the current time. michael@0: #define MISSPELLED_WORD_COUNT_PENALTY 4 michael@0: michael@0: // These notifications are broadcast when spell check starts and ends. STARTED michael@0: // must always be followed by ENDED. michael@0: #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started" michael@0: #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended" michael@0: michael@0: static bool ContentIsDescendantOf(nsINode* aPossibleDescendant, michael@0: nsINode* aPossibleAncestor); michael@0: michael@0: static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; michael@0: michael@0: mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker) michael@0: : mSpellChecker(aSpellChecker), mWordCount(0) michael@0: { michael@0: } michael@0: michael@0: // mozInlineSpellStatus::InitForEditorChange michael@0: // michael@0: // This is the most complicated case. For changes, we need to compute the michael@0: // range of stuff that changed based on the old and new caret positions, michael@0: // as well as use a range possibly provided by the editor (start and end, michael@0: // which are usually nullptr) to get a range with the union of these. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::InitForEditorChange( michael@0: EditAction aAction, michael@0: nsIDOMNode* aAnchorNode, int32_t aAnchorOffset, michael@0: nsIDOMNode* aPreviousNode, int32_t aPreviousOffset, michael@0: nsIDOMNode* aStartNode, int32_t aStartOffset, michael@0: nsIDOMNode* aEndNode, int32_t aEndOffset) michael@0: { michael@0: nsresult rv; michael@0: michael@0: nsCOMPtr doc; michael@0: rv = GetDocument(getter_AddRefs(doc)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // save the anchor point as a range so we can find the current word later michael@0: rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset, michael@0: getter_AddRefs(mAnchorRange)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (aAction == EditAction::deleteSelection) { michael@0: // Deletes are easy, the range is just the current anchor. We set the range michael@0: // to check to be empty, FinishInitOnEvent will fill in the range to be michael@0: // the current word. michael@0: mOp = eOpChangeDelete; michael@0: mRange = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: mOp = eOpChange; michael@0: michael@0: // range to check michael@0: nsCOMPtr prevNode = do_QueryInterface(aPreviousNode); michael@0: NS_ENSURE_STATE(prevNode); michael@0: michael@0: mRange = new nsRange(prevNode); michael@0: michael@0: // ...we need to put the start and end in the correct order michael@0: int16_t cmpResult; michael@0: rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (cmpResult < 0) { michael@0: // previous anchor node is before the current anchor michael@0: rv = mRange->SetStart(aPreviousNode, aPreviousOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = mRange->SetEnd(aAnchorNode, aAnchorOffset); michael@0: } else { michael@0: // previous anchor node is after (or the same as) the current anchor michael@0: rv = mRange->SetStart(aAnchorNode, aAnchorOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = mRange->SetEnd(aPreviousNode, aPreviousOffset); michael@0: } michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // On insert save this range: DoSpellCheck optimizes things in this range. michael@0: // Otherwise, just leave this nullptr. michael@0: if (aAction == EditAction::insertText) michael@0: mCreatedRange = mRange; michael@0: michael@0: // if we were given a range, we need to expand our range to encompass it michael@0: if (aStartNode && aEndNode) { michael@0: rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (cmpResult < 0) { // given range starts before michael@0: rv = mRange->SetStart(aStartNode, aStartOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (cmpResult > 0) { // given range ends after michael@0: rv = mRange->SetEnd(aEndNode, aEndOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatis::InitForNavigation michael@0: // michael@0: // For navigation events, we just need to store the new and old positions. michael@0: // michael@0: // In some cases, we detect that we shouldn't check. If this event should michael@0: // not be processed, *aContinue will be false. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::InitForNavigation( michael@0: bool aForceCheck, int32_t aNewPositionOffset, michael@0: nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset, michael@0: nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset, michael@0: bool* aContinue) michael@0: { michael@0: nsresult rv; michael@0: mOp = eOpNavigation; michael@0: michael@0: mForceNavigationWordCheck = aForceCheck; michael@0: mNewNavigationPositionOffset = aNewPositionOffset; michael@0: michael@0: // get the root node for checking michael@0: nsCOMPtr editor = do_QueryReferent(mSpellChecker->mEditor, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: nsCOMPtr rootElt; michael@0: rv = editor->GetRootElement(getter_AddRefs(rootElt)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // the anchor node might not be in the DOM anymore, check michael@0: nsCOMPtr root = do_QueryInterface(rootElt, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: nsCOMPtr currentAnchor = do_QueryInterface(aOldAnchorNode, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) { michael@0: *aContinue = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsCOMPtr doc; michael@0: rv = GetDocument(getter_AddRefs(doc)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset, michael@0: getter_AddRefs(mOldNavigationAnchorRange)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset, michael@0: getter_AddRefs(mAnchorRange)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: *aContinue = true; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::InitForSelection michael@0: // michael@0: // It is easy for selections since we always re-check the spellcheck michael@0: // selection. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::InitForSelection() michael@0: { michael@0: mOp = eOpSelection; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::InitForRange michael@0: // michael@0: // Called to cause the spellcheck of the given range. This will look like michael@0: // a change operation over the given range. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::InitForRange(nsRange* aRange) michael@0: { michael@0: mOp = eOpChange; michael@0: mRange = aRange; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::FinishInitOnEvent michael@0: // michael@0: // Called when the event is triggered to complete initialization that michael@0: // might require the WordUtil. This calls to the operation-specific michael@0: // initializer, and also sets the range to be the entire element if it michael@0: // is nullptr. michael@0: // michael@0: // Watch out: the range might still be nullptr if there is nothing to do, michael@0: // the caller will have to check for this. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil) michael@0: { michael@0: nsresult rv; michael@0: if (! mRange) { michael@0: rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0, michael@0: getter_AddRefs(mRange)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: switch (mOp) { michael@0: case eOpChange: michael@0: if (mAnchorRange) michael@0: return FillNoCheckRangeFromAnchor(aWordUtil); michael@0: break; michael@0: case eOpChangeDelete: michael@0: if (mAnchorRange) { michael@0: rv = FillNoCheckRangeFromAnchor(aWordUtil); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: // Delete events will have no range for the changed text (because it was michael@0: // deleted), and InitForEditorChange will set it to nullptr. Here, we select michael@0: // the entire word to cause any underlining to be removed. michael@0: mRange = mNoCheckRange; michael@0: break; michael@0: case eOpNavigation: michael@0: return FinishNavigationEvent(aWordUtil); michael@0: case eOpSelection: michael@0: // this gets special handling in ResumeCheck michael@0: break; michael@0: case eOpResume: michael@0: // everything should be initialized already in this case michael@0: break; michael@0: default: michael@0: NS_NOTREACHED("Bad operation"); michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::FinishNavigationEvent michael@0: // michael@0: // This verifies that we need to check the word at the previous caret michael@0: // position. Now that we have the word util, we can find the word belonging michael@0: // to the previous caret position. If the new position is inside that word, michael@0: // we don't want to do anything. In this case, we'll nullptr out mRange so michael@0: // that the caller will know not to continue. michael@0: // michael@0: // Notice that we don't set mNoCheckRange. We check here whether the cursor michael@0: // is in the word that needs checking, so it isn't necessary. Plus, the michael@0: // spellchecker isn't guaranteed to only check the given word, and it could michael@0: // remove the underline from the new word under the cursor. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil) michael@0: { michael@0: nsCOMPtr editor = do_QueryReferent(mSpellChecker->mEditor); michael@0: if (! editor) michael@0: return NS_ERROR_FAILURE; // editor is gone michael@0: michael@0: NS_ASSERTION(mAnchorRange, "No anchor for navigation!"); michael@0: nsCOMPtr newAnchorNode, oldAnchorNode; michael@0: int32_t newAnchorOffset, oldAnchorOffset; michael@0: michael@0: // get the DOM position of the old caret, the range should be collapsed michael@0: nsresult rv = mOldNavigationAnchorRange->GetStartContainer( michael@0: getter_AddRefs(oldAnchorNode)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // find the word on the old caret position, this is the one that we MAY need michael@0: // to check michael@0: nsRefPtr oldWord; michael@0: rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset, michael@0: getter_AddRefs(oldWord)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // aWordUtil.GetRangeForWord flushes pending notifications, check editor again. michael@0: editor = do_QueryReferent(mSpellChecker->mEditor); michael@0: if (! editor) michael@0: return NS_ERROR_FAILURE; // editor is gone michael@0: michael@0: // get the DOM position of the new caret, the range should be collapsed michael@0: rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = mAnchorRange->GetStartOffset(&newAnchorOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // see if the new cursor position is in the word of the old cursor position michael@0: bool isInRange = false; michael@0: if (! mForceNavigationWordCheck) { michael@0: rv = oldWord->IsPointInRange(newAnchorNode, michael@0: newAnchorOffset + mNewNavigationPositionOffset, michael@0: &isInRange); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: if (isInRange) { michael@0: // caller should give up michael@0: mRange = nullptr; michael@0: } else { michael@0: // check the old word michael@0: mRange = oldWord; michael@0: michael@0: // Once we've spellchecked the current word, we don't need to spellcheck michael@0: // for any more navigation events. michael@0: mSpellChecker->mNeedsCheckAfterNavigation = false; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::FillNoCheckRangeFromAnchor michael@0: // michael@0: // Given the mAnchorRange object, computes the range of the word it is on michael@0: // (if any) and fills that range into mNoCheckRange. This is used for michael@0: // change and navigation events to know which word we should skip spell michael@0: // checking on michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::FillNoCheckRangeFromAnchor( michael@0: mozInlineSpellWordUtil& aWordUtil) michael@0: { michael@0: nsCOMPtr anchorNode; michael@0: nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: int32_t anchorOffset; michael@0: rv = mAnchorRange->GetStartOffset(&anchorOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return aWordUtil.GetRangeForWord(anchorNode, anchorOffset, michael@0: getter_AddRefs(mNoCheckRange)); michael@0: } michael@0: michael@0: // mozInlineSpellStatus::GetDocument michael@0: // michael@0: // Returns the nsIDOMDocument object for the document for the michael@0: // current spellchecker. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument) michael@0: { michael@0: nsresult rv; michael@0: *aDocument = nullptr; michael@0: if (! mSpellChecker->mEditor) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: nsCOMPtr editor = do_QueryReferent(mSpellChecker->mEditor, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr domDoc; michael@0: rv = editor->GetDocument(getter_AddRefs(domDoc)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER); michael@0: domDoc.forget(aDocument); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellStatus::PositionToCollapsedRange michael@0: // michael@0: // Converts a given DOM position to a collapsed range covering that michael@0: // position. We use ranges to store DOM positions becuase they stay michael@0: // updated as the DOM is changed. michael@0: michael@0: nsresult michael@0: mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument, michael@0: nsIDOMNode* aNode, int32_t aOffset, nsIDOMRange** aRange) michael@0: { michael@0: *aRange = nullptr; michael@0: nsCOMPtr range; michael@0: nsresult rv = aDocument->CreateRange(getter_AddRefs(range)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = range->SetStart(aNode, aOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = range->SetEnd(aNode, aOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: range.swap(*aRange); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellResume michael@0: michael@0: class mozInlineSpellResume : public nsRunnable michael@0: { michael@0: public: michael@0: mozInlineSpellResume(const mozInlineSpellStatus& aStatus, michael@0: uint32_t aDisabledAsyncToken) michael@0: : mDisabledAsyncToken(aDisabledAsyncToken), mStatus(aStatus) {} michael@0: michael@0: nsresult Post() michael@0: { michael@0: return NS_DispatchToMainThread(this); michael@0: } michael@0: michael@0: NS_IMETHOD Run() michael@0: { michael@0: // Discard the resumption if the spell checker was disabled after the michael@0: // resumption was scheduled. michael@0: if (mDisabledAsyncToken == mStatus.mSpellChecker->mDisabledAsyncToken) { michael@0: mStatus.mSpellChecker->ResumeCheck(&mStatus); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: uint32_t mDisabledAsyncToken; michael@0: mozInlineSpellStatus mStatus; michael@0: }; michael@0: michael@0: // Used as the nsIEditorSpellCheck::InitSpellChecker callback. michael@0: class InitEditorSpellCheckCallback MOZ_FINAL : public nsIEditorSpellCheckCallback michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker) michael@0: : mSpellChecker(aSpellChecker) {} michael@0: michael@0: NS_IMETHOD EditorSpellCheckDone() michael@0: { michael@0: return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK; michael@0: } michael@0: michael@0: void Cancel() michael@0: { michael@0: mSpellChecker = nullptr; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mSpellChecker; michael@0: }; michael@0: NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback) michael@0: michael@0: michael@0: NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker) michael@0: NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker) michael@0: NS_INTERFACE_MAP_ENTRY(nsIEditActionListener) michael@0: NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) michael@0: NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) michael@0: NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) michael@0: NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION(mozInlineSpellChecker, michael@0: mSpellCheck, michael@0: mTreeWalker, michael@0: mCurrentSelectionAnchorNode) michael@0: michael@0: mozInlineSpellChecker::SpellCheckingState michael@0: mozInlineSpellChecker::gCanEnableSpellChecking = michael@0: mozInlineSpellChecker::SpellCheck_Uninitialized; michael@0: michael@0: mozInlineSpellChecker::mozInlineSpellChecker() : michael@0: mNumWordsInSpellSelection(0), michael@0: mMaxNumWordsInSpellSelection(250), michael@0: mNumPendingSpellChecks(0), michael@0: mNumPendingUpdateCurrentDictionary(0), michael@0: mDisabledAsyncToken(0), michael@0: mNeedsCheckAfterNavigation(false), michael@0: mFullSpellCheckScheduled(false) michael@0: { michael@0: nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); michael@0: if (prefs) michael@0: prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); michael@0: mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4; michael@0: } michael@0: michael@0: mozInlineSpellChecker::~mozInlineSpellChecker() michael@0: { michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck) michael@0: { michael@0: *aSpellCheck = mSpellCheck; michael@0: NS_IF_ADDREF(*aSpellCheck); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::Init(nsIEditor *aEditor) michael@0: { michael@0: mEditor = do_GetWeakReference(aEditor); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::Cleanup michael@0: // michael@0: // Called by the editor when the editor is going away. This is important michael@0: // because we remove listeners. We do NOT clean up anything else in this michael@0: // function, because it can get called while DoSpellCheck is running! michael@0: // michael@0: // Getting the style information there can cause DOM notifications to be michael@0: // flushed, which can cause editors to go away which will bring us here. michael@0: // We can not do anything that will cause DoSpellCheck to freak out. michael@0: michael@0: nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) michael@0: { michael@0: mNumWordsInSpellSelection = 0; michael@0: nsCOMPtr spellCheckSelection; michael@0: nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); michael@0: if (NS_FAILED(rv)) { michael@0: // Ensure we still unregister event listeners (but return a failure code) michael@0: UnregisterEventListeners(); michael@0: } else { michael@0: if (!aDestroyingFrames) { michael@0: spellCheckSelection->RemoveAllRanges(); michael@0: } michael@0: michael@0: rv = UnregisterEventListeners(); michael@0: } michael@0: michael@0: // Notify ENDED observers now. If we wait to notify as we normally do when michael@0: // these async operations finish, then in the meantime the editor may create michael@0: // another inline spell checker and cause more STARTED and ENDED michael@0: // notifications to be broadcast. Interleaved notifications for the same michael@0: // editor but different inline spell checkers could easily confuse michael@0: // observers. They may receive two consecutive STARTED notifications for michael@0: // example, which we guarantee will not happen. michael@0: michael@0: nsCOMPtr editor = do_QueryReferent(mEditor); michael@0: if (mPendingSpellCheck) { michael@0: // Cancel the pending editor spell checker initialization. michael@0: mPendingSpellCheck = nullptr; michael@0: mPendingInitEditorSpellCheckCallback->Cancel(); michael@0: mPendingInitEditorSpellCheckCallback = nullptr; michael@0: ChangeNumPendingSpellChecks(-1, editor); michael@0: } michael@0: michael@0: // Increment this token so that pending UpdateCurrentDictionary calls and michael@0: // scheduled spell checks are discarded when they finish. michael@0: mDisabledAsyncToken++; michael@0: michael@0: if (mNumPendingUpdateCurrentDictionary > 0) { michael@0: // Account for pending UpdateCurrentDictionary calls. michael@0: ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, editor); michael@0: mNumPendingUpdateCurrentDictionary = 0; michael@0: } michael@0: if (mNumPendingSpellChecks > 0) { michael@0: // If mNumPendingSpellChecks is still > 0 at this point, the remainder is michael@0: // pending scheduled spell checks. michael@0: ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor); michael@0: } michael@0: michael@0: mEditor = nullptr; michael@0: mFullSpellCheckScheduled = false; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::CanEnableInlineSpellChecking michael@0: // michael@0: // This function can be called to see if it seems likely that we can enable michael@0: // spellchecking before actually creating the InlineSpellChecking objects. michael@0: // michael@0: // The problem is that we can't get the dictionary list without actually michael@0: // creating a whole bunch of spellchecking objects. This function tries to michael@0: // do that and caches the result so we don't have to keep allocating those michael@0: // objects if there are no dictionaries or spellchecking. michael@0: // michael@0: // Whenever dictionaries are added or removed at runtime, this value must be michael@0: // updated before an observer notification is sent out about the change, to michael@0: // avoid editors getting a wrong cached result. michael@0: michael@0: bool // static michael@0: mozInlineSpellChecker::CanEnableInlineSpellChecking() michael@0: { michael@0: nsresult rv; michael@0: if (gCanEnableSpellChecking == SpellCheck_Uninitialized) { michael@0: gCanEnableSpellChecking = SpellCheck_NotAvailable; michael@0: michael@0: nsCOMPtr spellchecker = michael@0: do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv); michael@0: NS_ENSURE_SUCCESS(rv, false); michael@0: michael@0: bool canSpellCheck = false; michael@0: rv = spellchecker->CanSpellCheck(&canSpellCheck); michael@0: NS_ENSURE_SUCCESS(rv, false); michael@0: michael@0: if (canSpellCheck) michael@0: gCanEnableSpellChecking = SpellCheck_Available; michael@0: } michael@0: return (gCanEnableSpellChecking == SpellCheck_Available); michael@0: } michael@0: michael@0: void // static michael@0: mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() michael@0: { michael@0: gCanEnableSpellChecking = SpellCheck_Uninitialized; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::RegisterEventListeners michael@0: // michael@0: // The inline spell checker listens to mouse events and keyboard navigation+ // events. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::RegisterEventListeners() michael@0: { michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); michael@0: michael@0: editor->AddEditActionListener(this); michael@0: michael@0: nsCOMPtr doc; michael@0: nsresult rv = editor->GetDocument(getter_AddRefs(doc)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr piTarget = do_QueryInterface(doc, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this, michael@0: true, false); michael@0: piTarget->AddEventListener(NS_LITERAL_STRING("click"), this, michael@0: false, false); michael@0: piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this, michael@0: false, false); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::UnregisterEventListeners michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::UnregisterEventListeners() michael@0: { michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); michael@0: michael@0: editor->RemoveEditActionListener(this); michael@0: michael@0: nsCOMPtr doc; michael@0: editor->GetDocument(getter_AddRefs(doc)); michael@0: NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER); michael@0: michael@0: nsCOMPtr piTarget = do_QueryInterface(doc); michael@0: NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER); michael@0: michael@0: piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true); michael@0: piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false); michael@0: piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, false); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::GetEnableRealTimeSpell michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aEnabled); michael@0: *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::SetEnableRealTimeSpell michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) michael@0: { michael@0: if (!aEnabled) { michael@0: mSpellCheck = nullptr; michael@0: return Cleanup(false); michael@0: } michael@0: michael@0: if (mSpellCheck) { michael@0: // spellcheck the current contents. SpellCheckRange doesn't supply a created michael@0: // range to DoSpellCheck, which in our case is the entire range. But this michael@0: // optimization doesn't matter because there is nothing in the spellcheck michael@0: // selection when starting, which triggers a better optimization. michael@0: return SpellCheckRange(nullptr); michael@0: } michael@0: michael@0: if (mPendingSpellCheck) { michael@0: // The editor spell checker is already being initialized. michael@0: return NS_OK; michael@0: } michael@0: michael@0: mPendingSpellCheck = michael@0: do_CreateInstance("@mozilla.org/editor/editorspellchecker;1"); michael@0: NS_ENSURE_STATE(mPendingSpellCheck); michael@0: michael@0: nsCOMPtr filter = michael@0: do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1"); michael@0: if (!filter) { michael@0: mPendingSpellCheck = nullptr; michael@0: NS_ENSURE_STATE(filter); michael@0: } michael@0: mPendingSpellCheck->SetFilter(filter); michael@0: michael@0: mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this); michael@0: if (!mPendingInitEditorSpellCheckCallback) { michael@0: mPendingSpellCheck = nullptr; michael@0: NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback); michael@0: } michael@0: michael@0: nsCOMPtr editor = do_QueryReferent(mEditor); michael@0: nsresult rv = mPendingSpellCheck->InitSpellChecker( michael@0: editor, false, mPendingInitEditorSpellCheckCallback); michael@0: if (NS_FAILED(rv)) { michael@0: mPendingSpellCheck = nullptr; michael@0: mPendingInitEditorSpellCheckCallback = nullptr; michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: ChangeNumPendingSpellChecks(1); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Called when nsIEditorSpellCheck::InitSpellChecker completes. michael@0: nsresult michael@0: mozInlineSpellChecker::EditorSpellCheckInited() michael@0: { michael@0: NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!"); michael@0: michael@0: // spell checking is enabled, register our event listeners to track navigation michael@0: RegisterEventListeners(); michael@0: michael@0: mSpellCheck = mPendingSpellCheck; michael@0: mPendingSpellCheck = nullptr; michael@0: mPendingInitEditorSpellCheckCallback = nullptr; michael@0: ChangeNumPendingSpellChecks(-1); michael@0: michael@0: // spellcheck the current contents. SpellCheckRange doesn't supply a created michael@0: // range to DoSpellCheck, which in our case is the entire range. But this michael@0: // optimization doesn't matter because there is nothing in the spellcheck michael@0: // selection when starting, which triggers a better optimization. michael@0: return SpellCheckRange(nullptr); michael@0: } michael@0: michael@0: // Changes the number of pending spell checks by the given delta. If the number michael@0: // becomes zero or nonzero, observers are notified. See NotifyObservers for michael@0: // info on the aEditor parameter. michael@0: void michael@0: mozInlineSpellChecker::ChangeNumPendingSpellChecks(int32_t aDelta, michael@0: nsIEditor* aEditor) michael@0: { michael@0: int8_t oldNumPending = mNumPendingSpellChecks; michael@0: mNumPendingSpellChecks += aDelta; michael@0: NS_ASSERTION(mNumPendingSpellChecks >= 0, michael@0: "Unbalanced ChangeNumPendingSpellChecks calls!"); michael@0: if (oldNumPending == 0 && mNumPendingSpellChecks > 0) { michael@0: NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor); michael@0: } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) { michael@0: NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor); michael@0: } michael@0: } michael@0: michael@0: // Broadcasts the given topic to observers. aEditor is passed to observers if michael@0: // nonnull; otherwise mEditor is passed. michael@0: void michael@0: mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor) michael@0: { michael@0: nsCOMPtr os = mozilla::services::GetObserverService(); michael@0: if (!os) michael@0: return; michael@0: nsCOMPtr editor = aEditor; michael@0: if (!editor) { michael@0: editor = do_QueryReferent(mEditor); michael@0: } michael@0: os->NotifyObservers(editor, aTopic, nullptr); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::SpellCheckAfterEditorChange michael@0: // michael@0: // Called by the editor when nearly anything happens to change the content. michael@0: // michael@0: // The start and end positions specify a range for the thing that happened, michael@0: // but these are usually nullptr, even when you'd think they would be useful michael@0: // because you want the range (for example, pasting). We ignore them in michael@0: // this case. michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::SpellCheckAfterEditorChange( michael@0: int32_t aAction, nsISelection *aSelection, michael@0: nsIDOMNode *aPreviousSelectedNode, int32_t aPreviousSelectedOffset, michael@0: nsIDOMNode *aStartNode, int32_t aStartOffset, michael@0: nsIDOMNode *aEndNode, int32_t aEndOffset) michael@0: { michael@0: nsresult rv; michael@0: NS_ENSURE_ARG_POINTER(aSelection); michael@0: if (!mSpellCheck) michael@0: return NS_OK; // disabling spell checking is not an error michael@0: michael@0: // this means something has changed, and we never check the current word, michael@0: // therefore, we should spellcheck for subsequent caret navigations michael@0: mNeedsCheckAfterNavigation = true; michael@0: michael@0: // the anchor node is the position of the caret michael@0: nsCOMPtr anchorNode; michael@0: rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: int32_t anchorOffset; michael@0: rv = aSelection->GetAnchorOffset(&anchorOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForEditorChange((EditAction)aAction, michael@0: anchorNode, anchorOffset, michael@0: aPreviousSelectedNode, aPreviousSelectedOffset, michael@0: aStartNode, aStartOffset, michael@0: aEndNode, aEndOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = ScheduleSpellCheck(status); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // remember the current caret position after every change michael@0: SaveCurrentSelectionPosition(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::SpellCheckRange michael@0: // michael@0: // Spellchecks all the words in the given range. michael@0: // Supply a nullptr range and this will check the entire editor. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange) michael@0: { michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: nsRange* range = static_cast(aRange); michael@0: nsresult rv = status.InitForRange(range); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::GetMisspelledWord michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, int32_t aOffset, michael@0: nsIDOMRange **newword) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aNode); michael@0: nsCOMPtr spellCheckSelection; michael@0: nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); michael@0: NS_ENSURE_SUCCESS(res, res); michael@0: michael@0: return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::ReplaceWord michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, int32_t aOffset, michael@0: const nsAString &newword) michael@0: { michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); michael@0: NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE); michael@0: michael@0: nsCOMPtr range; michael@0: nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range)); michael@0: NS_ENSURE_SUCCESS(res, res); michael@0: michael@0: if (range) michael@0: { michael@0: editor->BeginTransaction(); michael@0: michael@0: nsCOMPtr selection; michael@0: res = editor->GetSelection(getter_AddRefs(selection)); michael@0: NS_ENSURE_SUCCESS(res, res); michael@0: selection->RemoveAllRanges(); michael@0: selection->AddRange(range); michael@0: editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); michael@0: michael@0: nsCOMPtr textEditor(do_QueryReferent(mEditor)); michael@0: if (textEditor) michael@0: textEditor->InsertText(newword); michael@0: michael@0: editor->EndTransaction(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::AddWordToDictionary michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::AddWordToDictionary(const nsAString &word) michael@0: { michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: nsAutoString wordstr(word); michael@0: nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get()); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForSelection(); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::RemoveWordFromDictionary michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word) michael@0: { michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: nsAutoString wordstr(word); michael@0: nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get()); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForRange(nullptr); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::IgnoreWord michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::IgnoreWord(const nsAString &word) michael@0: { michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: nsAutoString wordstr(word); michael@0: nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get()); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForSelection(); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::IgnoreWords michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::IgnoreWords(const char16_t **aWordsToIgnore, michael@0: uint32_t aCount) michael@0: { michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: // add each word to the ignore list and then recheck the document michael@0: for (uint32_t index = 0; index < aCount; index++) michael@0: mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]); michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: nsresult rv = status.InitForSelection(); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, int32_t aPosition) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent, michael@0: int32_t aPosition, nsresult aResult) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent, michael@0: int32_t aPosition) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent, michael@0: int32_t aPosition, nsresult aResult) michael@0: { michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, int32_t aOffset) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode, michael@0: int32_t aOffset, michael@0: nsIDOMNode *aNewLeftNode, nsresult aResult) michael@0: { michael@0: return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0); michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, michael@0: nsIDOMNode *aParent, nsresult aResult) michael@0: { michael@0: return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0); michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString & aString) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, michael@0: const nsAString & aString, nsresult aResult) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::MakeSpellCheckRange michael@0: // michael@0: // Given begin and end positions, this function constructs a range as michael@0: // required for ScheduleSpellCheck. If the start and end nodes are nullptr, michael@0: // then the entire range will be selected, and you can supply -1 as the michael@0: // offset to the end range to select all of that node. michael@0: // michael@0: // If the resulting range would be empty, nullptr is put into *aRange and the michael@0: // function succeeds. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::MakeSpellCheckRange( michael@0: nsIDOMNode* aStartNode, int32_t aStartOffset, michael@0: nsIDOMNode* aEndNode, int32_t aEndOffset, michael@0: nsRange** aRange) michael@0: { michael@0: nsresult rv; michael@0: *aRange = nullptr; michael@0: michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); michael@0: michael@0: nsCOMPtr doc; michael@0: rv = editor->GetDocument(getter_AddRefs(doc)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); michael@0: michael@0: nsCOMPtr range; michael@0: rv = doc->CreateRange(getter_AddRefs(range)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // possibly use full range of the editor michael@0: nsCOMPtr rootElem; michael@0: if (! aStartNode || ! aEndNode) { michael@0: rv = editor->GetRootElement(getter_AddRefs(rootElem)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: aStartNode = rootElem; michael@0: aStartOffset = 0; michael@0: michael@0: aEndNode = rootElem; michael@0: aEndOffset = -1; michael@0: } michael@0: michael@0: if (aEndOffset == -1) { michael@0: nsCOMPtr childNodes; michael@0: rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: uint32_t childCount; michael@0: rv = childNodes->GetLength(&childCount); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: aEndOffset = childCount; michael@0: } michael@0: michael@0: // sometimes we are are requested to check an empty range (possibly an empty michael@0: // document). This will result in assertions later. michael@0: if (aStartNode == aEndNode && aStartOffset == aEndOffset) michael@0: return NS_OK; michael@0: michael@0: rv = range->SetStart(aStartNode, aStartOffset); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (aEndOffset) michael@0: rv = range->SetEnd(aEndNode, aEndOffset); michael@0: else michael@0: rv = range->SetEndAfter(aEndNode); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: *aRange = static_cast(range.forget().take()); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, michael@0: int32_t aStartOffset, michael@0: nsIDOMNode *aEndNode, michael@0: int32_t aEndOffset) michael@0: { michael@0: nsRefPtr range; michael@0: nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, michael@0: aEndNode, aEndOffset, michael@0: getter_AddRefs(range)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (! range) michael@0: return NS_OK; // range is empty: nothing to do michael@0: michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForRange(range); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return ScheduleSpellCheck(status); michael@0: } michael@0: michael@0: // mozInlineSpellChecker::SkipSpellCheckForNode michael@0: // michael@0: // There are certain conditions when we don't want to spell check a node. In michael@0: // particular quotations, moz signatures, etc. This routine returns false michael@0: // for these cases. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::SkipSpellCheckForNode(nsIEditor* aEditor, michael@0: nsIDOMNode *aNode, michael@0: bool *checkSpelling) michael@0: { michael@0: *checkSpelling = true; michael@0: NS_ENSURE_ARG_POINTER(aNode); michael@0: michael@0: uint32_t flags; michael@0: aEditor->GetFlags(&flags); michael@0: if (flags & nsIPlaintextEditor::eEditorMailMask) michael@0: { michael@0: nsCOMPtr parent; michael@0: aNode->GetParentNode(getter_AddRefs(parent)); michael@0: michael@0: while (parent) michael@0: { michael@0: nsCOMPtr parentElement = do_QueryInterface(parent); michael@0: if (!parentElement) michael@0: break; michael@0: michael@0: nsAutoString parentTagName; michael@0: parentElement->GetTagName(parentTagName); michael@0: michael@0: if (parentTagName.Equals(NS_LITERAL_STRING("blockquote"), nsCaseInsensitiveStringComparator())) michael@0: { michael@0: nsAutoString quotetype; michael@0: parentElement->GetAttribute(NS_LITERAL_STRING("type"), quotetype); michael@0: if (quotetype.Equals(NS_LITERAL_STRING("cite"), nsCaseInsensitiveStringComparator())) michael@0: { michael@0: *checkSpelling = false; michael@0: break; michael@0: } michael@0: } michael@0: else if (parentTagName.Equals(NS_LITERAL_STRING("pre"), nsCaseInsensitiveStringComparator())) michael@0: { michael@0: nsAutoString classname; michael@0: parentElement->GetAttribute(NS_LITERAL_STRING("class"),classname); michael@0: if (classname.Equals(NS_LITERAL_STRING("moz-signature"))) michael@0: *checkSpelling = false; michael@0: } michael@0: michael@0: nsCOMPtr nextParent; michael@0: parent->GetParentNode(getter_AddRefs(nextParent)); michael@0: parent = nextParent; michael@0: } michael@0: } michael@0: else { michael@0: // Check spelling only if the node is editable, and GetSpellcheck() is true michael@0: // on the nearest HTMLElement ancestor. michael@0: nsCOMPtr content = do_QueryInterface(aNode); michael@0: if (!content->IsEditable()) { michael@0: *checkSpelling = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Make sure that we can always turn on spell checking for inputs/textareas. michael@0: // Note that because of the previous check, at this point we know that the michael@0: // node is editable. michael@0: if (content->IsInAnonymousSubtree()) { michael@0: nsCOMPtr node = content->GetParent(); michael@0: while (node && node->IsInNativeAnonymousSubtree()) { michael@0: node = node->GetParent(); michael@0: } michael@0: nsCOMPtr textControl = do_QueryInterface(node); michael@0: if (textControl) { michael@0: *checkSpelling = true; michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: // Get HTML element ancestor (might be aNode itself, although probably that michael@0: // has to be a text node in real life here) michael@0: nsCOMPtr htmlElement = do_QueryInterface(content); michael@0: while (content && !htmlElement) { michael@0: content = content->GetParent(); michael@0: htmlElement = do_QueryInterface(content); michael@0: } michael@0: NS_ASSERTION(htmlElement, "Why do we have no htmlElement?"); michael@0: if (!htmlElement) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: // See if it's spellcheckable michael@0: htmlElement->GetSpellcheck(checkSpelling); michael@0: return NS_OK; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::ScheduleSpellCheck michael@0: // michael@0: // This is called by code to do the actual spellchecking. We will set up michael@0: // the proper structures for calls to DoSpellCheck. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus) michael@0: { michael@0: if (mFullSpellCheckScheduled) { michael@0: // Just ignore this; we're going to spell-check everything anyway michael@0: return NS_OK; michael@0: } michael@0: michael@0: mozInlineSpellResume* resume = michael@0: new mozInlineSpellResume(aStatus, mDisabledAsyncToken); michael@0: NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY); michael@0: michael@0: nsresult rv = resume->Post(); michael@0: if (NS_FAILED(rv)) { michael@0: delete resume; michael@0: } else { michael@0: if (aStatus.IsFullSpellCheck()) { michael@0: // We're going to check everything. Suppress further spell-check attempts michael@0: // until that happens. michael@0: mFullSpellCheckScheduled = true; michael@0: } michael@0: ChangeNumPendingSpellChecks(1); michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::DoSpellCheckSelection michael@0: // michael@0: // Called to re-check all misspelled words. We iterate over all ranges in michael@0: // the selection and call DoSpellCheck on them. This is used when a word michael@0: // is ignored or added to the dictionary: all instances of that word should michael@0: // be removed from the selection. michael@0: // michael@0: // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable. michael@0: // Typically, checking this small amount of text is relatively fast, but michael@0: // for large numbers of words, a lag may be noticeable. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil, michael@0: nsISelection* aSpellCheckSelection, michael@0: mozInlineSpellStatus* aStatus) michael@0: { michael@0: nsresult rv; michael@0: michael@0: // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges. michael@0: mNumWordsInSpellSelection = 0; michael@0: michael@0: // Since we could be modifying the ranges for the spellCheckSelection while michael@0: // looping on the spell check selection, keep a separate array of range michael@0: // elements inside the selection michael@0: nsCOMArray ranges; michael@0: michael@0: int32_t count; michael@0: aSpellCheckSelection->GetRangeCount(&count); michael@0: michael@0: int32_t idx; michael@0: nsCOMPtr checkRange; michael@0: for (idx = 0; idx < count; idx ++) { michael@0: aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange)); michael@0: if (checkRange) { michael@0: if (! ranges.AppendObject(checkRange)) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: } michael@0: michael@0: // We have saved the ranges above. Clearing the spellcheck selection here michael@0: // isn't necessary (rechecking each word will modify it as necessary) but michael@0: // provides better performance. By ensuring that no ranges need to be michael@0: // removed in DoSpellCheck, we can save checking range inclusion which is michael@0: // slow. michael@0: aSpellCheckSelection->RemoveAllRanges(); michael@0: michael@0: // We use this state object for all calls, and just update its range. Note michael@0: // that we don't need to call FinishInit since we will be filling in the michael@0: // necessary information. michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForRange(nullptr); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: bool doneChecking; michael@0: for (idx = 0; idx < count; idx ++) { michael@0: checkRange = ranges[idx]; michael@0: if (checkRange) { michael@0: // We can consider this word as "added" since we know it has no spell michael@0: // check range over it that needs to be deleted. All the old ranges michael@0: // were cleared above. We also need to clear the word count so that we michael@0: // check all words instead of stopping early. michael@0: status.mRange = static_cast(checkRange.get()); michael@0: rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status, michael@0: &doneChecking); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!"); michael@0: michael@0: status.mWordCount = 0; michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::DoSpellCheck michael@0: // michael@0: // This function checks words intersecting the given range, excluding those michael@0: // inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange michael@0: // will have any spell selection removed (this is used to hide the michael@0: // underlining for the word that the caret is in). aNoCheckRange should be michael@0: // on word boundaries. michael@0: // michael@0: // mResume->mCreatedRange is a possibly nullptr range of new text that was michael@0: // inserted. Inside this range, we don't bother to check whether things are michael@0: // inside the spellcheck selection, which speeds up large paste operations michael@0: // considerably. michael@0: // michael@0: // Normal case when editing text by typing michael@0: // h e l l o w o r k d h o w a r e y o u michael@0: // ^ caret michael@0: // [-------] mRange michael@0: // [-------] mNoCheckRange michael@0: // -> does nothing (range is the same as the no check range) michael@0: // michael@0: // Case when pasting: michael@0: // [---------- pasted text ----------] michael@0: // h e l l o w o r k d h o w a r e y o u michael@0: // ^ caret michael@0: // [---] aNoCheckRange michael@0: // -> recheck all words in range except those in aNoCheckRange michael@0: // michael@0: // If checking is complete, *aDoneChecking will be set. If there is more michael@0: // but we ran out of time, this will be false and the range will be michael@0: // updated with the stuff that still needs checking. michael@0: michael@0: nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, michael@0: nsISelection *aSpellCheckSelection, michael@0: mozInlineSpellStatus* aStatus, michael@0: bool* aDoneChecking) michael@0: { michael@0: *aDoneChecking = true; michael@0: michael@0: NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: // get the editor for SkipSpellCheckForNode, this may fail in reasonable michael@0: // circumstances since the editor could have gone away michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: if (! editor) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: bool iscollapsed; michael@0: nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (iscollapsed) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr privSel = do_QueryInterface(aSpellCheckSelection); michael@0: michael@0: // see if the selection has any ranges, if not, then we can optimize checking michael@0: // range inclusion later (we have no ranges when we are initially checking or michael@0: // when there are no misspelled words yet). michael@0: int32_t originalRangeCount; michael@0: rv = aSpellCheckSelection->GetRangeCount(&originalRangeCount); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // set the starting DOM position to be the beginning of our range michael@0: { michael@0: // Scope for the node/offset pairs here so they don't get michael@0: // accidentally used later michael@0: nsINode* beginNode = aStatus->mRange->GetStartParent(); michael@0: int32_t beginOffset = aStatus->mRange->StartOffset(); michael@0: nsINode* endNode = aStatus->mRange->GetEndParent(); michael@0: int32_t endOffset = aStatus->mRange->EndOffset(); michael@0: michael@0: // Now check that we're still looking at a range that's under michael@0: // aWordUtil.GetRootNode() michael@0: nsINode* rootNode = aWordUtil.GetRootNode(); michael@0: if (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) || michael@0: !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) { michael@0: // Just bail out and don't try to spell-check this michael@0: return NS_OK; michael@0: } michael@0: michael@0: aWordUtil.SetEnd(endNode, endOffset); michael@0: aWordUtil.SetPosition(beginNode, beginOffset); michael@0: } michael@0: michael@0: // aWordUtil.SetPosition flushes pending notifications, check editor again. michael@0: editor = do_QueryReferent(mEditor); michael@0: if (! editor) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: int32_t wordsSinceTimeCheck = 0; michael@0: PRTime beginTime = PR_Now(); michael@0: michael@0: nsAutoString wordText; michael@0: nsRefPtr wordRange; michael@0: bool dontCheckWord; michael@0: while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText, michael@0: getter_AddRefs(wordRange), michael@0: &dontCheckWord)) && michael@0: wordRange) { michael@0: wordsSinceTimeCheck ++; michael@0: michael@0: // get the range for the current word. michael@0: // Not using nsINode here for now because we have to call into michael@0: // selection APIs that use nsIDOMNode. :( michael@0: nsCOMPtr beginNode, endNode; michael@0: int32_t beginOffset, endOffset; michael@0: wordRange->GetStartContainer(getter_AddRefs(beginNode)); michael@0: wordRange->GetEndContainer(getter_AddRefs(endNode)); michael@0: wordRange->GetStartOffset(&beginOffset); michael@0: wordRange->GetEndOffset(&endOffset); michael@0: michael@0: #ifdef DEBUG_INLINESPELL michael@0: printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get()); michael@0: if (dontCheckWord) michael@0: printf(" (not checking)"); michael@0: printf("\n"); michael@0: #endif michael@0: michael@0: // see if there is a spellcheck range that already intersects the word michael@0: // and remove it. We only need to remove old ranges, so don't bother if michael@0: // there were no ranges when we started out. michael@0: if (originalRangeCount > 0) { michael@0: // likewise, if this word is inside new text, we won't bother testing michael@0: bool inCreatedRange = false; michael@0: if (aStatus->mCreatedRange) michael@0: aStatus->mCreatedRange->IsPointInRange(beginNode, beginOffset, &inCreatedRange); michael@0: if (! inCreatedRange) { michael@0: nsTArray ranges; michael@0: nsCOMPtr firstNode = do_QueryInterface(beginNode); michael@0: nsCOMPtr lastNode = do_QueryInterface(endNode); michael@0: rv = privSel->GetRangesForIntervalArray(firstNode, beginOffset, michael@0: lastNode, endOffset, michael@0: true, &ranges); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: for (uint32_t i = 0; i < ranges.Length(); i++) michael@0: RemoveRange(aSpellCheckSelection, ranges[i]); michael@0: } michael@0: } michael@0: michael@0: // some words are special and don't need checking michael@0: if (dontCheckWord) michael@0: continue; michael@0: michael@0: // some nodes we don't spellcheck michael@0: bool checkSpelling; michael@0: rv = SkipSpellCheckForNode(editor, beginNode, &checkSpelling); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (!checkSpelling) michael@0: continue; michael@0: michael@0: // Don't check spelling if we're inside the noCheckRange. This needs to michael@0: // be done after we clear any old selection because the excluded word michael@0: // might have been previously marked. michael@0: // michael@0: // We do a simple check to see if the beginning of our word is in the michael@0: // exclusion range. Because the exclusion range is a multiple of a word, michael@0: // this is sufficient. michael@0: if (aStatus->mNoCheckRange) { michael@0: bool inExclusion = false; michael@0: aStatus->mNoCheckRange->IsPointInRange(beginNode, beginOffset, michael@0: &inExclusion); michael@0: if (inExclusion) michael@0: continue; michael@0: } michael@0: michael@0: // check spelling and add to selection if misspelled michael@0: bool isMisspelled; michael@0: aWordUtil.NormalizeWord(wordText); michael@0: rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled); michael@0: if (NS_FAILED(rv)) michael@0: continue; michael@0: michael@0: if (isMisspelled) { michael@0: // misspelled words count extra toward the max michael@0: wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY; michael@0: AddRange(aSpellCheckSelection, wordRange); michael@0: michael@0: aStatus->mWordCount ++; michael@0: if (aStatus->mWordCount >= mMaxMisspellingsPerCheck || michael@0: SpellCheckSelectionIsFull()) michael@0: break; michael@0: } michael@0: michael@0: // see if we've run out of time, only check every N words for perf michael@0: if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) { michael@0: wordsSinceTimeCheck = 0; michael@0: if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) { michael@0: // stop checking, our time limit has been exceeded michael@0: michael@0: // move the range to encompass the stuff that needs checking michael@0: rv = aStatus->mRange->SetStart(endNode, endOffset); michael@0: if (NS_FAILED(rv)) { michael@0: // The range might be unhappy because the beginning is after the michael@0: // end. This is possible when the requested end was in the middle michael@0: // of a word, just ignore this situation and assume we're done. michael@0: return NS_OK; michael@0: } michael@0: *aDoneChecking = false; michael@0: return NS_OK; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // An RAII helper that calls ChangeNumPendingSpellChecks on destruction. michael@0: class AutoChangeNumPendingSpellChecks michael@0: { michael@0: public: michael@0: AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker, michael@0: int32_t aDelta) michael@0: : mSpellChecker(aSpellChecker), mDelta(aDelta) {} michael@0: michael@0: ~AutoChangeNumPendingSpellChecks() michael@0: { michael@0: mSpellChecker->ChangeNumPendingSpellChecks(mDelta); michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mSpellChecker; michael@0: int32_t mDelta; michael@0: }; michael@0: michael@0: // mozInlineSpellChecker::ResumeCheck michael@0: // michael@0: // Called by the resume event when it fires. We will try to pick up where michael@0: // the last resume left off. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus) michael@0: { michael@0: // Observers should be notified that spell check has ended only after spell michael@0: // check is done below, but since there are many early returns in this method michael@0: // and the number of pending spell checks must be decremented regardless of michael@0: // whether the spell check actually happens, use this RAII object. michael@0: AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1); michael@0: michael@0: if (aStatus->IsFullSpellCheck()) { michael@0: // Allow posting new spellcheck resume events from inside michael@0: // ResumeCheck, now that we're actually firing. michael@0: NS_ASSERTION(mFullSpellCheckScheduled, michael@0: "How could this be false? The full spell check is " michael@0: "calling us!!"); michael@0: mFullSpellCheckScheduled = false; michael@0: } michael@0: michael@0: if (! mSpellCheck) michael@0: return NS_OK; // spell checking has been turned off michael@0: michael@0: nsCOMPtr editor = do_QueryReferent(mEditor); michael@0: if (! editor) michael@0: return NS_OK; // editor is gone michael@0: michael@0: mozInlineSpellWordUtil wordUtil; michael@0: nsresult rv = wordUtil.Init(mEditor); michael@0: if (NS_FAILED(rv)) michael@0: return NS_OK; // editor doesn't like us, don't assert michael@0: michael@0: nsCOMPtr spellCheckSelection; michael@0: rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoString currentDictionary; michael@0: rv = mSpellCheck->GetCurrentDictionary(currentDictionary); michael@0: if (NS_FAILED(rv)) { michael@0: // no active dictionary michael@0: int32_t count; michael@0: spellCheckSelection->GetRangeCount(&count); michael@0: for (int32_t index = count - 1; index >= 0; index--) { michael@0: nsCOMPtr checkRange; michael@0: spellCheckSelection->GetRangeAt(index, getter_AddRefs(checkRange)); michael@0: if (checkRange) { michael@0: RemoveRange(spellCheckSelection, checkRange); michael@0: } michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: CleanupRangesInSelection(spellCheckSelection); michael@0: michael@0: rv = aStatus->FinishInitOnEvent(wordUtil); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (! aStatus->mRange) michael@0: return NS_OK; // empty range, nothing to do michael@0: michael@0: bool doneChecking = true; michael@0: if (aStatus->mOp == mozInlineSpellStatus::eOpSelection) michael@0: rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus); michael@0: else michael@0: rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (! doneChecking) michael@0: rv = ScheduleSpellCheck(*aStatus); michael@0: return rv; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::IsPointInSelection michael@0: // michael@0: // Determines if a given (node,offset) point is inside the given michael@0: // selection. If so, the specific range of the selection that michael@0: // intersects is places in *aRange. (There may be multiple disjoint michael@0: // ranges in a selection.) michael@0: // michael@0: // If there is no intersection, *aRange will be nullptr. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection, michael@0: nsIDOMNode *aNode, michael@0: int32_t aOffset, michael@0: nsIDOMRange **aRange) michael@0: { michael@0: *aRange = nullptr; michael@0: michael@0: nsCOMPtr privSel(do_QueryInterface(aSelection)); michael@0: michael@0: nsTArray ranges; michael@0: nsCOMPtr node = do_QueryInterface(aNode); michael@0: nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, aOffset, michael@0: true, &ranges); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (ranges.Length() == 0) michael@0: return NS_OK; // no matches michael@0: michael@0: // there may be more than one range returned, and we don't know what do michael@0: // do with that, so just get the first one michael@0: NS_ADDREF(*aRange = ranges[0]); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection) michael@0: { michael@0: // integrity check - remove ranges that have collapsed to nothing. This michael@0: // can happen if the node containing a highlighted word was removed. michael@0: NS_ENSURE_ARG_POINTER(aSelection); michael@0: michael@0: int32_t count; michael@0: aSelection->GetRangeCount(&count); michael@0: michael@0: for (int32_t index = 0; index < count; index++) michael@0: { michael@0: nsCOMPtr checkRange; michael@0: aSelection->GetRangeAt(index, getter_AddRefs(checkRange)); michael@0: michael@0: if (checkRange) michael@0: { michael@0: bool collapsed; michael@0: checkRange->GetCollapsed(&collapsed); michael@0: if (collapsed) michael@0: { michael@0: RemoveRange(aSelection, checkRange); michael@0: index--; michael@0: count--; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: // mozInlineSpellChecker::RemoveRange michael@0: // michael@0: // For performance reasons, we have an upper bound on the number of word michael@0: // ranges in the spell check selection. When removing a range from the michael@0: // selection, we need to decrement mNumWordsInSpellSelection michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::RemoveRange(nsISelection* aSpellCheckSelection, michael@0: nsIDOMRange* aRange) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aSpellCheckSelection); michael@0: NS_ENSURE_ARG_POINTER(aRange); michael@0: michael@0: nsresult rv = aSpellCheckSelection->RemoveRange(aRange); michael@0: if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection) michael@0: mNumWordsInSpellSelection--; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: // mozInlineSpellChecker::AddRange michael@0: // michael@0: // For performance reasons, we have an upper bound on the number of word michael@0: // ranges we'll add to the spell check selection. Once we reach that upper michael@0: // bound, stop adding the ranges michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection, michael@0: nsIDOMRange* aRange) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aSpellCheckSelection); michael@0: NS_ENSURE_ARG_POINTER(aRange); michael@0: michael@0: nsresult rv = NS_OK; michael@0: michael@0: if (!SpellCheckSelectionIsFull()) michael@0: { michael@0: rv = aSpellCheckSelection->AddRange(aRange); michael@0: if (NS_SUCCEEDED(rv)) michael@0: mNumWordsInSpellSelection++; michael@0: } michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection) michael@0: { michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); michael@0: michael@0: nsCOMPtr selcon; michael@0: nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr spellCheckSelection; michael@0: return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection); michael@0: } michael@0: michael@0: nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() michael@0: { michael@0: nsCOMPtr editor (do_QueryReferent(mEditor)); michael@0: NS_ENSURE_TRUE(editor, NS_OK); michael@0: michael@0: // figure out the old caret position based on the current selection michael@0: nsCOMPtr selection; michael@0: nsresult rv = editor->GetSelection(getter_AddRefs(selection)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: selection->GetFocusOffset(&mCurrentSelectionOffset); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime michael@0: // for XPCOM's rap sheet michael@0: bool // static michael@0: ContentIsDescendantOf(nsINode* aPossibleDescendant, michael@0: nsINode* aPossibleAncestor) michael@0: { michael@0: NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!"); michael@0: NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!"); michael@0: michael@0: do { michael@0: if (aPossibleDescendant == aPossibleAncestor) michael@0: return true; michael@0: aPossibleDescendant = aPossibleDescendant->GetParentNode(); michael@0: } while (aPossibleDescendant); michael@0: michael@0: return false; michael@0: } michael@0: michael@0: // mozInlineSpellChecker::HandleNavigationEvent michael@0: // michael@0: // Acts upon mouse clicks and keyboard navigation changes, spell checking michael@0: // the previous word if the new navigation location moves us to another michael@0: // word. michael@0: // michael@0: // This is complicated by the fact that our mouse events are happening after michael@0: // selection has been changed to account for the mouse click. But keyboard michael@0: // events are happening before the caret selection has changed. Working michael@0: // around this by letting keyboard events setting forceWordSpellCheck to michael@0: // true. aNewPositionOffset also tries to work around this for the michael@0: // DOM_VK_RIGHT and DOM_VK_LEFT cases. michael@0: michael@0: nsresult michael@0: mozInlineSpellChecker::HandleNavigationEvent(bool aForceWordSpellCheck, michael@0: int32_t aNewPositionOffset) michael@0: { michael@0: nsresult rv; michael@0: michael@0: // If we already handled the navigation event and there is no possibility michael@0: // anything has changed since then, we don't have to do anything. This michael@0: // optimization makes a noticeable difference when you hold down a navigation michael@0: // key like Page Down. michael@0: if (! mNeedsCheckAfterNavigation) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr currentAnchorNode = mCurrentSelectionAnchorNode; michael@0: int32_t currentAnchorOffset = mCurrentSelectionOffset; michael@0: michael@0: // now remember the new focus position resulting from the event michael@0: rv = SaveCurrentSelectionPosition(); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: bool shouldPost; michael@0: mozInlineSpellStatus status(this); michael@0: rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset, michael@0: currentAnchorNode, currentAnchorOffset, michael@0: mCurrentSelectionAnchorNode, mCurrentSelectionOffset, michael@0: &shouldPost); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (shouldPost) { michael@0: rv = ScheduleSpellCheck(status); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent) michael@0: { michael@0: nsAutoString eventType; michael@0: aEvent->GetType(eventType); michael@0: michael@0: if (eventType.EqualsLiteral("blur")) { michael@0: return Blur(aEvent); michael@0: } michael@0: if (eventType.EqualsLiteral("click")) { michael@0: return MouseClick(aEvent); michael@0: } michael@0: if (eventType.EqualsLiteral("keypress")) { michael@0: return KeyPress(aEvent); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent) michael@0: { michael@0: // force spellcheck on blur, for instance when tabbing out of a textbox michael@0: HandleNavigationEvent(true); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent) michael@0: { michael@0: nsCOMPtrmouseEvent = do_QueryInterface(aMouseEvent); michael@0: NS_ENSURE_TRUE(mouseEvent, NS_OK); michael@0: michael@0: // ignore any errors from HandleNavigationEvent as we don't want to prevent michael@0: // anyone else from seeing this event. michael@0: int16_t button; michael@0: mouseEvent->GetButton(&button); michael@0: HandleNavigationEvent(button != 0); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent) michael@0: { michael@0: nsCOMPtrkeyEvent = do_QueryInterface(aKeyEvent); michael@0: NS_ENSURE_TRUE(keyEvent, NS_OK); michael@0: michael@0: uint32_t keyCode; michael@0: keyEvent->GetKeyCode(&keyCode); michael@0: michael@0: // we only care about navigation keys that moved selection michael@0: switch (keyCode) michael@0: { michael@0: case nsIDOMKeyEvent::DOM_VK_RIGHT: michael@0: case nsIDOMKeyEvent::DOM_VK_LEFT: michael@0: HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1); michael@0: break; michael@0: case nsIDOMKeyEvent::DOM_VK_UP: michael@0: case nsIDOMKeyEvent::DOM_VK_DOWN: michael@0: case nsIDOMKeyEvent::DOM_VK_HOME: michael@0: case nsIDOMKeyEvent::DOM_VK_END: michael@0: case nsIDOMKeyEvent::DOM_VK_PAGE_UP: michael@0: case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN: michael@0: HandleNavigationEvent(true /* force a spelling correction */); michael@0: break; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback. michael@0: class UpdateCurrentDictionaryCallback MOZ_FINAL : public nsIEditorSpellCheckCallback michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker, michael@0: uint32_t aDisabledAsyncToken) michael@0: : mSpellChecker(aSpellChecker), mDisabledAsyncToken(aDisabledAsyncToken) {} michael@0: michael@0: NS_IMETHOD EditorSpellCheckDone() michael@0: { michael@0: // Ignore this callback if SetEnableRealTimeSpell(false) was called after michael@0: // the UpdateCurrentDictionary call that triggered it. michael@0: return mSpellChecker->mDisabledAsyncToken > mDisabledAsyncToken ? michael@0: NS_OK : michael@0: mSpellChecker->CurrentDictionaryUpdated(); michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mSpellChecker; michael@0: uint32_t mDisabledAsyncToken; michael@0: }; michael@0: NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback) michael@0: michael@0: NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() michael@0: { michael@0: // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell michael@0: // checker is being initialized. Calling UpdateCurrentDictionary on michael@0: // mPendingSpellCheck simply queues the dictionary update after the init. michael@0: nsCOMPtr spellCheck = mSpellCheck ? mSpellCheck : michael@0: mPendingSpellCheck; michael@0: if (!spellCheck) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (NS_FAILED(spellCheck->GetCurrentDictionary(mPreviousDictionary))) { michael@0: mPreviousDictionary.Truncate(); michael@0: } michael@0: michael@0: nsRefPtr cb = michael@0: new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken); michael@0: NS_ENSURE_STATE(cb); michael@0: nsresult rv = spellCheck->UpdateCurrentDictionary(cb); michael@0: if (NS_FAILED(rv)) { michael@0: cb = nullptr; michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: mNumPendingUpdateCurrentDictionary++; michael@0: ChangeNumPendingSpellChecks(1); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes. michael@0: nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() michael@0: { michael@0: mNumPendingUpdateCurrentDictionary--; michael@0: NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0, michael@0: "CurrentDictionaryUpdated called without corresponding " michael@0: "UpdateCurrentDictionary call!"); michael@0: ChangeNumPendingSpellChecks(-1); michael@0: michael@0: nsAutoString currentDictionary; michael@0: if (!mSpellCheck || michael@0: NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) { michael@0: currentDictionary.Truncate(); michael@0: } michael@0: michael@0: if (!mPreviousDictionary.Equals(currentDictionary)) { michael@0: nsresult rv = SpellCheckRange(nullptr); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) michael@0: { michael@0: *aPending = mNumPendingSpellChecks > 0; michael@0: return NS_OK; michael@0: }