1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set sw=2 ts=2 sts=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /**
8  * This class is called by the editor to handle spellchecking after various
9  * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
10  * when the text is changed.
11  *
12  * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
13  * notifications to be flushed when we are called from the editor. This is
14  * because the call might originate from a frame, and flushing the
15  * notifications might cause that frame to be deleted.
16  *
17  * We post an event and do all of the spellchecking in that event handler.
18  * We store all DOM pointers in ranges because they are kept up-to-date with
19  * DOM changes that may have happened while the event was on the queue.
20  *
21  * We also allow the spellcheck to be suspended and resumed later. This makes
22  * large pastes or initializations with a lot of text not hang the browser UI.
23  *
24  * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
25  * true when we get any change, and false once there is no possibility
26  * something changed that we need to check on navigation. Navigation events
27  * tend to be a little tricky because we want to check the current word on
28  * exit if something has changed. If we navigate inside the word, we don't want
29  * to do anything. As a result, this flag is cleared in FinishNavigationEvent
30  * when we know that we are checking as a result of navigation.
31  */
32 
33 #include "mozInlineSpellChecker.h"
34 
35 #include "mozilla/Assertions.h"
36 #include "mozilla/Attributes.h"
37 #include "mozilla/EditAction.h"
38 #include "mozilla/EditorBase.h"
39 #include "mozilla/EditorSpellCheck.h"
40 #include "mozilla/EditorUtils.h"
41 #include "mozilla/Logging.h"
42 #include "mozilla/RangeUtils.h"
43 #include "mozilla/Services.h"
44 #include "mozilla/dom/Event.h"
45 #include "mozilla/dom/KeyboardEvent.h"
46 #include "mozilla/dom/KeyboardEventBinding.h"
47 #include "mozilla/dom/MouseEvent.h"
48 #include "mozilla/dom/Selection.h"
49 #include "mozInlineSpellWordUtil.h"
50 #include "nsCOMPtr.h"
51 #include "nsCRT.h"
52 #include "nsGenericHTMLElement.h"
53 #include "nsRange.h"
54 #include "nsIPrefBranch.h"
55 #include "nsIPrefService.h"
56 #include "nsIRunnable.h"
57 #include "nsServiceManagerUtils.h"
58 #include "nsString.h"
59 #include "nsThreadUtils.h"
60 #include "nsUnicharUtils.h"
61 #include "nsIContent.h"
62 #include "nsIContentInlines.h"
63 #include "nsRange.h"
64 #include "nsContentUtils.h"
65 #include "nsIObserverService.h"
66 #include "prtime.h"
67 
68 using mozilla::LogLevel;
69 using namespace mozilla;
70 using namespace mozilla::dom;
71 using namespace mozilla::ipc;
72 
73 // the number of milliseconds that we will take at once to do spellchecking
74 #define INLINESPELL_CHECK_TIMEOUT 1
75 
76 // The number of words to check before we look at the time to see if
77 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
78 // stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
79 // be too short to a low-end machine.
80 #define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
81 
82 // The maximum number of words to check word via IPC.
83 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
84 
85 // These notifications are broadcast when spell check starts and ends.  STARTED
86 // must always be followed by ENDED.
87 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
88 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
89 
90 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
91 
92 static const char kMaxSpellCheckSelectionSize[] =
93     "extensions.spellcheck.inline.max-misspellings";
94 static const PRTime kMaxSpellCheckTimeInUsec =
95     INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
96 
mozInlineSpellStatus(mozInlineSpellChecker * aSpellChecker,const Operation aOp,RefPtr<nsRange> && aRange,RefPtr<nsRange> && aCreatedRange,RefPtr<nsRange> && aAnchorRange,const bool aForceNavigationWordCheck,const int32_t aNewNavigationPositionOffset)97 mozInlineSpellStatus::mozInlineSpellStatus(
98     mozInlineSpellChecker* aSpellChecker, const Operation aOp,
99     RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
100     RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
101     const int32_t aNewNavigationPositionOffset)
102     : mSpellChecker(aSpellChecker),
103       mRange(std::move(aRange)),
104       mOp(aOp),
105       mCreatedRange(std::move(aCreatedRange)),
106       mAnchorRange(std::move(aAnchorRange)),
107       mForceNavigationWordCheck(aForceNavigationWordCheck),
108       mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}
109 
110 // mozInlineSpellStatus::CreateForEditorChange
111 //
112 //    This is the most complicated case. For changes, we need to compute the
113 //    range of stuff that changed based on the old and new caret positions,
114 //    as well as use a range possibly provided by the editor (start and end,
115 //    which are usually nullptr) to get a range with the union of these.
116 
117 // static
118 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
CreateForEditorChange(mozInlineSpellChecker & aSpellChecker,const EditSubAction aEditSubAction,nsINode * aAnchorNode,uint32_t aAnchorOffset,nsINode * aPreviousNode,uint32_t aPreviousOffset,nsINode * aStartNode,uint32_t aStartOffset,nsINode * aEndNode,uint32_t aEndOffset)119 mozInlineSpellStatus::CreateForEditorChange(
120     mozInlineSpellChecker& aSpellChecker, const EditSubAction aEditSubAction,
121     nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
122     uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
123     nsINode* aEndNode, uint32_t aEndOffset) {
124   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
125 
126   if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
127     return Err(NS_ERROR_FAILURE);
128   }
129 
130   bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
131   if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
132     // IME may remove the previous node if it cancels composition when
133     // there is no text around the composition.
134     deleted = !aPreviousNode->IsInComposedDoc();
135   }
136 
137   // save the anchor point as a range so we can find the current word later
138   RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
139       aAnchorNode, aAnchorOffset);
140   if (NS_WARN_IF(!anchorRange)) {
141     return Err(NS_ERROR_FAILURE);
142   }
143 
144   // Deletes are easy, the range is just the current anchor. We set the range
145   // to check to be empty, FinishInitOnEvent will fill in the range to be
146   // the current word.
147   RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);
148 
149   // On insert save this range: DoSpellCheck optimizes things in this range.
150   // Otherwise, just leave this nullptr.
151   RefPtr<nsRange> createdRange =
152       (aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;
153 
154   UniquePtr<mozInlineSpellStatus> status{
155       /* The constructor is `private`, hence the explicit allocation. */
156       new mozInlineSpellStatus{&aSpellChecker,
157                                deleted ? eOpChangeDelete : eOpChange,
158                                std::move(range), std::move(createdRange),
159                                std::move(anchorRange), false, 0}};
160   if (deleted) {
161     return status;
162   }
163 
164   // ...we need to put the start and end in the correct order
165   ErrorResult errorResult;
166   int16_t cmpResult = status->mAnchorRange->ComparePoint(
167       *aPreviousNode, aPreviousOffset, errorResult);
168   if (NS_WARN_IF(errorResult.Failed())) {
169     return Err(errorResult.StealNSResult());
170   }
171   nsresult rv;
172   if (cmpResult < 0) {
173     // previous anchor node is before the current anchor
174     rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
175                                         aAnchorNode, aAnchorOffset);
176     if (NS_WARN_IF(NS_FAILED(rv))) {
177       return Err(rv);
178     }
179   } else {
180     // previous anchor node is after (or the same as) the current anchor
181     rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
182                                         aPreviousNode, aPreviousOffset);
183     if (NS_WARN_IF(NS_FAILED(rv))) {
184       return Err(rv);
185     }
186   }
187 
188   // if we were given a range, we need to expand our range to encompass it
189   if (aStartNode && aEndNode) {
190     cmpResult =
191         status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
192     if (NS_WARN_IF(errorResult.Failed())) {
193       return Err(errorResult.StealNSResult());
194     }
195     if (cmpResult < 0) {  // given range starts before
196       rv = status->mRange->SetStart(aStartNode, aStartOffset);
197       if (NS_WARN_IF(NS_FAILED(rv))) {
198         return Err(rv);
199       }
200     }
201 
202     cmpResult =
203         status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
204     if (NS_WARN_IF(errorResult.Failed())) {
205       return Err(errorResult.StealNSResult());
206     }
207     if (cmpResult > 0) {  // given range ends after
208       rv = status->mRange->SetEnd(aEndNode, aEndOffset);
209       if (NS_WARN_IF(NS_FAILED(rv))) {
210         return Err(rv);
211       }
212     }
213   }
214 
215   return status;
216 }
217 
218 // mozInlineSpellStatus::CreateForNavigation
219 //
220 //    For navigation events, we just need to store the new and old positions.
221 //
222 //    In some cases, we detect that we shouldn't check. If this event should
223 //    not be processed, *aContinue will be false.
224 
225 // static
226 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
CreateForNavigation(mozInlineSpellChecker & aSpellChecker,bool aForceCheck,int32_t aNewPositionOffset,nsINode * aOldAnchorNode,uint32_t aOldAnchorOffset,nsINode * aNewAnchorNode,uint32_t aNewAnchorOffset,bool * aContinue)227 mozInlineSpellStatus::CreateForNavigation(
228     mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
229     int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
230     uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
231     uint32_t aNewAnchorOffset, bool* aContinue) {
232   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
233 
234   RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
235       aNewAnchorNode, aNewAnchorOffset);
236   if (NS_WARN_IF(!anchorRange)) {
237     return Err(NS_ERROR_FAILURE);
238   }
239 
240   UniquePtr<mozInlineSpellStatus> status{
241       /* The constructor is `private`, hence the explicit allocation. */
242       new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
243                                std::move(anchorRange), aForceCheck,
244                                aNewPositionOffset}};
245 
246   // get the root node for checking
247   EditorBase* editorBase = status->mSpellChecker->mEditorBase;
248   if (NS_WARN_IF(!editorBase)) {
249     return Err(NS_ERROR_FAILURE);
250   }
251   Element* root = editorBase->GetRoot();
252   if (NS_WARN_IF(!root)) {
253     return Err(NS_ERROR_FAILURE);
254   }
255   // the anchor node might not be in the DOM anymore, check
256   if (root && aOldAnchorNode &&
257       !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
258     *aContinue = false;
259     return status;
260   }
261 
262   status->mOldNavigationAnchorRange =
263       mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
264                                                      aOldAnchorOffset);
265   if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
266     return Err(NS_ERROR_FAILURE);
267   }
268 
269   *aContinue = true;
270   return status;
271 }
272 
273 // mozInlineSpellStatus::CreateForSelection
274 //
275 //    It is easy for selections since we always re-check the spellcheck
276 //    selection.
277 
278 // static
CreateForSelection(mozInlineSpellChecker & aSpellChecker)279 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
280     mozInlineSpellChecker& aSpellChecker) {
281   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
282 
283   UniquePtr<mozInlineSpellStatus> status{
284       /* The constructor is `private`, hence the explicit allocation. */
285       new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
286                                nullptr, false, 0}};
287   return status;
288 }
289 
290 // mozInlineSpellStatus::CreateForRange
291 //
292 //    Called to cause the spellcheck of the given range. This will look like
293 //    a change operation over the given range.
294 
295 // static
CreateForRange(mozInlineSpellChecker & aSpellChecker,nsRange * aRange)296 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
297     mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
298   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
299           ("%s: range=%p", __FUNCTION__, aRange));
300 
301   UniquePtr<mozInlineSpellStatus> status{
302       /* The constructor is `private`, hence the explicit allocation. */
303       new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
304                                nullptr, false, 0}};
305 
306   status->mRange = aRange;
307   return status;
308 }
309 
310 // mozInlineSpellStatus::FinishInitOnEvent
311 //
312 //    Called when the event is triggered to complete initialization that
313 //    might require the WordUtil. This calls to the operation-specific
314 //    initializer, and also sets the range to be the entire element if it
315 //    is nullptr.
316 //
317 //    Watch out: the range might still be nullptr if there is nothing to do,
318 //    the caller will have to check for this.
319 
FinishInitOnEvent(mozInlineSpellWordUtil & aWordUtil)320 nsresult mozInlineSpellStatus::FinishInitOnEvent(
321     mozInlineSpellWordUtil& aWordUtil) {
322   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
323           ("%s: mRange=%p", __FUNCTION__, mRange.get()));
324 
325   nsresult rv;
326   if (!mRange) {
327     rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
328                                             getter_AddRefs(mRange));
329     NS_ENSURE_SUCCESS(rv, rv);
330   }
331 
332   switch (mOp) {
333     case eOpChange:
334       if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
335       break;
336     case eOpChangeDelete:
337       if (mAnchorRange) {
338         rv = FillNoCheckRangeFromAnchor(aWordUtil);
339         NS_ENSURE_SUCCESS(rv, rv);
340       }
341       // Delete events will have no range for the changed text (because it was
342       // deleted), and CreateForEditorChange will set it to nullptr. Here, we
343       // select the entire word to cause any underlining to be removed.
344       mRange = mNoCheckRange;
345       break;
346     case eOpNavigation:
347       return FinishNavigationEvent(aWordUtil);
348     case eOpSelection:
349       // this gets special handling in ResumeCheck
350       break;
351     case eOpResume:
352       // everything should be initialized already in this case
353       break;
354     default:
355       MOZ_ASSERT_UNREACHABLE("Bad operation");
356       return NS_ERROR_NOT_INITIALIZED;
357   }
358   return NS_OK;
359 }
360 
361 // mozInlineSpellStatus::FinishNavigationEvent
362 //
363 //    This verifies that we need to check the word at the previous caret
364 //    position. Now that we have the word util, we can find the word belonging
365 //    to the previous caret position. If the new position is inside that word,
366 //    we don't want to do anything. In this case, we'll nullptr out mRange so
367 //    that the caller will know not to continue.
368 //
369 //    Notice that we don't set mNoCheckRange. We check here whether the cursor
370 //    is in the word that needs checking, so it isn't necessary. Plus, the
371 //    spellchecker isn't guaranteed to only check the given word, and it could
372 //    remove the underline from the new word under the cursor.
373 
FinishNavigationEvent(mozInlineSpellWordUtil & aWordUtil)374 nsresult mozInlineSpellStatus::FinishNavigationEvent(
375     mozInlineSpellWordUtil& aWordUtil) {
376   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
377 
378   RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
379   if (!editorBase) {
380     return NS_ERROR_FAILURE;  // editor is gone
381   }
382 
383   MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");
384 
385   if (!mOldNavigationAnchorRange->IsPositioned()) {
386     return NS_ERROR_NOT_INITIALIZED;
387   }
388 
389   // get the DOM position of the old caret, the range should be collapsed
390   nsCOMPtr<nsINode> oldAnchorNode =
391       mOldNavigationAnchorRange->GetStartContainer();
392   uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
393 
394   // find the word on the old caret position, this is the one that we MAY need
395   // to check
396   RefPtr<nsRange> oldWord;
397   nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
398                                           static_cast<int32_t>(oldAnchorOffset),
399                                           getter_AddRefs(oldWord));
400   NS_ENSURE_SUCCESS(rv, rv);
401 
402   // aWordUtil.GetRangeForWord flushes pending notifications, check editor
403   // again.
404   if (!mSpellChecker->mEditorBase) {
405     return NS_ERROR_FAILURE;  // editor is gone
406   }
407 
408   // get the DOM position of the new caret, the range should be collapsed
409   nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
410   uint32_t newAnchorOffset = mAnchorRange->StartOffset();
411 
412   // see if the new cursor position is in the word of the old cursor position
413   bool isInRange = false;
414   if (!mForceNavigationWordCheck) {
415     ErrorResult err;
416     isInRange = oldWord->IsPointInRange(
417         *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
418     if (NS_WARN_IF(err.Failed())) {
419       return err.StealNSResult();
420     }
421   }
422 
423   if (isInRange) {
424     // caller should give up
425     mRange = nullptr;
426   } else {
427     // check the old word
428     mRange = oldWord;
429 
430     // Once we've spellchecked the current word, we don't need to spellcheck
431     // for any more navigation events.
432     mSpellChecker->mNeedsCheckAfterNavigation = false;
433   }
434   return NS_OK;
435 }
436 
437 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
438 //
439 //    Given the mAnchorRange object, computes the range of the word it is on
440 //    (if any) and fills that range into mNoCheckRange. This is used for
441 //    change and navigation events to know which word we should skip spell
442 //    checking on
443 
FillNoCheckRangeFromAnchor(mozInlineSpellWordUtil & aWordUtil)444 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
445     mozInlineSpellWordUtil& aWordUtil) {
446   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
447 
448   if (!mAnchorRange->IsPositioned()) {
449     return NS_ERROR_NOT_INITIALIZED;
450   }
451   nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
452   uint32_t anchorOffset = mAnchorRange->StartOffset();
453   return aWordUtil.GetRangeForWord(anchorNode,
454                                    static_cast<int32_t>(anchorOffset),
455                                    getter_AddRefs(mNoCheckRange));
456 }
457 
458 // mozInlineSpellStatus::GetDocument
459 //
460 //    Returns the Document object for the document for the
461 //    current spellchecker.
462 
GetDocument() const463 Document* mozInlineSpellStatus::GetDocument() const {
464   if (!mSpellChecker->mEditorBase) {
465     return nullptr;
466   }
467 
468   return mSpellChecker->mEditorBase->GetDocument();
469 }
470 
471 // mozInlineSpellStatus::PositionToCollapsedRange
472 //
473 //    Converts a given DOM position to a collapsed range covering that
474 //    position. We use ranges to store DOM positions becuase they stay
475 //    updated as the DOM is changed.
476 
477 // static
PositionToCollapsedRange(nsINode * aNode,uint32_t aOffset)478 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
479     nsINode* aNode, uint32_t aOffset) {
480   if (NS_WARN_IF(!aNode)) {
481     return nullptr;
482   }
483   IgnoredErrorResult ignoredError;
484   RefPtr<nsRange> range =
485       nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
486   NS_WARNING_ASSERTION(!ignoredError.Failed(),
487                        "Creating collapsed range failed");
488   return range.forget();
489 }
490 
491 // mozInlineSpellResume
492 
493 class mozInlineSpellResume : public Runnable {
494  public:
mozInlineSpellResume(UniquePtr<mozInlineSpellStatus> && aStatus,uint32_t aDisabledAsyncToken)495   mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
496                        uint32_t aDisabledAsyncToken)
497       : Runnable("mozInlineSpellResume"),
498         mDisabledAsyncToken(aDisabledAsyncToken),
499         mStatus(std::move(aStatus)) {}
500 
Post()501   nsresult Post() {
502     nsCOMPtr<nsIRunnable> runnable(this);
503     return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
504                                            EventQueuePriority::Idle);
505   }
506 
Run()507   NS_IMETHOD Run() override {
508     // Discard the resumption if the spell checker was disabled after the
509     // resumption was scheduled.
510     if (mDisabledAsyncToken ==
511         mStatus->mSpellChecker->GetDisabledAsyncToken()) {
512       mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
513     }
514     return NS_OK;
515   }
516 
517  private:
518   uint32_t mDisabledAsyncToken;
519   UniquePtr<mozInlineSpellStatus> mStatus;
520 };
521 
522 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
523 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
~InitEditorSpellCheckCallback()524   ~InitEditorSpellCheckCallback() {}
525 
526  public:
527   NS_DECL_ISUPPORTS
528 
InitEditorSpellCheckCallback(mozInlineSpellChecker * aSpellChecker)529   explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
530       : mSpellChecker(aSpellChecker) {}
531 
EditorSpellCheckDone()532   NS_IMETHOD EditorSpellCheckDone() override {
533     return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
534   }
535 
Cancel()536   void Cancel() { mSpellChecker = nullptr; }
537 
538  private:
539   RefPtr<mozInlineSpellChecker> mSpellChecker;
540 };
541 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
542 
543 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
544   NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
545   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
546   NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
547   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
548   NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
549 NS_INTERFACE_MAP_END
550 
551 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
552 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
553 
554 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
555                               mCurrentSelectionAnchorNode)
556 
557 mozInlineSpellChecker::SpellCheckingState
558     mozInlineSpellChecker::gCanEnableSpellChecking =
559         mozInlineSpellChecker::SpellCheck_Uninitialized;
560 
mozInlineSpellChecker()561 mozInlineSpellChecker::mozInlineSpellChecker()
562     : mNumWordsInSpellSelection(0),
563       mMaxNumWordsInSpellSelection(250),
564       mNumPendingSpellChecks(0),
565       mNumPendingUpdateCurrentDictionary(0),
566       mDisabledAsyncToken(0),
567       mNeedsCheckAfterNavigation(false),
568       mFullSpellCheckScheduled(false),
569       mIsListeningToEditSubActions(false) {
570   nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
571   if (prefs)
572     prefs->GetIntPref(kMaxSpellCheckSelectionSize,
573                       &mMaxNumWordsInSpellSelection);
574 }
575 
~mozInlineSpellChecker()576 mozInlineSpellChecker::~mozInlineSpellChecker() {}
577 
GetEditorSpellCheck()578 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
579   return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
580 }
581 
582 NS_IMETHODIMP
GetSpellChecker(nsIEditorSpellCheck ** aSpellCheck)583 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
584   *aSpellCheck = mSpellCheck;
585   NS_IF_ADDREF(*aSpellCheck);
586   return NS_OK;
587 }
588 
589 NS_IMETHODIMP
Init(nsIEditor * aEditor)590 mozInlineSpellChecker::Init(nsIEditor* aEditor) {
591   mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
592   return NS_OK;
593 }
594 
595 // mozInlineSpellChecker::Cleanup
596 //
597 //    Called by the editor when the editor is going away. This is important
598 //    because we remove listeners. We do NOT clean up anything else in this
599 //    function, because it can get called while DoSpellCheck is running!
600 //
601 //    Getting the style information there can cause DOM notifications to be
602 //    flushed, which can cause editors to go away which will bring us here.
603 //    We can not do anything that will cause DoSpellCheck to freak out.
604 
605 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
Cleanup(bool aDestroyingFrames)606 mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
607   mNumWordsInSpellSelection = 0;
608   RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
609   nsresult rv = NS_OK;
610   if (!spellCheckSelection) {
611     // Ensure we still unregister event listeners (but return a failure code)
612     UnregisterEventListeners();
613     rv = NS_ERROR_FAILURE;
614   } else {
615     if (!aDestroyingFrames) {
616       spellCheckSelection->RemoveAllRanges(IgnoreErrors());
617     }
618 
619     rv = UnregisterEventListeners();
620   }
621 
622   // Notify ENDED observers now.  If we wait to notify as we normally do when
623   // these async operations finish, then in the meantime the editor may create
624   // another inline spell checker and cause more STARTED and ENDED
625   // notifications to be broadcast.  Interleaved notifications for the same
626   // editor but different inline spell checkers could easily confuse
627   // observers.  They may receive two consecutive STARTED notifications for
628   // example, which we guarantee will not happen.
629 
630   RefPtr<EditorBase> editorBase = std::move(mEditorBase);
631   if (mPendingSpellCheck) {
632     // Cancel the pending editor spell checker initialization.
633     mPendingSpellCheck = nullptr;
634     mPendingInitEditorSpellCheckCallback->Cancel();
635     mPendingInitEditorSpellCheckCallback = nullptr;
636     ChangeNumPendingSpellChecks(-1, editorBase);
637   }
638 
639   // Increment this token so that pending UpdateCurrentDictionary calls and
640   // scheduled spell checks are discarded when they finish.
641   mDisabledAsyncToken++;
642 
643   if (mNumPendingUpdateCurrentDictionary > 0) {
644     // Account for pending UpdateCurrentDictionary calls.
645     ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
646                                 editorBase);
647     mNumPendingUpdateCurrentDictionary = 0;
648   }
649   if (mNumPendingSpellChecks > 0) {
650     // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
651     // pending scheduled spell checks.
652     ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
653   }
654 
655   mFullSpellCheckScheduled = false;
656 
657   return rv;
658 }
659 
660 // mozInlineSpellChecker::CanEnableInlineSpellChecking
661 //
662 //    This function can be called to see if it seems likely that we can enable
663 //    spellchecking before actually creating the InlineSpellChecking objects.
664 //
665 //    The problem is that we can't get the dictionary list without actually
666 //    creating a whole bunch of spellchecking objects. This function tries to
667 //    do that and caches the result so we don't have to keep allocating those
668 //    objects if there are no dictionaries or spellchecking.
669 //
670 //    Whenever dictionaries are added or removed at runtime, this value must be
671 //    updated before an observer notification is sent out about the change, to
672 //    avoid editors getting a wrong cached result.
673 
674 bool  // static
CanEnableInlineSpellChecking()675 mozInlineSpellChecker::CanEnableInlineSpellChecking() {
676   if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
677     gCanEnableSpellChecking = SpellCheck_NotAvailable;
678 
679     nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
680 
681     bool canSpellCheck = false;
682     nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
683     NS_ENSURE_SUCCESS(rv, false);
684 
685     if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
686   }
687   return (gCanEnableSpellChecking == SpellCheck_Available);
688 }
689 
690 void  // static
UpdateCanEnableInlineSpellChecking()691 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
692   gCanEnableSpellChecking = SpellCheck_Uninitialized;
693 }
694 
695 // mozInlineSpellChecker::RegisterEventListeners
696 //
697 //    The inline spell checker listens to mouse events and keyboard navigation
698 //    events.
699 
RegisterEventListeners()700 nsresult mozInlineSpellChecker::RegisterEventListeners() {
701   if (NS_WARN_IF(!mEditorBase)) {
702     return NS_ERROR_FAILURE;
703   }
704 
705   StartToListenToEditSubActions();
706 
707   RefPtr<Document> doc = mEditorBase->GetDocument();
708   if (NS_WARN_IF(!doc)) {
709     return NS_ERROR_FAILURE;
710   }
711   doc->AddEventListener(u"blur"_ns, this, true, false);
712   doc->AddEventListener(u"click"_ns, this, false, false);
713   doc->AddEventListener(u"keypress"_ns, this, false, false);
714   return NS_OK;
715 }
716 
717 // mozInlineSpellChecker::UnregisterEventListeners
718 
UnregisterEventListeners()719 nsresult mozInlineSpellChecker::UnregisterEventListeners() {
720   if (NS_WARN_IF(!mEditorBase)) {
721     return NS_ERROR_FAILURE;
722   }
723 
724   EndListeningToEditSubActions();
725 
726   RefPtr<Document> doc = mEditorBase->GetDocument();
727   if (NS_WARN_IF(!doc)) {
728     return NS_ERROR_FAILURE;
729   }
730   doc->RemoveEventListener(u"blur"_ns, this, true);
731   doc->RemoveEventListener(u"click"_ns, this, false);
732   doc->RemoveEventListener(u"keypress"_ns, this, false);
733   return NS_OK;
734 }
735 
736 // mozInlineSpellChecker::GetEnableRealTimeSpell
737 
738 NS_IMETHODIMP
GetEnableRealTimeSpell(bool * aEnabled)739 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
740   NS_ENSURE_ARG_POINTER(aEnabled);
741   *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
742   return NS_OK;
743 }
744 
745 // mozInlineSpellChecker::SetEnableRealTimeSpell
746 
747 NS_IMETHODIMP
SetEnableRealTimeSpell(bool aEnabled)748 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
749   if (!aEnabled) {
750     mSpellCheck = nullptr;
751     return Cleanup(false);
752   }
753 
754   if (mSpellCheck) {
755     // spellcheck the current contents. SpellCheckRange doesn't supply a created
756     // range to DoSpellCheck, which in our case is the entire range. But this
757     // optimization doesn't matter because there is nothing in the spellcheck
758     // selection when starting, which triggers a better optimization.
759     return SpellCheckRange(nullptr);
760   }
761 
762   if (mPendingSpellCheck) {
763     // The editor spell checker is already being initialized.
764     return NS_OK;
765   }
766 
767   mPendingSpellCheck = new EditorSpellCheck();
768   mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
769 
770   mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
771   nsresult rv = mPendingSpellCheck->InitSpellChecker(
772       mEditorBase, false, mPendingInitEditorSpellCheckCallback);
773   if (NS_FAILED(rv)) {
774     mPendingSpellCheck = nullptr;
775     mPendingInitEditorSpellCheckCallback = nullptr;
776     NS_ENSURE_SUCCESS(rv, rv);
777   }
778 
779   ChangeNumPendingSpellChecks(1);
780 
781   return NS_OK;
782 }
783 
784 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
EditorSpellCheckInited()785 nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
786   MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");
787 
788   // spell checking is enabled, register our event listeners to track navigation
789   RegisterEventListeners();
790 
791   mSpellCheck = mPendingSpellCheck;
792   mPendingSpellCheck = nullptr;
793   mPendingInitEditorSpellCheckCallback = nullptr;
794   ChangeNumPendingSpellChecks(-1);
795 
796   // spellcheck the current contents. SpellCheckRange doesn't supply a created
797   // range to DoSpellCheck, which in our case is the entire range. But this
798   // optimization doesn't matter because there is nothing in the spellcheck
799   // selection when starting, which triggers a better optimization.
800   return SpellCheckRange(nullptr);
801 }
802 
803 // Changes the number of pending spell checks by the given delta.  If the number
804 // becomes zero or nonzero, observers are notified.  See NotifyObservers for
805 // info on the aEditor parameter.
ChangeNumPendingSpellChecks(int32_t aDelta,EditorBase * aEditorBase)806 void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
807     int32_t aDelta, EditorBase* aEditorBase) {
808   int8_t oldNumPending = mNumPendingSpellChecks;
809   mNumPendingSpellChecks += aDelta;
810   MOZ_ASSERT(mNumPendingSpellChecks >= 0,
811              "Unbalanced ChangeNumPendingSpellChecks calls!");
812   if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
813     NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
814   } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
815     NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
816   }
817 }
818 
819 // Broadcasts the given topic to observers.  aEditor is passed to observers if
820 // nonnull; otherwise mEditorBase is passed.
NotifyObservers(const char * aTopic,EditorBase * aEditorBase)821 void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
822                                             EditorBase* aEditorBase) {
823   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
824   if (!os) return;
825   // XXX Do we need to grab the editor here?  If it's necessary, each observer
826   //     should do it instead.
827   RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
828   os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
829                       nullptr);
830 }
831 
832 // mozInlineSpellChecker::SpellCheckAfterEditorChange
833 //
834 //    Called by the editor when nearly anything happens to change the content.
835 //
836 //    The start and end positions specify a range for the thing that happened,
837 //    but these are usually nullptr, even when you'd think they would be useful
838 //    because you want the range (for example, pasting). We ignore them in
839 //    this case.
840 
SpellCheckAfterEditorChange(EditSubAction aEditSubAction,Selection & aSelection,nsINode * aPreviousSelectedNode,uint32_t aPreviousSelectedOffset,nsINode * aStartNode,uint32_t aStartOffset,nsINode * aEndNode,uint32_t aEndOffset)841 nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
842     EditSubAction aEditSubAction, Selection& aSelection,
843     nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
844     nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
845     uint32_t aEndOffset) {
846   nsresult rv;
847   if (!mSpellCheck) return NS_OK;  // disabling spell checking is not an error
848 
849   // this means something has changed, and we never check the current word,
850   // therefore, we should spellcheck for subsequent caret navigations
851   mNeedsCheckAfterNavigation = true;
852 
853   // the anchor node is the position of the caret
854   Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
855       mozInlineSpellStatus::CreateForEditorChange(
856           *this, aEditSubAction, aSelection.GetAnchorNode(),
857           aSelection.AnchorOffset(), aPreviousSelectedNode,
858           aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
859           aEndOffset);
860   if (NS_WARN_IF(res.isErr())) {
861     return res.unwrapErr();
862   }
863 
864   rv = ScheduleSpellCheck(res.unwrap());
865   NS_ENSURE_SUCCESS(rv, rv);
866 
867   // remember the current caret position after every change
868   SaveCurrentSelectionPosition();
869   return NS_OK;
870 }
871 
872 // mozInlineSpellChecker::SpellCheckRange
873 //
874 //    Spellchecks all the words in the given range.
875 //    Supply a nullptr range and this will check the entire editor.
876 
SpellCheckRange(nsRange * aRange)877 nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
878   if (!mSpellCheck) {
879     NS_WARNING_ASSERTION(
880         mPendingSpellCheck,
881         "Trying to spellcheck, but checking seems to be disabled");
882     return NS_ERROR_NOT_INITIALIZED;
883   }
884 
885   UniquePtr<mozInlineSpellStatus> status =
886       mozInlineSpellStatus::CreateForRange(*this, aRange);
887   return ScheduleSpellCheck(std::move(status));
888 }
889 
890 // mozInlineSpellChecker::GetMisspelledWord
891 
892 NS_IMETHODIMP
GetMisspelledWord(nsINode * aNode,int32_t aOffset,nsRange ** newword)893 mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, int32_t aOffset,
894                                          nsRange** newword) {
895   if (NS_WARN_IF(!aNode)) {
896     return NS_ERROR_INVALID_ARG;
897   }
898   RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
899   if (NS_WARN_IF(!spellCheckSelection)) {
900     return NS_ERROR_FAILURE;
901   }
902   return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
903 }
904 
905 // mozInlineSpellChecker::ReplaceWord
906 
907 NS_IMETHODIMP
ReplaceWord(nsINode * aNode,int32_t aOffset,const nsAString & aNewWord)908 mozInlineSpellChecker::ReplaceWord(nsINode* aNode, int32_t aOffset,
909                                    const nsAString& aNewWord) {
910   if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
911     return NS_ERROR_FAILURE;
912   }
913 
914   RefPtr<nsRange> range;
915   nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
916   NS_ENSURE_SUCCESS(res, res);
917 
918   if (!range) {
919     return NS_OK;
920   }
921 
922   // In usual cases, any words shouldn't include line breaks, but technically,
923   // they may include and we need to avoid `HTMLTextAreaElement.value` returns
924   // \r.  Therefore, we need to handle it here.
925   nsString newWord(aNewWord);
926   if (mEditorBase->IsTextEditor()) {
927     nsContentUtils::PlatformToDOMLineBreaks(newWord);
928   }
929 
930   // Blink dispatches cancelable `beforeinput` event at collecting misspelled
931   // word so that we should allow to dispatch cancelable event.
932   RefPtr<EditorBase> editorBase(mEditorBase);
933   DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
934       newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
935   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
936   return NS_OK;
937 }
938 
939 // mozInlineSpellChecker::AddWordToDictionary
940 
941 NS_IMETHODIMP
AddWordToDictionary(const nsAString & word)942 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
943   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
944 
945   nsresult rv = mSpellCheck->AddWordToDictionary(word);
946   NS_ENSURE_SUCCESS(rv, rv);
947 
948   UniquePtr<mozInlineSpellStatus> status =
949       mozInlineSpellStatus::CreateForSelection(*this);
950   return ScheduleSpellCheck(std::move(status));
951 }
952 
953 //  mozInlineSpellChecker::RemoveWordFromDictionary
954 
955 NS_IMETHODIMP
RemoveWordFromDictionary(const nsAString & word)956 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
957   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
958 
959   nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
960   NS_ENSURE_SUCCESS(rv, rv);
961 
962   UniquePtr<mozInlineSpellStatus> status =
963       mozInlineSpellStatus::CreateForRange(*this, nullptr);
964   return ScheduleSpellCheck(std::move(status));
965 }
966 
967 // mozInlineSpellChecker::IgnoreWord
968 
969 NS_IMETHODIMP
IgnoreWord(const nsAString & word)970 mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
971   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
972 
973   nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
974   NS_ENSURE_SUCCESS(rv, rv);
975 
976   UniquePtr<mozInlineSpellStatus> status =
977       mozInlineSpellStatus::CreateForSelection(*this);
978   return ScheduleSpellCheck(std::move(status));
979 }
980 
981 // mozInlineSpellChecker::IgnoreWords
982 
983 NS_IMETHODIMP
IgnoreWords(const nsTArray<nsString> & aWordsToIgnore)984 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
985   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
986 
987   // add each word to the ignore list and then recheck the document
988   for (auto& word : aWordsToIgnore) {
989     mSpellCheck->IgnoreWordAllOccurrences(word);
990   }
991 
992   UniquePtr<mozInlineSpellStatus> status =
993       mozInlineSpellStatus::CreateForSelection(*this);
994   return ScheduleSpellCheck(std::move(status));
995 }
996 
DidSplitNode(nsINode * aExistingRightNode,nsINode * aNewLeftNode)997 void mozInlineSpellChecker::DidSplitNode(nsINode* aExistingRightNode,
998                                          nsINode* aNewLeftNode) {
999   if (!mIsListeningToEditSubActions) {
1000     return;
1001   }
1002   SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
1003 }
1004 
DidJoinNodes(nsINode & aLeftNode,nsINode & aRightNode)1005 void mozInlineSpellChecker::DidJoinNodes(nsINode& aLeftNode,
1006                                          nsINode& aRightNode) {
1007   if (!mIsListeningToEditSubActions) {
1008     return;
1009   }
1010   SpellCheckBetweenNodes(&aRightNode, 0, &aRightNode, 0);
1011 }
1012 
1013 // mozInlineSpellChecker::MakeSpellCheckRange
1014 //
1015 //    Given begin and end positions, this function constructs a range as
1016 //    required for ScheduleSpellCheck. If the start and end nodes are nullptr,
1017 //    then the entire range will be selected, and you can supply -1 as the
1018 //    offset to the end range to select all of that node.
1019 //
1020 //    If the resulting range would be empty, nullptr is put into *aRange and the
1021 //    function succeeds.
1022 
MakeSpellCheckRange(nsINode * aStartNode,int32_t aStartOffset,nsINode * aEndNode,int32_t aEndOffset,nsRange ** aRange) const1023 nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
1024                                                     int32_t aStartOffset,
1025                                                     nsINode* aEndNode,
1026                                                     int32_t aEndOffset,
1027                                                     nsRange** aRange) const {
1028   nsresult rv;
1029   *aRange = nullptr;
1030 
1031   if (NS_WARN_IF(!mEditorBase)) {
1032     return NS_ERROR_FAILURE;
1033   }
1034 
1035   RefPtr<Document> doc = mEditorBase->GetDocument();
1036   if (NS_WARN_IF(!doc)) {
1037     return NS_ERROR_FAILURE;
1038   }
1039 
1040   RefPtr<nsRange> range = nsRange::Create(doc);
1041 
1042   // possibly use full range of the editor
1043   if (!aStartNode || !aEndNode) {
1044     Element* domRootElement = mEditorBase->GetRoot();
1045     if (NS_WARN_IF(!domRootElement)) {
1046       return NS_ERROR_FAILURE;
1047     }
1048     aStartNode = aEndNode = domRootElement;
1049     aStartOffset = 0;
1050     aEndOffset = -1;
1051   }
1052 
1053   if (aEndOffset == -1) {
1054     // It's hard to say whether it's better to just do nsINode::GetChildCount or
1055     // get the ChildNodes() and then its length.  The latter is faster if we
1056     // keep going through this code for the same nodes (because it caches the
1057     // length).  The former is faster if we keep getting different nodes here...
1058     //
1059     // Let's do the thing which can't end up with bad O(N^2) behavior.
1060     aEndOffset = aEndNode->ChildNodes()->Length();
1061   }
1062 
1063   // sometimes we are are requested to check an empty range (possibly an empty
1064   // document). This will result in assertions later.
1065   if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
1066 
1067   if (aEndOffset) {
1068     rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
1069     if (NS_WARN_IF(NS_FAILED(rv))) {
1070       return rv;
1071     }
1072   } else {
1073     rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
1074                                RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
1075     if (NS_WARN_IF(NS_FAILED(rv))) {
1076       return rv;
1077     }
1078   }
1079 
1080   range.swap(*aRange);
1081   return NS_OK;
1082 }
1083 
SpellCheckBetweenNodes(nsINode * aStartNode,int32_t aStartOffset,nsINode * aEndNode,int32_t aEndOffset)1084 nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
1085                                                        int32_t aStartOffset,
1086                                                        nsINode* aEndNode,
1087                                                        int32_t aEndOffset) {
1088   RefPtr<nsRange> range;
1089   nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
1090                                     aEndOffset, getter_AddRefs(range));
1091   NS_ENSURE_SUCCESS(rv, rv);
1092 
1093   if (!range) return NS_OK;  // range is empty: nothing to do
1094 
1095   UniquePtr<mozInlineSpellStatus> status =
1096       mozInlineSpellStatus::CreateForRange(*this, range);
1097   return ScheduleSpellCheck(std::move(status));
1098 }
1099 
1100 // mozInlineSpellChecker::ShouldSpellCheckNode
1101 //
1102 //    There are certain conditions when we don't want to spell check a node. In
1103 //    particular quotations, moz signatures, etc. This routine returns false
1104 //    for these cases.
1105 
1106 // static
ShouldSpellCheckNode(EditorBase * aEditorBase,nsINode * aNode)1107 bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
1108                                                  nsINode* aNode) {
1109   MOZ_ASSERT(aNode);
1110   if (!aNode->IsContent()) return false;
1111 
1112   nsIContent* content = aNode->AsContent();
1113 
1114   if (aEditorBase->IsMailEditor()) {
1115     nsIContent* parent = content->GetParent();
1116     while (parent) {
1117       if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
1118           parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1119                                            nsGkAtoms::cite, eIgnoreCase)) {
1120         return false;
1121       }
1122       if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
1123           parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1124                                            nsGkAtoms::mozsignature,
1125                                            eIgnoreCase)) {
1126         return false;
1127       }
1128       if (parent->IsHTMLElement(nsGkAtoms::div) &&
1129           parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1130                                            nsGkAtoms::mozfwcontainer,
1131                                            eIgnoreCase)) {
1132         return false;
1133       }
1134 
1135       parent = parent->GetParent();
1136     }
1137   } else {
1138     // Check spelling only if the node is editable, and GetSpellcheck() is true
1139     // on the nearest HTMLElement ancestor.
1140     if (!content->IsEditable()) {
1141       return false;
1142     }
1143 
1144     // Make sure that we can always turn on spell checking for inputs/textareas.
1145     // Note that because of the previous check, at this point we know that the
1146     // node is editable.
1147     if (content->IsInNativeAnonymousSubtree()) {
1148       nsIContent* node = content->GetParent();
1149       while (node && node->IsInNativeAnonymousSubtree()) {
1150         node = node->GetParent();
1151       }
1152       if (node && node->IsTextControlElement()) {
1153         return true;
1154       }
1155     }
1156 
1157     // Get HTML element ancestor (might be aNode itself, although probably that
1158     // has to be a text node in real life here)
1159     nsIContent* parent = content;
1160     while (!parent->IsHTMLElement()) {
1161       parent = parent->GetParent();
1162       if (!parent) {
1163         return true;
1164       }
1165     }
1166 
1167     // See if it's spellcheckable
1168     return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
1169   }
1170 
1171   return true;
1172 }
1173 
1174 // mozInlineSpellChecker::ScheduleSpellCheck
1175 //
1176 //    This is called by code to do the actual spellchecking. We will set up
1177 //    the proper structures for calls to DoSpellCheck.
1178 
ScheduleSpellCheck(UniquePtr<mozInlineSpellStatus> && aStatus)1179 nsresult mozInlineSpellChecker::ScheduleSpellCheck(
1180     UniquePtr<mozInlineSpellStatus>&& aStatus) {
1181   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1182           ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
1183            mFullSpellCheckScheduled));
1184 
1185   if (mFullSpellCheckScheduled) {
1186     // Just ignore this; we're going to spell-check everything anyway
1187     return NS_OK;
1188   }
1189   // Cache the value because we are going to move aStatus's ownership to
1190   // the new created mozInlineSpellResume instance.
1191   bool isFullSpellCheck = aStatus->IsFullSpellCheck();
1192 
1193   RefPtr<mozInlineSpellResume> resume =
1194       new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
1195   NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1196 
1197   nsresult rv = resume->Post();
1198   if (NS_SUCCEEDED(rv)) {
1199     if (isFullSpellCheck) {
1200       // We're going to check everything.  Suppress further spell-check attempts
1201       // until that happens.
1202       mFullSpellCheckScheduled = true;
1203     }
1204     ChangeNumPendingSpellChecks(1);
1205   }
1206   return rv;
1207 }
1208 
1209 // mozInlineSpellChecker::DoSpellCheckSelection
1210 //
1211 //    Called to re-check all misspelled words. We iterate over all ranges in
1212 //    the selection and call DoSpellCheck on them. This is used when a word
1213 //    is ignored or added to the dictionary: all instances of that word should
1214 //    be removed from the selection.
1215 //
1216 //    FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1217 //    Typically, checking this small amount of text is relatively fast, but
1218 //    for large numbers of words, a lag may be noticeable.
1219 
DoSpellCheckSelection(mozInlineSpellWordUtil & aWordUtil,Selection * aSpellCheckSelection)1220 nsresult mozInlineSpellChecker::DoSpellCheckSelection(
1221     mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
1222   nsresult rv;
1223 
1224   // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1225   mNumWordsInSpellSelection = 0;
1226 
1227   // Since we could be modifying the ranges for the spellCheckSelection while
1228   // looping on the spell check selection, keep a separate array of range
1229   // elements inside the selection
1230   nsTArray<RefPtr<nsRange>> ranges;
1231 
1232   int32_t count = aSpellCheckSelection->RangeCount();
1233 
1234   for (int32_t idx = 0; idx < count; idx++) {
1235     nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
1236     if (range) {
1237       ranges.AppendElement(range);
1238     }
1239   }
1240 
1241   // We have saved the ranges above. Clearing the spellcheck selection here
1242   // isn't necessary (rechecking each word will modify it as necessary) but
1243   // provides better performance. By ensuring that no ranges need to be
1244   // removed in DoSpellCheck, we can save checking range inclusion which is
1245   // slow.
1246   aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
1247 
1248   // We use this state object for all calls, and just update its range. Note
1249   // that we don't need to call FinishInit since we will be filling in the
1250   // necessary information.
1251   UniquePtr<mozInlineSpellStatus> status =
1252       mozInlineSpellStatus::CreateForRange(*this, nullptr);
1253 
1254   bool doneChecking;
1255   for (int32_t idx = 0; idx < count; idx++) {
1256     // We can consider this word as "added" since we know it has no spell
1257     // check range over it that needs to be deleted. All the old ranges
1258     // were cleared above. We also need to clear the word count so that we
1259     // check all words instead of stopping early.
1260     status->mRange = ranges[idx];
1261     rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
1262     NS_ENSURE_SUCCESS(rv, rv);
1263     MOZ_ASSERT(
1264         doneChecking,
1265         "We gave the spellchecker one word, but it didn't finish checking?!?!");
1266   }
1267 
1268   return NS_OK;
1269 }
1270 
1271 class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
1272  public:
1273   /**
1274    * @param aStatus must be non-nullptr.
1275    */
SpellCheckerSlice(mozInlineSpellChecker & aInlineSpellChecker,mozInlineSpellWordUtil & aWordUtil,mozilla::dom::Selection & aSpellCheckSelection,const mozilla::UniquePtr<mozInlineSpellStatus> & aStatus,bool & aDoneChecking)1276   SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
1277                     mozInlineSpellWordUtil& aWordUtil,
1278                     mozilla::dom::Selection& aSpellCheckSelection,
1279                     const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
1280                     bool& aDoneChecking)
1281       : mInlineSpellChecker{aInlineSpellChecker},
1282         mWordUtil{aWordUtil},
1283         mSpellCheckSelection{aSpellCheckSelection},
1284         mStatus{aStatus},
1285         mDoneChecking{aDoneChecking} {
1286     MOZ_ASSERT(aStatus);
1287   }
1288 
1289   [[nodiscard]] nsresult Execute();
1290 
1291  private:
1292   // Creates an async request to check the words and update the ranges for the
1293   // misspellings.
1294   //
1295   // @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
1296   // @param aOldRangesForSomeWords ranges from previous spellcheckings which
1297   //                               might need to be removed. Its length might
1298   //                               differ from `aWords.Length()`.
1299   // @param aNodeOffsetRangesForWords One range for each word in aWords. So
1300   //                                  `aNodeOffsetRangesForWords.Length() ==
1301   //                                  aWords.Length()`.
1302   void CheckWordsAndUpdateRangesForMisspellings(
1303       const nsTArray<nsString>& aWords,
1304       nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1305       nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);
1306 
1307   void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);
1308 
1309   bool ShouldSpellCheckRange(const nsRange& aRange) const;
1310 
1311   bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;
1312 
1313   mozInlineSpellChecker& mInlineSpellChecker;
1314   mozInlineSpellWordUtil& mWordUtil;
1315   mozilla::dom::Selection& mSpellCheckSelection;
1316   const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
1317   bool& mDoneChecking;
1318 };
1319 
ShouldSpellCheckRange(const nsRange & aRange) const1320 bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
1321     const nsRange& aRange) const {
1322   if (aRange.Collapsed()) {
1323     return false;
1324   }
1325 
1326   nsINode* beginNode = aRange.GetStartContainer();
1327   nsINode* endNode = aRange.GetEndContainer();
1328 
1329   const nsINode* rootNode = mWordUtil.GetRootNode();
1330   return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
1331          beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
1332          endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
1333 }
1334 
IsInNoCheckRange(const nsINode & aNode,int32_t aOffset) const1335 bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
1336     const nsINode& aNode, int32_t aOffset) const {
1337   ErrorResult erv;
1338   return mStatus->GetNoCheckRange() &&
1339          mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
1340 }
1341 
RemoveRanges(const nsTArray<RefPtr<nsRange>> & aRanges)1342 void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
1343     const nsTArray<RefPtr<nsRange>>& aRanges) {
1344   for (uint32_t i = 0; i < aRanges.Length(); i++) {
1345     mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
1346   }
1347 }
1348 
1349 // mozInlineSpellChecker::SpellCheckerSlice::Execute
1350 //
1351 //    This function checks words intersecting the given range, excluding those
1352 //    inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
1353 //    will have any spell selection removed (this is used to hide the
1354 //    underlining for the word that the caret is in). aNoCheckRange should be
1355 //    on word boundaries.
1356 //
1357 //    mResume->mCreatedRange is a possibly nullptr range of new text that was
1358 //    inserted.  Inside this range, we don't bother to check whether things are
1359 //    inside the spellcheck selection, which speeds up large paste operations
1360 //    considerably.
1361 //
1362 //    Normal case when editing text by typing
1363 //       h e l l o   w o r k d   h o w   a r e   y o u
1364 //                            ^ caret
1365 //                   [-------] mRange
1366 //                   [-------] mNoCheckRange
1367 //      -> does nothing (range is the same as the no check range)
1368 //
1369 //    Case when pasting:
1370 //             [---------- pasted text ----------]
1371 //       h e l l o   w o r k d   h o w   a r e   y o u
1372 //                                                ^ caret
1373 //                                               [---] aNoCheckRange
1374 //      -> recheck all words in range except those in aNoCheckRange
1375 //
1376 //    If checking is complete, *aDoneChecking will be set. If there is more
1377 //    but we ran out of time, this will be false and the range will be
1378 //    updated with the stuff that still needs checking.
1379 
Execute()1380 nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
1381   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1382 
1383   mDoneChecking = true;
1384 
1385   if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
1386     return NS_ERROR_NOT_INITIALIZED;
1387   }
1388 
1389   if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
1390     return NS_OK;
1391   }
1392 
1393   // get the editor for ShouldSpellCheckNode, this may fail in reasonable
1394   // circumstances since the editor could have gone away
1395   RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
1396   if (!editorBase || editorBase->Destroyed()) {
1397     return NS_ERROR_FAILURE;
1398   }
1399 
1400   if (!ShouldSpellCheckRange(*mStatus->mRange)) {
1401     // Just bail out and don't try to spell-check this
1402     return NS_OK;
1403   }
1404 
1405   // see if the selection has any ranges, if not, then we can optimize checking
1406   // range inclusion later (we have no ranges when we are initially checking or
1407   // when there are no misspelled words yet).
1408   const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();
1409 
1410   // set the starting DOM position to be the beginning of our range
1411   if (nsresult rv = mWordUtil.SetPositionAndEnd(
1412           mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
1413           mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
1414       NS_FAILED(rv)) {
1415     // Just bail out and don't try to spell-check this
1416     return NS_OK;
1417   }
1418 
1419   // aWordUtil.SetPosition flushes pending notifications, check editor again.
1420   if (!mInlineSpellChecker.mEditorBase) {
1421     return NS_ERROR_FAILURE;
1422   }
1423 
1424   int32_t wordsChecked = 0;
1425   PRTime beginTime = PR_Now();
1426 
1427   nsTArray<nsString> normalizedWords;
1428   nsTArray<RefPtr<nsRange>> oldRangesToRemove;
1429   nsTArray<NodeOffsetRange> checkRanges;
1430   mozInlineSpellWordUtil::Word word;
1431   static const size_t requestChunkSize =
1432       INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
1433 
1434   while (mWordUtil.GetNextWord(word)) {
1435     // get the range for the current word.
1436     nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
1437     nsINode* const endNode = word.mNodeOffsetRange.End().Node();
1438     const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
1439     const int32_t endOffset = word.mNodeOffsetRange.End().Offset();
1440 
1441     // see if we've done enough words in this round and run out of time.
1442     if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
1443         PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
1444       // stop checking, our time limit has been exceeded.
1445       MOZ_LOG(
1446           sInlineSpellCheckerLog, LogLevel::Verbose,
1447           ("%s: we have run out of time, schedule next round.", __FUNCTION__));
1448 
1449       CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1450                                                std::move(oldRangesToRemove),
1451                                                std::move(checkRanges));
1452 
1453       // move the range to encompass the stuff that needs checking.
1454       nsresult rv = mStatus->mRange->SetStart(beginNode, beginOffset);
1455       if (NS_FAILED(rv)) {
1456         // The range might be unhappy because the beginning is after the
1457         // end. This is possible when the requested end was in the middle
1458         // of a word, just ignore this situation and assume we're done.
1459         return NS_OK;
1460       }
1461       mDoneChecking = false;
1462       return NS_OK;
1463     }
1464 
1465     MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1466             ("%s: got word \"%s\"%s", __FUNCTION__,
1467              NS_ConvertUTF16toUTF8(word.mText).get(),
1468              word.mSkipChecking ? " (not checking)" : ""));
1469 
1470     // see if there is a spellcheck range that already intersects the word
1471     // and remove it. We only need to remove old ranges, so don't bother if
1472     // there were no ranges when we started out.
1473     if (originalRangeCount > 0) {
1474       ErrorResult erv;
1475       // likewise, if this word is inside new text, we won't bother testing
1476       if (!mStatus->GetCreatedRange() ||
1477           !mStatus->GetCreatedRange()->IsPointInRange(*beginNode, beginOffset,
1478                                                       erv)) {
1479         MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1480                 ("%s: removing ranges for some interval.", __FUNCTION__));
1481 
1482         nsTArray<RefPtr<nsRange>> ranges;
1483         mSpellCheckSelection.GetRangesForInterval(
1484             *beginNode, beginOffset, *endNode, endOffset, true, ranges, erv);
1485         ENSURE_SUCCESS(erv, erv.StealNSResult());
1486         oldRangesToRemove.AppendElements(std::move(ranges));
1487       }
1488     }
1489 
1490     // some words are special and don't need checking
1491     if (word.mSkipChecking) {
1492       continue;
1493     }
1494 
1495     // some nodes we don't spellcheck
1496     if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
1497       continue;
1498     }
1499 
1500     // Don't check spelling if we're inside the noCheckRange. This needs to
1501     // be done after we clear any old selection because the excluded word
1502     // might have been previously marked.
1503     //
1504     // We do a simple check to see if the beginning of our word is in the
1505     // exclusion range. Because the exclusion range is a multiple of a word,
1506     // this is sufficient.
1507     if (IsInNoCheckRange(*beginNode, beginOffset)) {
1508       continue;
1509     }
1510 
1511     // check spelling and add to selection if misspelled
1512     mozInlineSpellWordUtil::NormalizeWord(word.mText);
1513     normalizedWords.AppendElement(word.mText);
1514     checkRanges.AppendElement(word.mNodeOffsetRange);
1515     wordsChecked++;
1516     if (normalizedWords.Length() >= requestChunkSize) {
1517       CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1518                                                std::move(oldRangesToRemove),
1519                                                std::move(checkRanges));
1520       normalizedWords.Clear();
1521       oldRangesToRemove = {};
1522       // Set new empty data for spellcheck range in DOM to avoid
1523       // clang-tidy detection.
1524       checkRanges = nsTArray<NodeOffsetRange>();
1525     }
1526   }
1527 
1528   CheckWordsAndUpdateRangesForMisspellings(
1529       normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));
1530 
1531   return NS_OK;
1532 }
1533 
DoSpellCheck(mozInlineSpellWordUtil & aWordUtil,Selection * aSpellCheckSelection,const UniquePtr<mozInlineSpellStatus> & aStatus,bool * aDoneChecking)1534 nsresult mozInlineSpellChecker::DoSpellCheck(
1535     mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
1536     const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
1537   MOZ_ASSERT(aDoneChecking);
1538 
1539   SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
1540                                       aStatus, *aDoneChecking};
1541 
1542   return spellCheckerSlice.Execute();
1543 }
1544 
1545 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
1546 class MOZ_RAII AutoChangeNumPendingSpellChecks final {
1547  public:
AutoChangeNumPendingSpellChecks(mozInlineSpellChecker * aSpellChecker,int32_t aDelta)1548   explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
1549                                            int32_t aDelta)
1550       : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
1551 
~AutoChangeNumPendingSpellChecks()1552   ~AutoChangeNumPendingSpellChecks() {
1553     mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
1554   }
1555 
1556  private:
1557   RefPtr<mozInlineSpellChecker> mSpellChecker;
1558   int32_t mDelta;
1559 };
1560 
1561 void mozInlineSpellChecker::SpellCheckerSlice::
CheckWordsAndUpdateRangesForMisspellings(const nsTArray<nsString> & aWords,nsTArray<RefPtr<nsRange>> && aOldRangesForSomeWords,nsTArray<NodeOffsetRange> && aNodeOffsetRangesForWords)1562     CheckWordsAndUpdateRangesForMisspellings(
1563         const nsTArray<nsString>& aWords,
1564         nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1565         nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
1566   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1567           ("%s: aWords.Length()=%i", __FUNCTION__,
1568            static_cast<int>(aWords.Length())));
1569 
1570   MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());
1571 
1572   // TODO:
1573   // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
1574   // to remove ranges more efficiently.
1575 
1576   if (aWords.IsEmpty()) {
1577     RemoveRanges(aOldRangesForSomeWords);
1578     return;
1579   }
1580 
1581   mInlineSpellChecker.ChangeNumPendingSpellChecks(1);
1582 
1583   RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
1584   RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
1585   uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
1586   mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
1587       GetMainThreadSerialEventTarget(), __func__,
1588       [inlineSpellChecker, spellCheckerSelection,
1589        nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
1590        oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
1591        token](const nsTArray<bool>& aIsMisspelled) {
1592         if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1593           // This result is never used
1594           return;
1595         }
1596 
1597         if (!inlineSpellChecker->mEditorBase ||
1598             inlineSpellChecker->mEditorBase->Destroyed()) {
1599           return;
1600         }
1601 
1602         AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);
1603 
1604         if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
1605           return;
1606         }
1607 
1608         inlineSpellChecker->UpdateRangesForMisspelledWords(
1609             nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
1610             *spellCheckerSelection);
1611       },
1612       [inlineSpellChecker, token](nsresult aRv) {
1613         if (!inlineSpellChecker->mEditorBase ||
1614             inlineSpellChecker->mEditorBase->Destroyed()) {
1615           return;
1616         }
1617 
1618         if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1619           // This result is never used
1620           return;
1621         }
1622 
1623         inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
1624       });
1625 }
1626 
1627 // mozInlineSpellChecker::ResumeCheck
1628 //
1629 //    Called by the resume event when it fires. We will try to pick up where
1630 //    the last resume left off.
1631 
ResumeCheck(UniquePtr<mozInlineSpellStatus> && aStatus)1632 nsresult mozInlineSpellChecker::ResumeCheck(
1633     UniquePtr<mozInlineSpellStatus>&& aStatus) {
1634   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1635 
1636   // Observers should be notified that spell check has ended only after spell
1637   // check is done below, but since there are many early returns in this method
1638   // and the number of pending spell checks must be decremented regardless of
1639   // whether the spell check actually happens, use this RAII object.
1640   AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
1641 
1642   if (aStatus->IsFullSpellCheck()) {
1643     // Allow posting new spellcheck resume events from inside
1644     // ResumeCheck, now that we're actually firing.
1645     MOZ_ASSERT(mFullSpellCheckScheduled,
1646                "How could this be false?  The full spell check is "
1647                "calling us!!");
1648     mFullSpellCheckScheduled = false;
1649   }
1650 
1651   if (!mSpellCheck) return NS_OK;  // spell checking has been turned off
1652 
1653   if (!mEditorBase) {
1654     return NS_OK;
1655   }
1656 
1657   Maybe<mozInlineSpellWordUtil> wordUtil{
1658       mozInlineSpellWordUtil::Create(*mEditorBase)};
1659   if (!wordUtil) {
1660     return NS_OK;  // editor doesn't like us, don't assert
1661   }
1662 
1663   RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
1664   if (NS_WARN_IF(!spellCheckSelection)) {
1665     return NS_ERROR_FAILURE;
1666   }
1667 
1668   nsAutoCString currentDictionary;
1669   nsresult rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
1670   if (NS_FAILED(rv)) {
1671     MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1672             ("%s: no active dictionary.", __FUNCTION__));
1673 
1674     // no active dictionary
1675     int32_t count = spellCheckSelection->RangeCount();
1676     for (int32_t index = count - 1; index >= 0; index--) {
1677       RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
1678       if (checkRange) {
1679         RemoveRange(spellCheckSelection, checkRange);
1680       }
1681     }
1682     return NS_OK;
1683   }
1684 
1685   CleanupRangesInSelection(spellCheckSelection);
1686 
1687   rv = aStatus->FinishInitOnEvent(*wordUtil);
1688   NS_ENSURE_SUCCESS(rv, rv);
1689   if (!aStatus->mRange) return NS_OK;  // empty range, nothing to do
1690 
1691   bool doneChecking = true;
1692   if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
1693     rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
1694   else
1695     rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
1696   NS_ENSURE_SUCCESS(rv, rv);
1697 
1698   if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
1699   return rv;
1700 }
1701 
1702 // mozInlineSpellChecker::IsPointInSelection
1703 //
1704 //    Determines if a given (node,offset) point is inside the given
1705 //    selection. If so, the specific range of the selection that
1706 //    intersects is places in *aRange. (There may be multiple disjoint
1707 //    ranges in a selection.)
1708 //
1709 //    If there is no intersection, *aRange will be nullptr.
1710 
1711 // static
IsPointInSelection(Selection & aSelection,nsINode * aNode,int32_t aOffset,nsRange ** aRange)1712 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
1713                                                    nsINode* aNode,
1714                                                    int32_t aOffset,
1715                                                    nsRange** aRange) {
1716   *aRange = nullptr;
1717 
1718   nsTArray<nsRange*> ranges;
1719   nsresult rv = aSelection.GetRangesForIntervalArray(aNode, aOffset, aNode,
1720                                                      aOffset, true, &ranges);
1721   NS_ENSURE_SUCCESS(rv, rv);
1722 
1723   if (ranges.Length() == 0) return NS_OK;  // no matches
1724 
1725   // there may be more than one range returned, and we don't know what do
1726   // do with that, so just get the first one
1727   NS_ADDREF(*aRange = ranges[0]);
1728   return NS_OK;
1729 }
1730 
CleanupRangesInSelection(Selection * aSelection)1731 nsresult mozInlineSpellChecker::CleanupRangesInSelection(
1732     Selection* aSelection) {
1733   // integrity check - remove ranges that have collapsed to nothing. This
1734   // can happen if the node containing a highlighted word was removed.
1735   if (!aSelection) return NS_ERROR_FAILURE;
1736 
1737   int32_t count = aSelection->RangeCount();
1738 
1739   for (int32_t index = 0; index < count; index++) {
1740     nsRange* checkRange = aSelection->GetRangeAt(index);
1741     if (checkRange) {
1742       if (checkRange->Collapsed()) {
1743         RemoveRange(aSelection, checkRange);
1744         index--;
1745         count--;
1746       }
1747     }
1748   }
1749 
1750   return NS_OK;
1751 }
1752 
1753 // mozInlineSpellChecker::RemoveRange
1754 //
1755 //    For performance reasons, we have an upper bound on the number of word
1756 //    ranges  in the spell check selection. When removing a range from the
1757 //    selection, we need to decrement mNumWordsInSpellSelection
1758 
RemoveRange(Selection * aSpellCheckSelection,nsRange * aRange)1759 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
1760                                             nsRange* aRange) {
1761   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1762 
1763   NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1764   NS_ENSURE_ARG_POINTER(aRange);
1765 
1766   ErrorResult rv;
1767   RefPtr<nsRange> range{aRange};
1768   RefPtr<Selection> selection{aSpellCheckSelection};
1769   selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
1770   if (!rv.Failed() && mNumWordsInSpellSelection) mNumWordsInSpellSelection--;
1771 
1772   return rv.StealNSResult();
1773 }
1774 
1775 struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange {
EqualsmozInlineSpellChecker::CompareRangeAndNodeOffsetRange1776   static bool Equals(const RefPtr<nsRange>& aRange,
1777                      const NodeOffsetRange& aNodeOffsetRange) {
1778     return aNodeOffsetRange == *aRange;
1779   }
1780 };
1781 
UpdateRangesForMisspelledWords(const nsTArray<NodeOffsetRange> & aNodeOffsetRangesForWords,const nsTArray<RefPtr<nsRange>> & aOldRangesForSomeWords,const nsTArray<bool> & aIsMisspelled,Selection & aSpellCheckerSelection)1782 void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
1783     const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords,
1784     const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords,
1785     const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) {
1786   MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
1787 
1788   MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length());
1789 
1790   // When the spellchecker checks text containing words separated by "/", it may
1791   // happen that some words checked in one timeslice, are checked again in a
1792   // following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo"
1793   // and "baz" are checked in one timeslice and two ranges are added for them.
1794   // In the following timeslice "foo" and "baz" are checked again but since
1795   // their corresponding ranges are already in the spellcheck-Selection
1796   // they don't have to be added again and since "foo" and "baz" still contain
1797   // spelling mistakes, they don't have to be removed.
1798   //
1799   // In this case, it's more efficient to keep the existing ranges.
1800 
1801   AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1802       oldRangesMarkedForRemoval;
1803   for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) {
1804     oldRangesMarkedForRemoval.AppendElement(true);
1805   }
1806 
1807   AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1808       nodeOffsetRangesMarkedForAdding;
1809   for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) {
1810     nodeOffsetRangesMarkedForAdding.AppendElement(false);
1811   }
1812 
1813   for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
1814     if (!aIsMisspelled[i]) {
1815       continue;
1816     }
1817 
1818     const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i];
1819     const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
1820         nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
1821     if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
1822         aOldRangesForSomeWords[indexOfOldRangeToKeep]->GetSelection() ==
1823         &aSpellCheckerSelection /** TODO: warn in case the old range doesn't
1824                                   belong to the selection. This is not critical,
1825                                   because other code can always remove them
1826                                   before the actual spellchecking happens. */) {
1827       MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1828               ("%s: reusing old range.", __FUNCTION__));
1829 
1830       oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false;
1831     } else {
1832       nodeOffsetRangesMarkedForAdding[i] = true;
1833     }
1834   }
1835 
1836   for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) {
1837     if (oldRangesMarkedForRemoval[i]) {
1838       RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]);
1839     }
1840   }
1841 
1842   // Add ranges after removing the marked old ones, so that the Selection can
1843   // become full again.
1844   for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) {
1845     if (nodeOffsetRangesMarkedForAdding[i]) {
1846       RefPtr<nsRange> wordRange =
1847           mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]);
1848       // If we somehow can't make a range for this word, just ignore
1849       // it.
1850       if (wordRange) {
1851         AddRange(&aSpellCheckerSelection, wordRange);
1852       }
1853     }
1854   }
1855 }
1856 
1857 // mozInlineSpellChecker::AddRange
1858 //
1859 //    For performance reasons, we have an upper bound on the number of word
1860 //    ranges we'll add to the spell check selection. Once we reach that upper
1861 //    bound, stop adding the ranges
1862 
AddRange(Selection * aSpellCheckSelection,nsRange * aRange)1863 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
1864                                          nsRange* aRange) {
1865   NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1866   NS_ENSURE_ARG_POINTER(aRange);
1867 
1868   nsresult rv = NS_OK;
1869 
1870   if (!IsSpellCheckSelectionFull()) {
1871     IgnoredErrorResult err;
1872     aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
1873                                                                     err);
1874     if (err.Failed()) {
1875       rv = err.StealNSResult();
1876     } else {
1877       mNumWordsInSpellSelection++;
1878     }
1879   }
1880 
1881   return rv;
1882 }
1883 
GetSpellCheckSelection()1884 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
1885   if (NS_WARN_IF(!mEditorBase)) {
1886     return nullptr;
1887   }
1888   RefPtr<Selection> selection =
1889       mEditorBase->GetSelection(SelectionType::eSpellCheck);
1890   if (!selection) {
1891     return nullptr;
1892   }
1893   return selection.forget();
1894 }
1895 
SaveCurrentSelectionPosition()1896 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
1897   if (NS_WARN_IF(!mEditorBase)) {
1898     return NS_OK;  // XXX Why NS_OK?
1899   }
1900 
1901   // figure out the old caret position based on the current selection
1902   RefPtr<Selection> selection = mEditorBase->GetSelection();
1903   if (NS_WARN_IF(!selection)) {
1904     return NS_ERROR_FAILURE;
1905   }
1906 
1907   mCurrentSelectionAnchorNode = selection->GetFocusNode();
1908   mCurrentSelectionOffset = selection->FocusOffset();
1909 
1910   return NS_OK;
1911 }
1912 
1913 // mozInlineSpellChecker::HandleNavigationEvent
1914 //
1915 //    Acts upon mouse clicks and keyboard navigation changes, spell checking
1916 //    the previous word if the new navigation location moves us to another
1917 //    word.
1918 //
1919 //    This is complicated by the fact that our mouse events are happening after
1920 //    selection has been changed to account for the mouse click. But keyboard
1921 //    events are happening before the caret selection has changed. Working
1922 //    around this by letting keyboard events setting forceWordSpellCheck to
1923 //    true. aNewPositionOffset also tries to work around this for the
1924 //    DOM_VK_RIGHT and DOM_VK_LEFT cases.
1925 
HandleNavigationEvent(bool aForceWordSpellCheck,int32_t aNewPositionOffset)1926 nsresult mozInlineSpellChecker::HandleNavigationEvent(
1927     bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
1928   nsresult rv;
1929 
1930   // If we already handled the navigation event and there is no possibility
1931   // anything has changed since then, we don't have to do anything. This
1932   // optimization makes a noticeable difference when you hold down a navigation
1933   // key like Page Down.
1934   if (!mNeedsCheckAfterNavigation) return NS_OK;
1935 
1936   nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
1937   uint32_t currentAnchorOffset = mCurrentSelectionOffset;
1938 
1939   // now remember the new focus position resulting from the event
1940   rv = SaveCurrentSelectionPosition();
1941   NS_ENSURE_SUCCESS(rv, rv);
1942 
1943   bool shouldPost;
1944   Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
1945       mozInlineSpellStatus::CreateForNavigation(
1946           *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
1947           currentAnchorOffset, mCurrentSelectionAnchorNode,
1948           mCurrentSelectionOffset, &shouldPost);
1949 
1950   if (NS_WARN_IF(res.isErr())) {
1951     return res.unwrapErr();
1952   }
1953 
1954   if (shouldPost) {
1955     rv = ScheduleSpellCheck(res.unwrap());
1956     NS_ENSURE_SUCCESS(rv, rv);
1957   }
1958 
1959   return NS_OK;
1960 }
1961 
1962 NS_IMETHODIMP
HandleEvent(Event * aEvent)1963 mozInlineSpellChecker::HandleEvent(Event* aEvent) {
1964   nsAutoString eventType;
1965   aEvent->GetType(eventType);
1966 
1967   if (eventType.EqualsLiteral("blur")) {
1968     return OnBlur(aEvent);
1969   }
1970   if (eventType.EqualsLiteral("click")) {
1971     return OnMouseClick(aEvent);
1972   }
1973   if (eventType.EqualsLiteral("keypress")) {
1974     return OnKeyPress(aEvent);
1975   }
1976 
1977   return NS_OK;
1978 }
1979 
OnBlur(Event * aEvent)1980 nsresult mozInlineSpellChecker::OnBlur(Event* aEvent) {
1981   // force spellcheck on blur, for instance when tabbing out of a textbox
1982   HandleNavigationEvent(true);
1983   return NS_OK;
1984 }
1985 
OnMouseClick(Event * aMouseEvent)1986 nsresult mozInlineSpellChecker::OnMouseClick(Event* aMouseEvent) {
1987   MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1988   NS_ENSURE_TRUE(mouseEvent, NS_OK);
1989 
1990   // ignore any errors from HandleNavigationEvent as we don't want to prevent
1991   // anyone else from seeing this event.
1992   HandleNavigationEvent(mouseEvent->Button() != 0);
1993   return NS_OK;
1994 }
1995 
OnKeyPress(Event * aKeyEvent)1996 nsresult mozInlineSpellChecker::OnKeyPress(Event* aKeyEvent) {
1997   RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
1998   NS_ENSURE_TRUE(keyEvent, NS_OK);
1999 
2000   uint32_t keyCode = keyEvent->KeyCode();
2001 
2002   // we only care about navigation keys that moved selection
2003   switch (keyCode) {
2004     case KeyboardEvent_Binding::DOM_VK_RIGHT:
2005     case KeyboardEvent_Binding::DOM_VK_LEFT:
2006       HandleNavigationEvent(
2007           false, keyCode == KeyboardEvent_Binding::DOM_VK_RIGHT ? 1 : -1);
2008       break;
2009     case KeyboardEvent_Binding::DOM_VK_UP:
2010     case KeyboardEvent_Binding::DOM_VK_DOWN:
2011     case KeyboardEvent_Binding::DOM_VK_HOME:
2012     case KeyboardEvent_Binding::DOM_VK_END:
2013     case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
2014     case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
2015       HandleNavigationEvent(true /* force a spelling correction */);
2016       break;
2017   }
2018 
2019   return NS_OK;
2020 }
2021 
2022 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
2023 class UpdateCurrentDictionaryCallback final
2024     : public nsIEditorSpellCheckCallback {
2025  public:
2026   NS_DECL_ISUPPORTS
2027 
UpdateCurrentDictionaryCallback(mozInlineSpellChecker * aSpellChecker,uint32_t aDisabledAsyncToken)2028   explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
2029                                            uint32_t aDisabledAsyncToken)
2030       : mSpellChecker(aSpellChecker),
2031         mDisabledAsyncToken(aDisabledAsyncToken) {}
2032 
EditorSpellCheckDone()2033   NS_IMETHOD EditorSpellCheckDone() override {
2034     // Ignore this callback if SetEnableRealTimeSpell(false) was called after
2035     // the UpdateCurrentDictionary call that triggered it.
2036     return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
2037                ? NS_OK
2038                : mSpellChecker->CurrentDictionaryUpdated();
2039   }
2040 
2041  private:
~UpdateCurrentDictionaryCallback()2042   ~UpdateCurrentDictionaryCallback() {}
2043 
2044   RefPtr<mozInlineSpellChecker> mSpellChecker;
2045   uint32_t mDisabledAsyncToken;
2046 };
NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback,nsIEditorSpellCheckCallback)2047 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
2048 
2049 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
2050   // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
2051   // checker is being initialized.  Calling UpdateCurrentDictionary on
2052   // mPendingSpellCheck simply queues the dictionary update after the init.
2053   RefPtr<EditorSpellCheck> spellCheck =
2054       mSpellCheck ? mSpellCheck : mPendingSpellCheck;
2055   if (!spellCheck) {
2056     return NS_OK;
2057   }
2058 
2059   RefPtr<UpdateCurrentDictionaryCallback> cb =
2060       new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
2061   NS_ENSURE_STATE(cb);
2062   nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
2063   if (NS_FAILED(rv)) {
2064     cb = nullptr;
2065     return rv;
2066   }
2067   mNumPendingUpdateCurrentDictionary++;
2068   ChangeNumPendingSpellChecks(1);
2069 
2070   return NS_OK;
2071 }
2072 
2073 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
CurrentDictionaryUpdated()2074 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
2075   mNumPendingUpdateCurrentDictionary--;
2076   MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0,
2077              "CurrentDictionaryUpdated called without corresponding "
2078              "UpdateCurrentDictionary call!");
2079   ChangeNumPendingSpellChecks(-1);
2080 
2081   nsresult rv = SpellCheckRange(nullptr);
2082   NS_ENSURE_SUCCESS(rv, rv);
2083 
2084   return NS_OK;
2085 }
2086 
2087 NS_IMETHODIMP
GetSpellCheckPending(bool * aPending)2088 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
2089   *aPending = mNumPendingSpellChecks > 0;
2090   return NS_OK;
2091 }
2092