extensions/spellcheck/src/mozInlineSpellChecker.cpp

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 /**
     7  * This class is called by the editor to handle spellchecking after various
     8  * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
     9  * when the text is changed.
    10  *
    11  * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
    12  * notifications to be flushed when we are called from the editor. This is
    13  * because the call might originate from a frame, and flushing the
    14  * notifications might cause that frame to be deleted.
    15  *
    16  * Using the WordUtil class to find words causes DOM notifications to be
    17  * flushed because it asks for style information. As a result, we post an event
    18  * and do all of the spellchecking in that event handler, which occurs later.
    19  * We store all DOM pointers in ranges because they are kept up-to-date with
    20  * DOM changes that may have happened while the event was on the queue.
    21  *
    22  * We also allow the spellcheck to be suspended and resumed later. This makes
    23  * large pastes or initializations with a lot of text not hang the browser UI.
    24  *
    25  * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
    26  * true when we get any change, and false once there is no possibility
    27  * something changed that we need to check on navigation. Navigation events
    28  * tend to be a little tricky because we want to check the current word on
    29  * exit if something has changed. If we navigate inside the word, we don't want
    30  * to do anything. As a result, this flag is cleared in FinishNavigationEvent
    31  * when we know that we are checking as a result of navigation.
    32  */
    34 #include "mozInlineSpellChecker.h"
    35 #include "mozInlineSpellWordUtil.h"
    36 #include "mozISpellI18NManager.h"
    37 #include "nsCOMPtr.h"
    38 #include "nsCRT.h"
    39 #include "nsIDOMNode.h"
    40 #include "nsIDOMDocument.h"
    41 #include "nsIDOMElement.h"
    42 #include "nsIDOMHTMLElement.h"
    43 #include "nsIDOMMouseEvent.h"
    44 #include "nsIDOMKeyEvent.h"
    45 #include "nsIDOMNode.h"
    46 #include "nsIDOMNodeList.h"
    47 #include "nsRange.h"
    48 #include "nsIPlaintextEditor.h"
    49 #include "nsIPrefBranch.h"
    50 #include "nsIPrefService.h"
    51 #include "nsIRunnable.h"
    52 #include "nsISelection.h"
    53 #include "nsISelectionPrivate.h"
    54 #include "nsISelectionController.h"
    55 #include "nsIServiceManager.h"
    56 #include "nsITextServicesFilter.h"
    57 #include "nsString.h"
    58 #include "nsThreadUtils.h"
    59 #include "nsUnicharUtils.h"
    60 #include "nsIContent.h"
    61 #include "nsRange.h"
    62 #include "nsContentUtils.h"
    63 #include "nsEditor.h"
    64 #include "mozilla/Services.h"
    65 #include "nsIObserverService.h"
    66 #include "nsITextControlElement.h"
    67 #include "prtime.h" 
    69 using namespace mozilla::dom;
    71 // Set to spew messages to the console about what is happening.
    72 //#define DEBUG_INLINESPELL
    74 // the number of milliseconds that we will take at once to do spellchecking
    75 #define INLINESPELL_CHECK_TIMEOUT 50
    77 // The number of words to check before we look at the time to see if
    78 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending
    79 // too much time checking the clock. Note that misspelled words count for
    80 // more than one word in this calculation.
    81 #define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50
    83 // This number is the number of checked words a misspelled word counts for
    84 // when we're checking the time to see if the alloted time is up for
    85 // spellchecking. Misspelled words take longer to process since we have to
    86 // create a range, so they count more. The exact number isn't very important
    87 // since this just controls how often we check the current time.
    88 #define MISSPELLED_WORD_COUNT_PENALTY 4
    90 // These notifications are broadcast when spell check starts and ends.  STARTED
    91 // must always be followed by ENDED.
    92 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
    93 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
    95 static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
    96                                     nsINode* aPossibleAncestor);
    98 static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings";
   100 mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
   101     : mSpellChecker(aSpellChecker), mWordCount(0)
   102 {
   103 }
   105 // mozInlineSpellStatus::InitForEditorChange
   106 //
   107 //    This is the most complicated case. For changes, we need to compute the
   108 //    range of stuff that changed based on the old and new caret positions,
   109 //    as well as use a range possibly provided by the editor (start and end,
   110 //    which are usually nullptr) to get a range with the union of these.
   112 nsresult
   113 mozInlineSpellStatus::InitForEditorChange(
   114     EditAction aAction,
   115     nsIDOMNode* aAnchorNode, int32_t aAnchorOffset,
   116     nsIDOMNode* aPreviousNode, int32_t aPreviousOffset,
   117     nsIDOMNode* aStartNode, int32_t aStartOffset,
   118     nsIDOMNode* aEndNode, int32_t aEndOffset)
   119 {
   120   nsresult rv;
   122   nsCOMPtr<nsIDOMDocument> doc;
   123   rv = GetDocument(getter_AddRefs(doc));
   124   NS_ENSURE_SUCCESS(rv, rv);
   126   // save the anchor point as a range so we can find the current word later
   127   rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset,
   128                                 getter_AddRefs(mAnchorRange));
   129   NS_ENSURE_SUCCESS(rv, rv);
   131   if (aAction == EditAction::deleteSelection) {
   132     // Deletes are easy, the range is just the current anchor. We set the range
   133     // to check to be empty, FinishInitOnEvent will fill in the range to be
   134     // the current word.
   135     mOp = eOpChangeDelete;
   136     mRange = nullptr;
   137     return NS_OK;
   138   }
   140   mOp = eOpChange;
   142   // range to check
   143   nsCOMPtr<nsINode> prevNode = do_QueryInterface(aPreviousNode);
   144   NS_ENSURE_STATE(prevNode);
   146   mRange = new nsRange(prevNode);
   148   // ...we need to put the start and end in the correct order
   149   int16_t cmpResult;
   150   rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult);
   151   NS_ENSURE_SUCCESS(rv, rv);
   152   if (cmpResult < 0) {
   153     // previous anchor node is before the current anchor
   154     rv = mRange->SetStart(aPreviousNode, aPreviousOffset);
   155     NS_ENSURE_SUCCESS(rv, rv);
   156     rv = mRange->SetEnd(aAnchorNode, aAnchorOffset);
   157   } else {
   158     // previous anchor node is after (or the same as) the current anchor
   159     rv = mRange->SetStart(aAnchorNode, aAnchorOffset);
   160     NS_ENSURE_SUCCESS(rv, rv);
   161     rv = mRange->SetEnd(aPreviousNode, aPreviousOffset);
   162   }
   163   NS_ENSURE_SUCCESS(rv, rv);
   165   // On insert save this range: DoSpellCheck optimizes things in this range.
   166   // Otherwise, just leave this nullptr.
   167   if (aAction == EditAction::insertText)
   168     mCreatedRange = mRange;
   170   // if we were given a range, we need to expand our range to encompass it
   171   if (aStartNode && aEndNode) {
   172     rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult);
   173     NS_ENSURE_SUCCESS(rv, rv);
   174     if (cmpResult < 0) { // given range starts before
   175       rv = mRange->SetStart(aStartNode, aStartOffset);
   176       NS_ENSURE_SUCCESS(rv, rv);
   177     }
   179     rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult);
   180     NS_ENSURE_SUCCESS(rv, rv);
   181     if (cmpResult > 0) { // given range ends after
   182       rv = mRange->SetEnd(aEndNode, aEndOffset);
   183       NS_ENSURE_SUCCESS(rv, rv);
   184     }
   185   }
   187   return NS_OK;
   188 }
   190 // mozInlineSpellStatis::InitForNavigation
   191 //
   192 //    For navigation events, we just need to store the new and old positions.
   193 //
   194 //    In some cases, we detect that we shouldn't check. If this event should
   195 //    not be processed, *aContinue will be false.
   197 nsresult
   198 mozInlineSpellStatus::InitForNavigation(
   199     bool aForceCheck, int32_t aNewPositionOffset,
   200     nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset,
   201     nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset,
   202     bool* aContinue)
   203 {
   204   nsresult rv;
   205   mOp = eOpNavigation;
   207   mForceNavigationWordCheck = aForceCheck;
   208   mNewNavigationPositionOffset = aNewPositionOffset;
   210   // get the root node for checking
   211   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
   212   NS_ENSURE_SUCCESS(rv, rv);
   213   nsCOMPtr<nsIDOMElement> rootElt;
   214   rv = editor->GetRootElement(getter_AddRefs(rootElt));
   215   NS_ENSURE_SUCCESS(rv, rv);
   217   // the anchor node might not be in the DOM anymore, check
   218   nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv);
   219   NS_ENSURE_SUCCESS(rv, rv);
   220   nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
   221   NS_ENSURE_SUCCESS(rv, rv);
   222   if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
   223     *aContinue = false;
   224     return NS_OK;
   225   }
   227   nsCOMPtr<nsIDOMDocument> doc;
   228   rv = GetDocument(getter_AddRefs(doc));
   229   NS_ENSURE_SUCCESS(rv, rv);
   231   rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset,
   232                                 getter_AddRefs(mOldNavigationAnchorRange));
   233   NS_ENSURE_SUCCESS(rv, rv);
   234   rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset,
   235                                 getter_AddRefs(mAnchorRange));
   236   NS_ENSURE_SUCCESS(rv, rv);
   238   *aContinue = true;
   239   return NS_OK;
   240 }
   242 // mozInlineSpellStatus::InitForSelection
   243 //
   244 //    It is easy for selections since we always re-check the spellcheck
   245 //    selection.
   247 nsresult
   248 mozInlineSpellStatus::InitForSelection()
   249 {
   250   mOp = eOpSelection;
   251   return NS_OK;
   252 }
   254 // mozInlineSpellStatus::InitForRange
   255 //
   256 //    Called to cause the spellcheck of the given range. This will look like
   257 //    a change operation over the given range.
   259 nsresult
   260 mozInlineSpellStatus::InitForRange(nsRange* aRange)
   261 {
   262   mOp = eOpChange;
   263   mRange = aRange;
   264   return NS_OK;
   265 }
   267 // mozInlineSpellStatus::FinishInitOnEvent
   268 //
   269 //    Called when the event is triggered to complete initialization that
   270 //    might require the WordUtil. This calls to the operation-specific
   271 //    initializer, and also sets the range to be the entire element if it
   272 //    is nullptr.
   273 //
   274 //    Watch out: the range might still be nullptr if there is nothing to do,
   275 //    the caller will have to check for this.
   277 nsresult
   278 mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil)
   279 {
   280   nsresult rv;
   281   if (! mRange) {
   282     rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
   283                                             getter_AddRefs(mRange));
   284     NS_ENSURE_SUCCESS(rv, rv);
   285   }
   287   switch (mOp) {
   288     case eOpChange:
   289       if (mAnchorRange)
   290         return FillNoCheckRangeFromAnchor(aWordUtil);
   291       break;
   292     case eOpChangeDelete:
   293       if (mAnchorRange) {
   294         rv = FillNoCheckRangeFromAnchor(aWordUtil);
   295         NS_ENSURE_SUCCESS(rv, rv);
   296       }
   297       // Delete events will have no range for the changed text (because it was
   298       // deleted), and InitForEditorChange will set it to nullptr. Here, we select
   299       // the entire word to cause any underlining to be removed.
   300       mRange = mNoCheckRange;
   301       break;
   302     case eOpNavigation:
   303       return FinishNavigationEvent(aWordUtil);
   304     case eOpSelection:
   305       // this gets special handling in ResumeCheck
   306       break;
   307     case eOpResume:
   308       // everything should be initialized already in this case
   309       break;
   310     default:
   311       NS_NOTREACHED("Bad operation");
   312       return NS_ERROR_NOT_INITIALIZED;
   313   }
   314   return NS_OK;
   315 }
   317 // mozInlineSpellStatus::FinishNavigationEvent
   318 //
   319 //    This verifies that we need to check the word at the previous caret
   320 //    position. Now that we have the word util, we can find the word belonging
   321 //    to the previous caret position. If the new position is inside that word,
   322 //    we don't want to do anything. In this case, we'll nullptr out mRange so
   323 //    that the caller will know not to continue.
   324 //
   325 //    Notice that we don't set mNoCheckRange. We check here whether the cursor
   326 //    is in the word that needs checking, so it isn't necessary. Plus, the
   327 //    spellchecker isn't guaranteed to only check the given word, and it could
   328 //    remove the underline from the new word under the cursor.
   330 nsresult
   331 mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil)
   332 {
   333   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor);
   334   if (! editor)
   335     return NS_ERROR_FAILURE; // editor is gone
   337   NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
   338   nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode;
   339   int32_t newAnchorOffset, oldAnchorOffset;
   341   // get the DOM position of the old caret, the range should be collapsed
   342   nsresult rv = mOldNavigationAnchorRange->GetStartContainer(
   343       getter_AddRefs(oldAnchorNode));
   344   NS_ENSURE_SUCCESS(rv, rv);
   345   rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset);
   346   NS_ENSURE_SUCCESS(rv, rv);
   348   // find the word on the old caret position, this is the one that we MAY need
   349   // to check
   350   nsRefPtr<nsRange> oldWord;
   351   rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset,
   352                                  getter_AddRefs(oldWord));
   353   NS_ENSURE_SUCCESS(rv, rv);
   355   // aWordUtil.GetRangeForWord flushes pending notifications, check editor again.
   356   editor = do_QueryReferent(mSpellChecker->mEditor);
   357   if (! editor)
   358     return NS_ERROR_FAILURE; // editor is gone
   360   // get the DOM position of the new caret, the range should be collapsed
   361   rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode));
   362   NS_ENSURE_SUCCESS(rv, rv);
   363   rv = mAnchorRange->GetStartOffset(&newAnchorOffset);
   364   NS_ENSURE_SUCCESS(rv, rv);
   366   // see if the new cursor position is in the word of the old cursor position
   367   bool isInRange = false;
   368   if (! mForceNavigationWordCheck) {
   369     rv = oldWord->IsPointInRange(newAnchorNode,
   370                                  newAnchorOffset + mNewNavigationPositionOffset,
   371                                  &isInRange);
   372     NS_ENSURE_SUCCESS(rv, rv);
   373   }
   375   if (isInRange) {
   376     // caller should give up
   377     mRange = nullptr;
   378   } else {
   379     // check the old word
   380     mRange = oldWord;
   382     // Once we've spellchecked the current word, we don't need to spellcheck
   383     // for any more navigation events.
   384     mSpellChecker->mNeedsCheckAfterNavigation = false;
   385   }
   386   return NS_OK;
   387 }
   389 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
   390 //
   391 //    Given the mAnchorRange object, computes the range of the word it is on
   392 //    (if any) and fills that range into mNoCheckRange. This is used for
   393 //    change and navigation events to know which word we should skip spell
   394 //    checking on
   396 nsresult
   397 mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
   398     mozInlineSpellWordUtil& aWordUtil)
   399 {
   400   nsCOMPtr<nsIDOMNode> anchorNode;
   401   nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode));
   402   NS_ENSURE_SUCCESS(rv, rv);
   404   int32_t anchorOffset;
   405   rv = mAnchorRange->GetStartOffset(&anchorOffset);
   406   NS_ENSURE_SUCCESS(rv, rv);
   408   return aWordUtil.GetRangeForWord(anchorNode, anchorOffset,
   409                                    getter_AddRefs(mNoCheckRange));
   410 }
   412 // mozInlineSpellStatus::GetDocument
   413 //
   414 //    Returns the nsIDOMDocument object for the document for the
   415 //    current spellchecker.
   417 nsresult
   418 mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument)
   419 {
   420   nsresult rv;
   421   *aDocument = nullptr;
   422   if (! mSpellChecker->mEditor)
   423     return NS_ERROR_UNEXPECTED;
   425   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
   426   NS_ENSURE_SUCCESS(rv, rv);
   428   nsCOMPtr<nsIDOMDocument> domDoc;
   429   rv = editor->GetDocument(getter_AddRefs(domDoc));
   430   NS_ENSURE_SUCCESS(rv, rv);
   431   NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER);
   432   domDoc.forget(aDocument);
   433   return NS_OK;
   434 }
   436 // mozInlineSpellStatus::PositionToCollapsedRange
   437 //
   438 //    Converts a given DOM position to a collapsed range covering that
   439 //    position. We use ranges to store DOM positions becuase they stay
   440 //    updated as the DOM is changed.
   442 nsresult
   443 mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument,
   444     nsIDOMNode* aNode, int32_t aOffset, nsIDOMRange** aRange)
   445 {
   446   *aRange = nullptr;
   447   nsCOMPtr<nsIDOMRange> range;
   448   nsresult rv = aDocument->CreateRange(getter_AddRefs(range));
   449   NS_ENSURE_SUCCESS(rv, rv);
   451   rv = range->SetStart(aNode, aOffset);
   452   NS_ENSURE_SUCCESS(rv, rv);
   453   rv = range->SetEnd(aNode, aOffset);
   454   NS_ENSURE_SUCCESS(rv, rv);
   456   range.swap(*aRange);
   457   return NS_OK;
   458 }
   460 // mozInlineSpellResume
   462 class mozInlineSpellResume : public nsRunnable
   463 {
   464 public:
   465   mozInlineSpellResume(const mozInlineSpellStatus& aStatus,
   466                        uint32_t aDisabledAsyncToken)
   467     : mDisabledAsyncToken(aDisabledAsyncToken), mStatus(aStatus) {}
   469   nsresult Post()
   470   {
   471     return NS_DispatchToMainThread(this);
   472   }
   474   NS_IMETHOD Run()
   475   {
   476     // Discard the resumption if the spell checker was disabled after the
   477     // resumption was scheduled.
   478     if (mDisabledAsyncToken == mStatus.mSpellChecker->mDisabledAsyncToken) {
   479       mStatus.mSpellChecker->ResumeCheck(&mStatus);
   480     }
   481     return NS_OK;
   482   }
   484 private:
   485   uint32_t mDisabledAsyncToken;
   486   mozInlineSpellStatus mStatus;
   487 };
   489 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
   490 class InitEditorSpellCheckCallback MOZ_FINAL : public nsIEditorSpellCheckCallback
   491 {
   492 public:
   493   NS_DECL_ISUPPORTS
   495   explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
   496     : mSpellChecker(aSpellChecker) {}
   498   NS_IMETHOD EditorSpellCheckDone()
   499   {
   500     return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
   501   }
   503   void Cancel()
   504   {
   505     mSpellChecker = nullptr;
   506   }
   508 private:
   509   nsRefPtr<mozInlineSpellChecker> mSpellChecker;
   510 };
   511 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
   514 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
   515   NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
   516   NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
   517   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
   518   NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
   519   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
   520   NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
   521 NS_INTERFACE_MAP_END
   523 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
   524 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
   526 NS_IMPL_CYCLE_COLLECTION(mozInlineSpellChecker,
   527                          mSpellCheck,
   528                          mTreeWalker,
   529                          mCurrentSelectionAnchorNode)
   531 mozInlineSpellChecker::SpellCheckingState
   532   mozInlineSpellChecker::gCanEnableSpellChecking =
   533   mozInlineSpellChecker::SpellCheck_Uninitialized;
   535 mozInlineSpellChecker::mozInlineSpellChecker() :
   536     mNumWordsInSpellSelection(0),
   537     mMaxNumWordsInSpellSelection(250),
   538     mNumPendingSpellChecks(0),
   539     mNumPendingUpdateCurrentDictionary(0),
   540     mDisabledAsyncToken(0),
   541     mNeedsCheckAfterNavigation(false),
   542     mFullSpellCheckScheduled(false)
   543 {
   544   nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
   545   if (prefs)
   546     prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); 
   547   mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4;
   548 }
   550 mozInlineSpellChecker::~mozInlineSpellChecker()
   551 {
   552 }
   554 NS_IMETHODIMP
   555 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck)
   556 {
   557   *aSpellCheck = mSpellCheck;
   558   NS_IF_ADDREF(*aSpellCheck);
   559   return NS_OK;
   560 }
   562 NS_IMETHODIMP
   563 mozInlineSpellChecker::Init(nsIEditor *aEditor)
   564 {
   565   mEditor = do_GetWeakReference(aEditor);
   566   return NS_OK;
   567 }
   569 // mozInlineSpellChecker::Cleanup
   570 //
   571 //    Called by the editor when the editor is going away. This is important
   572 //    because we remove listeners. We do NOT clean up anything else in this
   573 //    function, because it can get called while DoSpellCheck is running!
   574 //
   575 //    Getting the style information there can cause DOM notifications to be
   576 //    flushed, which can cause editors to go away which will bring us here.
   577 //    We can not do anything that will cause DoSpellCheck to freak out.
   579 nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames)
   580 {
   581   mNumWordsInSpellSelection = 0;
   582   nsCOMPtr<nsISelection> spellCheckSelection;
   583   nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
   584   if (NS_FAILED(rv)) {
   585     // Ensure we still unregister event listeners (but return a failure code)
   586     UnregisterEventListeners();
   587   } else {
   588     if (!aDestroyingFrames) {
   589       spellCheckSelection->RemoveAllRanges();
   590     }
   592     rv = UnregisterEventListeners();
   593   }
   595   // Notify ENDED observers now.  If we wait to notify as we normally do when
   596   // these async operations finish, then in the meantime the editor may create
   597   // another inline spell checker and cause more STARTED and ENDED
   598   // notifications to be broadcast.  Interleaved notifications for the same
   599   // editor but different inline spell checkers could easily confuse
   600   // observers.  They may receive two consecutive STARTED notifications for
   601   // example, which we guarantee will not happen.
   603   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
   604   if (mPendingSpellCheck) {
   605     // Cancel the pending editor spell checker initialization.
   606     mPendingSpellCheck = nullptr;
   607     mPendingInitEditorSpellCheckCallback->Cancel();
   608     mPendingInitEditorSpellCheckCallback = nullptr;
   609     ChangeNumPendingSpellChecks(-1, editor);
   610   }
   612   // Increment this token so that pending UpdateCurrentDictionary calls and
   613   // scheduled spell checks are discarded when they finish.
   614   mDisabledAsyncToken++;
   616   if (mNumPendingUpdateCurrentDictionary > 0) {
   617     // Account for pending UpdateCurrentDictionary calls.
   618     ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, editor);
   619     mNumPendingUpdateCurrentDictionary = 0;
   620   }
   621   if (mNumPendingSpellChecks > 0) {
   622     // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
   623     // pending scheduled spell checks.
   624     ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor);
   625   }
   627   mEditor = nullptr;
   628   mFullSpellCheckScheduled = false;
   630   return rv;
   631 }
   633 // mozInlineSpellChecker::CanEnableInlineSpellChecking
   634 //
   635 //    This function can be called to see if it seems likely that we can enable
   636 //    spellchecking before actually creating the InlineSpellChecking objects.
   637 //
   638 //    The problem is that we can't get the dictionary list without actually
   639 //    creating a whole bunch of spellchecking objects. This function tries to
   640 //    do that and caches the result so we don't have to keep allocating those
   641 //    objects if there are no dictionaries or spellchecking.
   642 //
   643 //    Whenever dictionaries are added or removed at runtime, this value must be
   644 //    updated before an observer notification is sent out about the change, to
   645 //    avoid editors getting a wrong cached result.
   647 bool // static
   648 mozInlineSpellChecker::CanEnableInlineSpellChecking()
   649 {
   650   nsresult rv;
   651   if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
   652     gCanEnableSpellChecking = SpellCheck_NotAvailable;
   654     nsCOMPtr<nsIEditorSpellCheck> spellchecker =
   655       do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
   656     NS_ENSURE_SUCCESS(rv, false);
   658     bool canSpellCheck = false;
   659     rv = spellchecker->CanSpellCheck(&canSpellCheck);
   660     NS_ENSURE_SUCCESS(rv, false);
   662     if (canSpellCheck)
   663       gCanEnableSpellChecking = SpellCheck_Available;
   664   }
   665   return (gCanEnableSpellChecking == SpellCheck_Available);
   666 }
   668 void // static
   669 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
   670 {
   671   gCanEnableSpellChecking = SpellCheck_Uninitialized;
   672 }
   674 // mozInlineSpellChecker::RegisterEventListeners
   675 //
   676 //    The inline spell checker listens to mouse events and keyboard navigation+ //    events.
   678 nsresult
   679 mozInlineSpellChecker::RegisterEventListeners()
   680 {
   681   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
   682   NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
   684   editor->AddEditActionListener(this);
   686   nsCOMPtr<nsIDOMDocument> doc;
   687   nsresult rv = editor->GetDocument(getter_AddRefs(doc));
   688   NS_ENSURE_SUCCESS(rv, rv);
   690   nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc, &rv);
   691   NS_ENSURE_SUCCESS(rv, rv);
   693   piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this,
   694                              true, false);
   695   piTarget->AddEventListener(NS_LITERAL_STRING("click"), this,
   696                              false, false);
   697   piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this,
   698                              false, false);
   699   return NS_OK;
   700 }
   702 // mozInlineSpellChecker::UnregisterEventListeners
   704 nsresult
   705 mozInlineSpellChecker::UnregisterEventListeners()
   706 {
   707   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
   708   NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
   710   editor->RemoveEditActionListener(this);
   712   nsCOMPtr<nsIDOMDocument> doc;
   713   editor->GetDocument(getter_AddRefs(doc));
   714   NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
   716   nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc);
   717   NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
   719   piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
   720   piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false);
   721   piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, false);
   722   return NS_OK;
   723 }
   725 // mozInlineSpellChecker::GetEnableRealTimeSpell
   727 NS_IMETHODIMP
   728 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled)
   729 {
   730   NS_ENSURE_ARG_POINTER(aEnabled);
   731   *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
   732   return NS_OK;
   733 }
   735 // mozInlineSpellChecker::SetEnableRealTimeSpell
   737 NS_IMETHODIMP
   738 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
   739 {
   740   if (!aEnabled) {
   741     mSpellCheck = nullptr;
   742     return Cleanup(false);
   743   }
   745   if (mSpellCheck) {
   746     // spellcheck the current contents. SpellCheckRange doesn't supply a created
   747     // range to DoSpellCheck, which in our case is the entire range. But this
   748     // optimization doesn't matter because there is nothing in the spellcheck
   749     // selection when starting, which triggers a better optimization.
   750     return SpellCheckRange(nullptr);
   751   }
   753   if (mPendingSpellCheck) {
   754     // The editor spell checker is already being initialized.
   755     return NS_OK;
   756   }
   758   mPendingSpellCheck =
   759     do_CreateInstance("@mozilla.org/editor/editorspellchecker;1");
   760   NS_ENSURE_STATE(mPendingSpellCheck);
   762   nsCOMPtr<nsITextServicesFilter> filter =
   763     do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1");
   764   if (!filter) {
   765     mPendingSpellCheck = nullptr;
   766     NS_ENSURE_STATE(filter);
   767   }
   768   mPendingSpellCheck->SetFilter(filter);
   770   mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
   771   if (!mPendingInitEditorSpellCheckCallback) {
   772     mPendingSpellCheck = nullptr;
   773     NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback);
   774   }
   776   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
   777   nsresult rv = mPendingSpellCheck->InitSpellChecker(
   778                   editor, false, mPendingInitEditorSpellCheckCallback);
   779   if (NS_FAILED(rv)) {
   780     mPendingSpellCheck = nullptr;
   781     mPendingInitEditorSpellCheckCallback = nullptr;
   782     NS_ENSURE_SUCCESS(rv, rv);
   783   }
   785   ChangeNumPendingSpellChecks(1);
   787   return NS_OK;
   788 }
   790 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
   791 nsresult
   792 mozInlineSpellChecker::EditorSpellCheckInited()
   793 {
   794   NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!");
   796   // spell checking is enabled, register our event listeners to track navigation
   797   RegisterEventListeners();
   799   mSpellCheck = mPendingSpellCheck;
   800   mPendingSpellCheck = nullptr;
   801   mPendingInitEditorSpellCheckCallback = nullptr;
   802   ChangeNumPendingSpellChecks(-1);
   804   // spellcheck the current contents. SpellCheckRange doesn't supply a created
   805   // range to DoSpellCheck, which in our case is the entire range. But this
   806   // optimization doesn't matter because there is nothing in the spellcheck
   807   // selection when starting, which triggers a better optimization.
   808   return SpellCheckRange(nullptr);
   809 }
   811 // Changes the number of pending spell checks by the given delta.  If the number
   812 // becomes zero or nonzero, observers are notified.  See NotifyObservers for
   813 // info on the aEditor parameter.
   814 void
   815 mozInlineSpellChecker::ChangeNumPendingSpellChecks(int32_t aDelta,
   816                                                    nsIEditor* aEditor)
   817 {
   818   int8_t oldNumPending = mNumPendingSpellChecks;
   819   mNumPendingSpellChecks += aDelta;
   820   NS_ASSERTION(mNumPendingSpellChecks >= 0,
   821                "Unbalanced ChangeNumPendingSpellChecks calls!");
   822   if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
   823     NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor);
   824   } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
   825     NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor);
   826   }
   827 }
   829 // Broadcasts the given topic to observers.  aEditor is passed to observers if
   830 // nonnull; otherwise mEditor is passed.
   831 void
   832 mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor)
   833 {
   834   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   835   if (!os)
   836     return;
   837   nsCOMPtr<nsIEditor> editor = aEditor;
   838   if (!editor) {
   839     editor = do_QueryReferent(mEditor);
   840   }
   841   os->NotifyObservers(editor, aTopic, nullptr);
   842 }
   844 // mozInlineSpellChecker::SpellCheckAfterEditorChange
   845 //
   846 //    Called by the editor when nearly anything happens to change the content.
   847 //
   848 //    The start and end positions specify a range for the thing that happened,
   849 //    but these are usually nullptr, even when you'd think they would be useful
   850 //    because you want the range (for example, pasting). We ignore them in
   851 //    this case.
   853 NS_IMETHODIMP
   854 mozInlineSpellChecker::SpellCheckAfterEditorChange(
   855     int32_t aAction, nsISelection *aSelection,
   856     nsIDOMNode *aPreviousSelectedNode, int32_t aPreviousSelectedOffset,
   857     nsIDOMNode *aStartNode, int32_t aStartOffset,
   858     nsIDOMNode *aEndNode, int32_t aEndOffset)
   859 {
   860   nsresult rv;
   861   NS_ENSURE_ARG_POINTER(aSelection);
   862   if (!mSpellCheck)
   863     return NS_OK; // disabling spell checking is not an error
   865   // this means something has changed, and we never check the current word,
   866   // therefore, we should spellcheck for subsequent caret navigations
   867   mNeedsCheckAfterNavigation = true;
   869   // the anchor node is the position of the caret
   870   nsCOMPtr<nsIDOMNode> anchorNode;
   871   rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
   872   NS_ENSURE_SUCCESS(rv, rv);
   873   int32_t anchorOffset;
   874   rv = aSelection->GetAnchorOffset(&anchorOffset);
   875   NS_ENSURE_SUCCESS(rv, rv);
   877   mozInlineSpellStatus status(this);
   878   rv = status.InitForEditorChange((EditAction)aAction,
   879                                   anchorNode, anchorOffset,
   880                                   aPreviousSelectedNode, aPreviousSelectedOffset,
   881                                   aStartNode, aStartOffset,
   882                                   aEndNode, aEndOffset);
   883   NS_ENSURE_SUCCESS(rv, rv);
   884   rv = ScheduleSpellCheck(status);
   885   NS_ENSURE_SUCCESS(rv, rv);
   887   // remember the current caret position after every change
   888   SaveCurrentSelectionPosition();
   889   return NS_OK;
   890 }
   892 // mozInlineSpellChecker::SpellCheckRange
   893 //
   894 //    Spellchecks all the words in the given range.
   895 //    Supply a nullptr range and this will check the entire editor.
   897 nsresult
   898 mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
   899 {
   900   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
   902   mozInlineSpellStatus status(this);
   903   nsRange* range = static_cast<nsRange*>(aRange);
   904   nsresult rv = status.InitForRange(range);
   905   NS_ENSURE_SUCCESS(rv, rv);
   906   return ScheduleSpellCheck(status);
   907 }
   909 // mozInlineSpellChecker::GetMisspelledWord
   911 NS_IMETHODIMP
   912 mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, int32_t aOffset,
   913                                          nsIDOMRange **newword)
   914 {
   915   NS_ENSURE_ARG_POINTER(aNode);
   916   nsCOMPtr<nsISelection> spellCheckSelection;
   917   nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
   918   NS_ENSURE_SUCCESS(res, res); 
   920   return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
   921 }
   923 // mozInlineSpellChecker::ReplaceWord
   925 NS_IMETHODIMP
   926 mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, int32_t aOffset,
   927                                    const nsAString &newword)
   928 {
   929   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
   930   NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
   931   NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
   933   nsCOMPtr<nsIDOMRange> range;
   934   nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
   935   NS_ENSURE_SUCCESS(res, res); 
   937   if (range)
   938   {
   939     editor->BeginTransaction();
   941     nsCOMPtr<nsISelection> selection;
   942     res = editor->GetSelection(getter_AddRefs(selection));
   943     NS_ENSURE_SUCCESS(res, res);
   944     selection->RemoveAllRanges();
   945     selection->AddRange(range);
   946     editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
   948     nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
   949     if (textEditor)
   950       textEditor->InsertText(newword);
   952     editor->EndTransaction();
   953   }
   955   return NS_OK;
   956 }
   958 // mozInlineSpellChecker::AddWordToDictionary
   960 NS_IMETHODIMP
   961 mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
   962 {
   963   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
   965   nsAutoString wordstr(word);
   966   nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
   967   NS_ENSURE_SUCCESS(rv, rv); 
   969   mozInlineSpellStatus status(this);
   970   rv = status.InitForSelection();
   971   NS_ENSURE_SUCCESS(rv, rv);
   972   return ScheduleSpellCheck(status);
   973 }
   975 //  mozInlineSpellChecker::RemoveWordFromDictionary
   977 NS_IMETHODIMP
   978 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word)
   979 {
   980   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
   982   nsAutoString wordstr(word);
   983   nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get());
   984   NS_ENSURE_SUCCESS(rv, rv); 
   986   mozInlineSpellStatus status(this);
   987   rv = status.InitForRange(nullptr);
   988   NS_ENSURE_SUCCESS(rv, rv);
   989   return ScheduleSpellCheck(status);
   990 }
   992 // mozInlineSpellChecker::IgnoreWord
   994 NS_IMETHODIMP
   995 mozInlineSpellChecker::IgnoreWord(const nsAString &word)
   996 {
   997   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
   999   nsAutoString wordstr(word);
  1000   nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
  1001   NS_ENSURE_SUCCESS(rv, rv); 
  1003   mozInlineSpellStatus status(this);
  1004   rv = status.InitForSelection();
  1005   NS_ENSURE_SUCCESS(rv, rv);
  1006   return ScheduleSpellCheck(status);
  1009 // mozInlineSpellChecker::IgnoreWords
  1011 NS_IMETHODIMP
  1012 mozInlineSpellChecker::IgnoreWords(const char16_t **aWordsToIgnore,
  1013                                    uint32_t aCount)
  1015   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
  1017   // add each word to the ignore list and then recheck the document
  1018   for (uint32_t index = 0; index < aCount; index++)
  1019     mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
  1021   mozInlineSpellStatus status(this);
  1022   nsresult rv = status.InitForSelection();
  1023   NS_ENSURE_SUCCESS(rv, rv);
  1024   return ScheduleSpellCheck(status);
  1027 NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, int32_t aPosition)
  1029   return NS_OK;
  1032 NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
  1033                                                    int32_t aPosition, nsresult aResult)
  1035   return NS_OK;
  1038 NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
  1039                                                     int32_t aPosition)
  1041   return NS_OK;
  1044 NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
  1045                                                    int32_t aPosition, nsresult aResult)
  1048   return NS_OK;
  1051 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
  1053   return NS_OK;
  1056 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
  1058   return NS_OK;
  1061 NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, int32_t aOffset)
  1063   return NS_OK;
  1066 NS_IMETHODIMP
  1067 mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
  1068                                     int32_t aOffset,
  1069                                     nsIDOMNode *aNewLeftNode, nsresult aResult)
  1071   return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
  1074 NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
  1076   return NS_OK;
  1079 NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, 
  1080                                                   nsIDOMNode *aParent, nsresult aResult)
  1082   return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
  1085 NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString & aString)
  1087   return NS_OK;
  1090 NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset,
  1091                                                    const nsAString & aString, nsresult aResult)
  1093   return NS_OK;
  1096 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength)
  1098   return NS_OK;
  1101 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult)
  1103   return NS_OK;
  1106 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
  1108   return NS_OK;
  1111 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
  1113   return NS_OK;
  1116 // mozInlineSpellChecker::MakeSpellCheckRange
  1117 //
  1118 //    Given begin and end positions, this function constructs a range as
  1119 //    required for ScheduleSpellCheck. If the start and end nodes are nullptr,
  1120 //    then the entire range will be selected, and you can supply -1 as the
  1121 //    offset to the end range to select all of that node.
  1122 //
  1123 //    If the resulting range would be empty, nullptr is put into *aRange and the
  1124 //    function succeeds.
  1126 nsresult
  1127 mozInlineSpellChecker::MakeSpellCheckRange(
  1128     nsIDOMNode* aStartNode, int32_t aStartOffset,
  1129     nsIDOMNode* aEndNode, int32_t aEndOffset,
  1130     nsRange** aRange)
  1132   nsresult rv;
  1133   *aRange = nullptr;
  1135   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
  1136   NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
  1138   nsCOMPtr<nsIDOMDocument> doc;
  1139   rv = editor->GetDocument(getter_AddRefs(doc));
  1140   NS_ENSURE_SUCCESS(rv, rv);
  1141   NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
  1143   nsCOMPtr<nsIDOMRange> range;
  1144   rv = doc->CreateRange(getter_AddRefs(range));
  1145   NS_ENSURE_SUCCESS(rv, rv);
  1147   // possibly use full range of the editor
  1148   nsCOMPtr<nsIDOMElement> rootElem;
  1149   if (! aStartNode || ! aEndNode) {
  1150     rv = editor->GetRootElement(getter_AddRefs(rootElem));
  1151     NS_ENSURE_SUCCESS(rv, rv);
  1153     aStartNode = rootElem;
  1154     aStartOffset = 0;
  1156     aEndNode = rootElem;
  1157     aEndOffset = -1;
  1160   if (aEndOffset == -1) {
  1161     nsCOMPtr<nsIDOMNodeList> childNodes;
  1162     rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
  1163     NS_ENSURE_SUCCESS(rv, rv);
  1165     uint32_t childCount;
  1166     rv = childNodes->GetLength(&childCount);
  1167     NS_ENSURE_SUCCESS(rv, rv);
  1169     aEndOffset = childCount;
  1172   // sometimes we are are requested to check an empty range (possibly an empty
  1173   // document). This will result in assertions later.
  1174   if (aStartNode == aEndNode && aStartOffset == aEndOffset)
  1175     return NS_OK;
  1177   rv = range->SetStart(aStartNode, aStartOffset);
  1178   NS_ENSURE_SUCCESS(rv, rv);
  1179   if (aEndOffset)
  1180     rv = range->SetEnd(aEndNode, aEndOffset);
  1181   else
  1182     rv = range->SetEndAfter(aEndNode);
  1183   NS_ENSURE_SUCCESS(rv, rv);
  1185   *aRange = static_cast<nsRange*>(range.forget().take());
  1186   return NS_OK;
  1189 nsresult
  1190 mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
  1191                                               int32_t aStartOffset,
  1192                                               nsIDOMNode *aEndNode,
  1193                                               int32_t aEndOffset)
  1195   nsRefPtr<nsRange> range;
  1196   nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
  1197                                     aEndNode, aEndOffset,
  1198                                     getter_AddRefs(range));
  1199   NS_ENSURE_SUCCESS(rv, rv);
  1201   if (! range)
  1202     return NS_OK; // range is empty: nothing to do
  1204   mozInlineSpellStatus status(this);
  1205   rv = status.InitForRange(range);
  1206   NS_ENSURE_SUCCESS(rv, rv);
  1207   return ScheduleSpellCheck(status);
  1210 // mozInlineSpellChecker::SkipSpellCheckForNode
  1211 //
  1212 //    There are certain conditions when we don't want to spell check a node. In
  1213 //    particular quotations, moz signatures, etc. This routine returns false
  1214 //    for these cases.
  1216 nsresult
  1217 mozInlineSpellChecker::SkipSpellCheckForNode(nsIEditor* aEditor,
  1218                                              nsIDOMNode *aNode,
  1219                                              bool *checkSpelling)
  1221   *checkSpelling = true;
  1222   NS_ENSURE_ARG_POINTER(aNode);
  1224   uint32_t flags;
  1225   aEditor->GetFlags(&flags);
  1226   if (flags & nsIPlaintextEditor::eEditorMailMask)
  1228     nsCOMPtr<nsIDOMNode> parent;
  1229     aNode->GetParentNode(getter_AddRefs(parent));
  1231     while (parent)
  1233       nsCOMPtr<nsIDOMElement> parentElement = do_QueryInterface(parent);
  1234       if (!parentElement)
  1235         break;
  1237       nsAutoString parentTagName;
  1238       parentElement->GetTagName(parentTagName);
  1240       if (parentTagName.Equals(NS_LITERAL_STRING("blockquote"), nsCaseInsensitiveStringComparator()))
  1242         nsAutoString quotetype;
  1243         parentElement->GetAttribute(NS_LITERAL_STRING("type"), quotetype);
  1244         if (quotetype.Equals(NS_LITERAL_STRING("cite"), nsCaseInsensitiveStringComparator()))
  1246           *checkSpelling = false;
  1247           break;
  1250       else if (parentTagName.Equals(NS_LITERAL_STRING("pre"), nsCaseInsensitiveStringComparator()))
  1252         nsAutoString classname;
  1253         parentElement->GetAttribute(NS_LITERAL_STRING("class"),classname);
  1254         if (classname.Equals(NS_LITERAL_STRING("moz-signature")))
  1255           *checkSpelling = false;
  1258       nsCOMPtr<nsIDOMNode> nextParent;
  1259       parent->GetParentNode(getter_AddRefs(nextParent));
  1260       parent = nextParent;
  1263   else {
  1264     // Check spelling only if the node is editable, and GetSpellcheck() is true
  1265     // on the nearest HTMLElement ancestor.
  1266     nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
  1267     if (!content->IsEditable()) {
  1268       *checkSpelling = false;
  1269       return NS_OK;
  1272     // Make sure that we can always turn on spell checking for inputs/textareas.
  1273     // Note that because of the previous check, at this point we know that the
  1274     // node is editable.
  1275     if (content->IsInAnonymousSubtree()) {
  1276       nsCOMPtr<nsIContent> node = content->GetParent();
  1277       while (node && node->IsInNativeAnonymousSubtree()) {
  1278         node = node->GetParent();
  1280       nsCOMPtr<nsITextControlElement> textControl = do_QueryInterface(node);
  1281       if (textControl) {
  1282         *checkSpelling = true;
  1283         return NS_OK;
  1287     // Get HTML element ancestor (might be aNode itself, although probably that
  1288     // has to be a text node in real life here)
  1289     nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(content);
  1290     while (content && !htmlElement) {
  1291       content = content->GetParent();
  1292       htmlElement = do_QueryInterface(content);
  1294     NS_ASSERTION(htmlElement, "Why do we have no htmlElement?");
  1295     if (!htmlElement) {
  1296       return NS_OK;
  1299     // See if it's spellcheckable
  1300     htmlElement->GetSpellcheck(checkSpelling);
  1301     return NS_OK;
  1304   return NS_OK;
  1307 // mozInlineSpellChecker::ScheduleSpellCheck
  1308 //
  1309 //    This is called by code to do the actual spellchecking. We will set up
  1310 //    the proper structures for calls to DoSpellCheck.
  1312 nsresult
  1313 mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
  1315   if (mFullSpellCheckScheduled) {
  1316     // Just ignore this; we're going to spell-check everything anyway
  1317     return NS_OK;
  1320   mozInlineSpellResume* resume =
  1321     new mozInlineSpellResume(aStatus, mDisabledAsyncToken);
  1322   NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
  1324   nsresult rv = resume->Post();
  1325   if (NS_FAILED(rv)) {
  1326     delete resume;
  1327   } else {
  1328     if (aStatus.IsFullSpellCheck()) {
  1329       // We're going to check everything.  Suppress further spell-check attempts
  1330       // until that happens.
  1331       mFullSpellCheckScheduled = true;
  1333     ChangeNumPendingSpellChecks(1);
  1335   return rv;
  1338 // mozInlineSpellChecker::DoSpellCheckSelection
  1339 //
  1340 //    Called to re-check all misspelled words. We iterate over all ranges in
  1341 //    the selection and call DoSpellCheck on them. This is used when a word
  1342 //    is ignored or added to the dictionary: all instances of that word should
  1343 //    be removed from the selection.
  1344 //
  1345 //    FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
  1346 //    Typically, checking this small amount of text is relatively fast, but
  1347 //    for large numbers of words, a lag may be noticeable.
  1349 nsresult
  1350 mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
  1351                                              nsISelection* aSpellCheckSelection,
  1352                                              mozInlineSpellStatus* aStatus)
  1354   nsresult rv;
  1356   // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
  1357   mNumWordsInSpellSelection = 0;
  1359   // Since we could be modifying the ranges for the spellCheckSelection while
  1360   // looping on the spell check selection, keep a separate array of range
  1361   // elements inside the selection
  1362   nsCOMArray<nsIDOMRange> ranges;
  1364   int32_t count;
  1365   aSpellCheckSelection->GetRangeCount(&count);
  1367   int32_t idx;
  1368   nsCOMPtr<nsIDOMRange> checkRange;
  1369   for (idx = 0; idx < count; idx ++) {
  1370     aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange));
  1371     if (checkRange) {
  1372       if (! ranges.AppendObject(checkRange))
  1373         return NS_ERROR_OUT_OF_MEMORY;
  1377   // We have saved the ranges above. Clearing the spellcheck selection here
  1378   // isn't necessary (rechecking each word will modify it as necessary) but
  1379   // provides better performance. By ensuring that no ranges need to be
  1380   // removed in DoSpellCheck, we can save checking range inclusion which is
  1381   // slow.
  1382   aSpellCheckSelection->RemoveAllRanges();
  1384   // We use this state object for all calls, and just update its range. Note
  1385   // that we don't need to call FinishInit since we will be filling in the
  1386   // necessary information.
  1387   mozInlineSpellStatus status(this);
  1388   rv = status.InitForRange(nullptr);
  1389   NS_ENSURE_SUCCESS(rv, rv);
  1391   bool doneChecking;
  1392   for (idx = 0; idx < count; idx ++) {
  1393     checkRange = ranges[idx];
  1394     if (checkRange) {
  1395       // We can consider this word as "added" since we know it has no spell
  1396       // check range over it that needs to be deleted. All the old ranges
  1397       // were cleared above. We also need to clear the word count so that we
  1398       // check all words instead of stopping early.
  1399       status.mRange = static_cast<nsRange*>(checkRange.get());
  1400       rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status,
  1401                         &doneChecking);
  1402       NS_ENSURE_SUCCESS(rv, rv);
  1403       NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!");
  1405       status.mWordCount = 0;
  1409   return NS_OK;
  1412 // mozInlineSpellChecker::DoSpellCheck
  1413 //
  1414 //    This function checks words intersecting the given range, excluding those
  1415 //    inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
  1416 //    will have any spell selection removed (this is used to hide the
  1417 //    underlining for the word that the caret is in). aNoCheckRange should be
  1418 //    on word boundaries.
  1419 //
  1420 //    mResume->mCreatedRange is a possibly nullptr range of new text that was
  1421 //    inserted.  Inside this range, we don't bother to check whether things are
  1422 //    inside the spellcheck selection, which speeds up large paste operations
  1423 //    considerably.
  1424 //
  1425 //    Normal case when editing text by typing
  1426 //       h e l l o   w o r k d   h o w   a r e   y o u
  1427 //                            ^ caret
  1428 //                   [-------] mRange
  1429 //                   [-------] mNoCheckRange
  1430 //      -> does nothing (range is the same as the no check range)
  1431 //
  1432 //    Case when pasting:
  1433 //             [---------- pasted text ----------]
  1434 //       h e l l o   w o r k d   h o w   a r e   y o u
  1435 //                                                ^ caret
  1436 //                                               [---] aNoCheckRange
  1437 //      -> recheck all words in range except those in aNoCheckRange
  1438 //
  1439 //    If checking is complete, *aDoneChecking will be set. If there is more
  1440 //    but we ran out of time, this will be false and the range will be
  1441 //    updated with the stuff that still needs checking.
  1443 nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
  1444                                              nsISelection *aSpellCheckSelection,
  1445                                              mozInlineSpellStatus* aStatus,
  1446                                              bool* aDoneChecking)
  1448   *aDoneChecking = true;
  1450   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
  1452   // get the editor for SkipSpellCheckForNode, this may fail in reasonable
  1453   // circumstances since the editor could have gone away
  1454   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
  1455   if (! editor)
  1456     return NS_ERROR_FAILURE;
  1458   bool iscollapsed;
  1459   nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed);
  1460   NS_ENSURE_SUCCESS(rv, rv);
  1461   if (iscollapsed)
  1462     return NS_OK;
  1464   nsCOMPtr<nsISelectionPrivate> privSel = do_QueryInterface(aSpellCheckSelection);
  1466   // see if the selection has any ranges, if not, then we can optimize checking
  1467   // range inclusion later (we have no ranges when we are initially checking or
  1468   // when there are no misspelled words yet).
  1469   int32_t originalRangeCount;
  1470   rv = aSpellCheckSelection->GetRangeCount(&originalRangeCount);
  1471   NS_ENSURE_SUCCESS(rv, rv);
  1473   // set the starting DOM position to be the beginning of our range
  1475     // Scope for the node/offset pairs here so they don't get
  1476     // accidentally used later
  1477     nsINode* beginNode = aStatus->mRange->GetStartParent();
  1478     int32_t beginOffset = aStatus->mRange->StartOffset();
  1479     nsINode* endNode = aStatus->mRange->GetEndParent();
  1480     int32_t endOffset = aStatus->mRange->EndOffset();
  1482     // Now check that we're still looking at a range that's under
  1483     // aWordUtil.GetRootNode()
  1484     nsINode* rootNode = aWordUtil.GetRootNode();
  1485     if (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) ||
  1486         !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) {
  1487       // Just bail out and don't try to spell-check this
  1488       return NS_OK;
  1491     aWordUtil.SetEnd(endNode, endOffset);
  1492     aWordUtil.SetPosition(beginNode, beginOffset);
  1495   // aWordUtil.SetPosition flushes pending notifications, check editor again.
  1496   editor = do_QueryReferent(mEditor);
  1497   if (! editor)
  1498     return NS_ERROR_FAILURE;
  1500   int32_t wordsSinceTimeCheck = 0;
  1501   PRTime beginTime = PR_Now();
  1503   nsAutoString wordText;
  1504   nsRefPtr<nsRange> wordRange;
  1505   bool dontCheckWord;
  1506   while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
  1507                                             getter_AddRefs(wordRange),
  1508                                             &dontCheckWord)) &&
  1509          wordRange) {
  1510     wordsSinceTimeCheck ++;
  1512     // get the range for the current word.
  1513     // Not using nsINode here for now because we have to call into
  1514     // selection APIs that use nsIDOMNode. :(
  1515     nsCOMPtr<nsIDOMNode> beginNode, endNode;
  1516     int32_t beginOffset, endOffset;
  1517     wordRange->GetStartContainer(getter_AddRefs(beginNode));
  1518     wordRange->GetEndContainer(getter_AddRefs(endNode));
  1519     wordRange->GetStartOffset(&beginOffset);
  1520     wordRange->GetEndOffset(&endOffset);
  1522 #ifdef DEBUG_INLINESPELL
  1523     printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
  1524     if (dontCheckWord)
  1525       printf(" (not checking)");
  1526     printf("\n");
  1527 #endif
  1529     // see if there is a spellcheck range that already intersects the word
  1530     // and remove it. We only need to remove old ranges, so don't bother if
  1531     // there were no ranges when we started out.
  1532     if (originalRangeCount > 0) {
  1533       // likewise, if this word is inside new text, we won't bother testing
  1534       bool inCreatedRange = false;
  1535       if (aStatus->mCreatedRange)
  1536         aStatus->mCreatedRange->IsPointInRange(beginNode, beginOffset, &inCreatedRange);
  1537       if (! inCreatedRange) {
  1538         nsTArray<nsRange*> ranges;
  1539         nsCOMPtr<nsINode> firstNode = do_QueryInterface(beginNode);
  1540         nsCOMPtr<nsINode> lastNode = do_QueryInterface(endNode);
  1541         rv = privSel->GetRangesForIntervalArray(firstNode, beginOffset,
  1542                                                 lastNode, endOffset,
  1543                                                 true, &ranges);
  1544         NS_ENSURE_SUCCESS(rv, rv);
  1545         for (uint32_t i = 0; i < ranges.Length(); i++)
  1546           RemoveRange(aSpellCheckSelection, ranges[i]);
  1550     // some words are special and don't need checking
  1551     if (dontCheckWord)
  1552       continue;
  1554     // some nodes we don't spellcheck
  1555     bool checkSpelling;
  1556     rv = SkipSpellCheckForNode(editor, beginNode, &checkSpelling);
  1557     NS_ENSURE_SUCCESS(rv, rv);
  1558     if (!checkSpelling)
  1559       continue;
  1561     // Don't check spelling if we're inside the noCheckRange. This needs to
  1562     // be done after we clear any old selection because the excluded word
  1563     // might have been previously marked.
  1564     //
  1565     // We do a simple check to see if the beginning of our word is in the
  1566     // exclusion range. Because the exclusion range is a multiple of a word,
  1567     // this is sufficient.
  1568     if (aStatus->mNoCheckRange) {
  1569       bool inExclusion = false;
  1570       aStatus->mNoCheckRange->IsPointInRange(beginNode, beginOffset,
  1571                                              &inExclusion);
  1572       if (inExclusion)
  1573         continue;
  1576     // check spelling and add to selection if misspelled
  1577     bool isMisspelled;
  1578     aWordUtil.NormalizeWord(wordText);
  1579     rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
  1580     if (NS_FAILED(rv))
  1581       continue;
  1583     if (isMisspelled) {
  1584       // misspelled words count extra toward the max
  1585       wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
  1586       AddRange(aSpellCheckSelection, wordRange);
  1588       aStatus->mWordCount ++;
  1589       if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
  1590           SpellCheckSelectionIsFull())
  1591         break;
  1594     // see if we've run out of time, only check every N words for perf
  1595     if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) {
  1596       wordsSinceTimeCheck = 0;
  1597       if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) {
  1598         // stop checking, our time limit has been exceeded
  1600         // move the range to encompass the stuff that needs checking
  1601         rv = aStatus->mRange->SetStart(endNode, endOffset);
  1602         if (NS_FAILED(rv)) {
  1603           // The range might be unhappy because the beginning is after the
  1604           // end. This is possible when the requested end was in the middle
  1605           // of a word, just ignore this situation and assume we're done.
  1606           return NS_OK;
  1608         *aDoneChecking = false;
  1609         return NS_OK;
  1614   return NS_OK;
  1617 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
  1618 class AutoChangeNumPendingSpellChecks
  1620 public:
  1621   AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
  1622                                   int32_t aDelta)
  1623     : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
  1625   ~AutoChangeNumPendingSpellChecks()
  1627     mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
  1630 private:
  1631   nsRefPtr<mozInlineSpellChecker> mSpellChecker;
  1632   int32_t mDelta;
  1633 };
  1635 // mozInlineSpellChecker::ResumeCheck
  1636 //
  1637 //    Called by the resume event when it fires. We will try to pick up where
  1638 //    the last resume left off.
  1640 nsresult
  1641 mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
  1643   // Observers should be notified that spell check has ended only after spell
  1644   // check is done below, but since there are many early returns in this method
  1645   // and the number of pending spell checks must be decremented regardless of
  1646   // whether the spell check actually happens, use this RAII object.
  1647   AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
  1649   if (aStatus->IsFullSpellCheck()) {
  1650     // Allow posting new spellcheck resume events from inside
  1651     // ResumeCheck, now that we're actually firing.
  1652     NS_ASSERTION(mFullSpellCheckScheduled,
  1653                  "How could this be false?  The full spell check is "
  1654                  "calling us!!");
  1655     mFullSpellCheckScheduled = false;
  1658   if (! mSpellCheck)
  1659     return NS_OK; // spell checking has been turned off
  1661   nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
  1662   if (! editor)
  1663     return NS_OK; // editor is gone
  1665   mozInlineSpellWordUtil wordUtil;
  1666   nsresult rv = wordUtil.Init(mEditor);
  1667   if (NS_FAILED(rv))
  1668     return NS_OK; // editor doesn't like us, don't assert
  1670   nsCOMPtr<nsISelection> spellCheckSelection;
  1671   rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
  1672   NS_ENSURE_SUCCESS(rv, rv);
  1674   nsAutoString currentDictionary;
  1675   rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
  1676   if (NS_FAILED(rv)) {
  1677     // no active dictionary
  1678     int32_t count;
  1679     spellCheckSelection->GetRangeCount(&count);
  1680     for (int32_t index = count - 1; index >= 0; index--) {
  1681       nsCOMPtr<nsIDOMRange> checkRange;
  1682       spellCheckSelection->GetRangeAt(index, getter_AddRefs(checkRange));
  1683       if (checkRange) {
  1684         RemoveRange(spellCheckSelection, checkRange);
  1687     return NS_OK; 
  1690   CleanupRangesInSelection(spellCheckSelection);
  1692   rv = aStatus->FinishInitOnEvent(wordUtil);
  1693   NS_ENSURE_SUCCESS(rv, rv);
  1694   if (! aStatus->mRange)
  1695     return NS_OK; // empty range, nothing to do
  1697   bool doneChecking = true;
  1698   if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
  1699     rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus);
  1700   else
  1701     rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
  1702   NS_ENSURE_SUCCESS(rv, rv);
  1704   if (! doneChecking)
  1705     rv = ScheduleSpellCheck(*aStatus);
  1706   return rv;
  1709 // mozInlineSpellChecker::IsPointInSelection
  1710 //
  1711 //    Determines if a given (node,offset) point is inside the given
  1712 //    selection. If so, the specific range of the selection that
  1713 //    intersects is places in *aRange. (There may be multiple disjoint
  1714 //    ranges in a selection.)
  1715 //
  1716 //    If there is no intersection, *aRange will be nullptr.
  1718 nsresult
  1719 mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
  1720                                           nsIDOMNode *aNode,
  1721                                           int32_t aOffset,
  1722                                           nsIDOMRange **aRange)
  1724   *aRange = nullptr;
  1726   nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection));
  1728   nsTArray<nsRange*> ranges;
  1729   nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
  1730   nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, aOffset,
  1731                                                    true, &ranges);
  1732   NS_ENSURE_SUCCESS(rv, rv);
  1734   if (ranges.Length() == 0)
  1735     return NS_OK; // no matches
  1737   // there may be more than one range returned, and we don't know what do
  1738   // do with that, so just get the first one
  1739   NS_ADDREF(*aRange = ranges[0]);
  1740   return NS_OK;
  1743 nsresult
  1744 mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection)
  1746   // integrity check - remove ranges that have collapsed to nothing. This
  1747   // can happen if the node containing a highlighted word was removed.
  1748   NS_ENSURE_ARG_POINTER(aSelection);
  1750   int32_t count;
  1751   aSelection->GetRangeCount(&count);
  1753   for (int32_t index = 0; index < count; index++)
  1755     nsCOMPtr<nsIDOMRange> checkRange;
  1756     aSelection->GetRangeAt(index, getter_AddRefs(checkRange));
  1758     if (checkRange)
  1760       bool collapsed;
  1761       checkRange->GetCollapsed(&collapsed);
  1762       if (collapsed)
  1764         RemoveRange(aSelection, checkRange);
  1765         index--;
  1766         count--;
  1771   return NS_OK;
  1775 // mozInlineSpellChecker::RemoveRange
  1776 //
  1777 //    For performance reasons, we have an upper bound on the number of word
  1778 //    ranges  in the spell check selection. When removing a range from the
  1779 //    selection, we need to decrement mNumWordsInSpellSelection
  1781 nsresult
  1782 mozInlineSpellChecker::RemoveRange(nsISelection* aSpellCheckSelection,
  1783                                    nsIDOMRange* aRange)
  1785   NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
  1786   NS_ENSURE_ARG_POINTER(aRange);
  1788   nsresult rv = aSpellCheckSelection->RemoveRange(aRange);
  1789   if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection)
  1790     mNumWordsInSpellSelection--;
  1792   return rv;
  1796 // mozInlineSpellChecker::AddRange
  1797 //
  1798 //    For performance reasons, we have an upper bound on the number of word
  1799 //    ranges we'll add to the spell check selection. Once we reach that upper
  1800 //    bound, stop adding the ranges
  1802 nsresult
  1803 mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
  1804                                 nsIDOMRange* aRange)
  1806   NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
  1807   NS_ENSURE_ARG_POINTER(aRange);
  1809   nsresult rv = NS_OK;
  1811   if (!SpellCheckSelectionIsFull())
  1813     rv = aSpellCheckSelection->AddRange(aRange);
  1814     if (NS_SUCCEEDED(rv))
  1815       mNumWordsInSpellSelection++;
  1818   return rv;
  1821 nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
  1823   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
  1824   NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
  1826   nsCOMPtr<nsISelectionController> selcon;
  1827   nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
  1828   NS_ENSURE_SUCCESS(rv, rv); 
  1830   nsCOMPtr<nsISelection> spellCheckSelection;
  1831   return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
  1834 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
  1836   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
  1837   NS_ENSURE_TRUE(editor, NS_OK);
  1839   // figure out the old caret position based on the current selection
  1840   nsCOMPtr<nsISelection> selection;
  1841   nsresult rv = editor->GetSelection(getter_AddRefs(selection));
  1842   NS_ENSURE_SUCCESS(rv, rv);
  1844   rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
  1845   NS_ENSURE_SUCCESS(rv, rv);
  1847   selection->GetFocusOffset(&mCurrentSelectionOffset);
  1849   return NS_OK;
  1852 // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
  1853 // for XPCOM's rap sheet
  1854 bool // static
  1855 ContentIsDescendantOf(nsINode* aPossibleDescendant,
  1856                       nsINode* aPossibleAncestor)
  1858   NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
  1859   NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
  1861   do {
  1862     if (aPossibleDescendant == aPossibleAncestor)
  1863       return true;
  1864     aPossibleDescendant = aPossibleDescendant->GetParentNode();
  1865   } while (aPossibleDescendant);
  1867   return false;
  1870 // mozInlineSpellChecker::HandleNavigationEvent
  1871 //
  1872 //    Acts upon mouse clicks and keyboard navigation changes, spell checking
  1873 //    the previous word if the new navigation location moves us to another
  1874 //    word.
  1875 //
  1876 //    This is complicated by the fact that our mouse events are happening after
  1877 //    selection has been changed to account for the mouse click. But keyboard
  1878 //    events are happening before the caret selection has changed. Working
  1879 //    around this by letting keyboard events setting forceWordSpellCheck to
  1880 //    true. aNewPositionOffset also tries to work around this for the
  1881 //    DOM_VK_RIGHT and DOM_VK_LEFT cases.
  1883 nsresult
  1884 mozInlineSpellChecker::HandleNavigationEvent(bool aForceWordSpellCheck,
  1885                                              int32_t aNewPositionOffset)
  1887   nsresult rv;
  1889   // If we already handled the navigation event and there is no possibility
  1890   // anything has changed since then, we don't have to do anything. This
  1891   // optimization makes a noticeable difference when you hold down a navigation
  1892   // key like Page Down.
  1893   if (! mNeedsCheckAfterNavigation)
  1894     return NS_OK;
  1896   nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
  1897   int32_t currentAnchorOffset = mCurrentSelectionOffset;
  1899   // now remember the new focus position resulting from the event
  1900   rv = SaveCurrentSelectionPosition();
  1901   NS_ENSURE_SUCCESS(rv, rv);
  1903   bool shouldPost;
  1904   mozInlineSpellStatus status(this);
  1905   rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
  1906                                 currentAnchorNode, currentAnchorOffset,
  1907                                 mCurrentSelectionAnchorNode, mCurrentSelectionOffset,
  1908                                 &shouldPost);
  1909   NS_ENSURE_SUCCESS(rv, rv);
  1910   if (shouldPost) {
  1911     rv = ScheduleSpellCheck(status);
  1912     NS_ENSURE_SUCCESS(rv, rv);
  1915   return NS_OK;
  1918 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
  1920   nsAutoString eventType;
  1921   aEvent->GetType(eventType);
  1923   if (eventType.EqualsLiteral("blur")) {
  1924     return Blur(aEvent);
  1926   if (eventType.EqualsLiteral("click")) {
  1927     return MouseClick(aEvent);
  1929   if (eventType.EqualsLiteral("keypress")) {
  1930     return KeyPress(aEvent);
  1933   return NS_OK;
  1936 nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
  1938   // force spellcheck on blur, for instance when tabbing out of a textbox
  1939   HandleNavigationEvent(true);
  1940   return NS_OK;
  1943 nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
  1945   nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
  1946   NS_ENSURE_TRUE(mouseEvent, NS_OK);
  1948   // ignore any errors from HandleNavigationEvent as we don't want to prevent 
  1949   // anyone else from seeing this event.
  1950   int16_t button;
  1951   mouseEvent->GetButton(&button);
  1952   HandleNavigationEvent(button != 0);
  1953   return NS_OK;
  1956 nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
  1958   nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
  1959   NS_ENSURE_TRUE(keyEvent, NS_OK);
  1961   uint32_t keyCode;
  1962   keyEvent->GetKeyCode(&keyCode);
  1964   // we only care about navigation keys that moved selection 
  1965   switch (keyCode)
  1967     case nsIDOMKeyEvent::DOM_VK_RIGHT:
  1968     case nsIDOMKeyEvent::DOM_VK_LEFT:
  1969       HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
  1970       break;
  1971     case nsIDOMKeyEvent::DOM_VK_UP:
  1972     case nsIDOMKeyEvent::DOM_VK_DOWN:
  1973     case nsIDOMKeyEvent::DOM_VK_HOME:
  1974     case nsIDOMKeyEvent::DOM_VK_END:
  1975     case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
  1976     case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
  1977       HandleNavigationEvent(true /* force a spelling correction */);
  1978       break;
  1981   return NS_OK;
  1984 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
  1985 class UpdateCurrentDictionaryCallback MOZ_FINAL : public nsIEditorSpellCheckCallback
  1987 public:
  1988   NS_DECL_ISUPPORTS
  1990   explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
  1991                                            uint32_t aDisabledAsyncToken)
  1992     : mSpellChecker(aSpellChecker), mDisabledAsyncToken(aDisabledAsyncToken) {}
  1994   NS_IMETHOD EditorSpellCheckDone()
  1996     // Ignore this callback if SetEnableRealTimeSpell(false) was called after
  1997     // the UpdateCurrentDictionary call that triggered it.
  1998     return mSpellChecker->mDisabledAsyncToken > mDisabledAsyncToken ?
  1999            NS_OK :
  2000            mSpellChecker->CurrentDictionaryUpdated();
  2003 private:
  2004   nsRefPtr<mozInlineSpellChecker> mSpellChecker;
  2005   uint32_t mDisabledAsyncToken;
  2006 };
  2007 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
  2009 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
  2011   // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
  2012   // checker is being initialized.  Calling UpdateCurrentDictionary on
  2013   // mPendingSpellCheck simply queues the dictionary update after the init.
  2014   nsCOMPtr<nsIEditorSpellCheck> spellCheck = mSpellCheck ? mSpellCheck :
  2015                                              mPendingSpellCheck;
  2016   if (!spellCheck) {
  2017     return NS_OK;
  2020   if (NS_FAILED(spellCheck->GetCurrentDictionary(mPreviousDictionary))) {
  2021     mPreviousDictionary.Truncate();
  2024   nsRefPtr<UpdateCurrentDictionaryCallback> cb =
  2025     new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
  2026   NS_ENSURE_STATE(cb);
  2027   nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
  2028   if (NS_FAILED(rv)) {
  2029     cb = nullptr;
  2030     NS_ENSURE_SUCCESS(rv, rv);
  2032   mNumPendingUpdateCurrentDictionary++;
  2033   ChangeNumPendingSpellChecks(1);
  2035   return NS_OK;
  2038 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
  2039 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated()
  2041   mNumPendingUpdateCurrentDictionary--;
  2042   NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0,
  2043                "CurrentDictionaryUpdated called without corresponding "
  2044                "UpdateCurrentDictionary call!");
  2045   ChangeNumPendingSpellChecks(-1);
  2047   nsAutoString currentDictionary;
  2048   if (!mSpellCheck ||
  2049       NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) {
  2050     currentDictionary.Truncate();
  2053   if (!mPreviousDictionary.Equals(currentDictionary)) {
  2054     nsresult rv = SpellCheckRange(nullptr);
  2055     NS_ENSURE_SUCCESS(rv, rv);
  2058   return NS_OK;
  2061 NS_IMETHODIMP
  2062 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending)
  2064   *aPending = mNumPendingSpellChecks > 0;
  2065   return NS_OK;

mercurial