Bug 1700051: part 19) `const`-qualify and rename `mozInlineSpellChecker::SpellCheckSe...
[gecko.git] / extensions / spellcheck / src / mozInlineSpellChecker.cpp
blob0ff263fe8982157cac2e2375ebe5d26c25e9bb4b
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/. */
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.
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.
17 * Using the WordUtil class to find words causes DOM notifications to be
18 * flushed because it asks for style information. As a result, we post an event
19 * and do all of the spellchecking in that event handler, which occurs later.
20 * We store all DOM pointers in ranges because they are kept up-to-date with
21 * DOM changes that may have happened while the event was on the queue.
23 * We also allow the spellcheck to be suspended and resumed later. This makes
24 * large pastes or initializations with a lot of text not hang the browser UI.
26 * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
27 * true when we get any change, and false once there is no possibility
28 * something changed that we need to check on navigation. Navigation events
29 * tend to be a little tricky because we want to check the current word on
30 * exit if something has changed. If we navigate inside the word, we don't want
31 * to do anything. As a result, this flag is cleared in FinishNavigationEvent
32 * when we know that we are checking as a result of navigation.
35 #include "mozInlineSpellChecker.h"
37 #include "mozilla/EditAction.h"
38 #include "mozilla/EditorSpellCheck.h"
39 #include "mozilla/EditorUtils.h"
40 #include "mozilla/Logging.h"
41 #include "mozilla/RangeUtils.h"
42 #include "mozilla/Services.h"
43 #include "mozilla/TextEditor.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"
68 using mozilla::LogLevel;
69 using namespace mozilla;
70 using namespace mozilla::dom;
71 using namespace mozilla::ipc;
73 // the number of milliseconds that we will take at once to do spellchecking
74 #define INLINESPELL_CHECK_TIMEOUT 1
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
82 // The maximum number of words to check word via IPC.
83 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
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"
90 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
92 static const char kMaxSpellCheckSelectionSize[] =
93 "extensions.spellcheck.inline.max-misspellings";
94 static const PRTime kMaxSpellCheckTimeInUsec =
95 INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
97 mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
98 : mSpellChecker(aSpellChecker) {}
100 // mozInlineSpellStatus::CreateForEditorChange
102 // This is the most complicated case. For changes, we need to compute the
103 // range of stuff that changed based on the old and new caret positions,
104 // as well as use a range possibly provided by the editor (start and end,
105 // which are usually nullptr) to get a range with the union of these.
107 // static
108 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
109 mozInlineSpellStatus::CreateForEditorChange(
110 mozInlineSpellChecker& aSpellChecker, EditSubAction aEditSubAction,
111 nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
112 uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
113 nsINode* aEndNode, uint32_t aEndOffset) {
114 if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
115 return Err(NS_ERROR_FAILURE);
118 UniquePtr<mozInlineSpellStatus> status{
119 /* The constructor is `private`, hence the explicit allocation. */
120 new mozInlineSpellStatus{&aSpellChecker}};
122 // save the anchor point as a range so we can find the current word later
123 status->mAnchorRange =
124 status->PositionToCollapsedRange(aAnchorNode, aAnchorOffset);
125 if (NS_WARN_IF(!status->mAnchorRange)) {
126 return Err(NS_ERROR_FAILURE);
129 bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
130 if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
131 // IME may remove the previous node if it cancels composition when
132 // there is no text around the composition.
133 deleted = !aPreviousNode->IsInComposedDoc();
136 if (deleted) {
137 // Deletes are easy, the range is just the current anchor. We set the range
138 // to check to be empty, FinishInitOnEvent will fill in the range to be
139 // the current word.
140 status->mOp = eOpChangeDelete;
141 status->mRange = nullptr;
142 return status;
145 status->mOp = eOpChange;
147 // range to check
148 status->mRange = nsRange::Create(aPreviousNode);
150 // ...we need to put the start and end in the correct order
151 ErrorResult errorResult;
152 int16_t cmpResult = status->mAnchorRange->ComparePoint(
153 *aPreviousNode, aPreviousOffset, errorResult);
154 if (NS_WARN_IF(errorResult.Failed())) {
155 return Err(errorResult.StealNSResult());
157 nsresult rv;
158 if (cmpResult < 0) {
159 // previous anchor node is before the current anchor
160 rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
161 aAnchorNode, aAnchorOffset);
162 if (NS_WARN_IF(NS_FAILED(rv))) {
163 return Err(rv);
165 } else {
166 // previous anchor node is after (or the same as) the current anchor
167 rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
168 aPreviousNode, aPreviousOffset);
169 if (NS_WARN_IF(NS_FAILED(rv))) {
170 return Err(rv);
174 // On insert save this range: DoSpellCheck optimizes things in this range.
175 // Otherwise, just leave this nullptr.
176 if (aEditSubAction == EditSubAction::eInsertText) {
177 status->mCreatedRange = status->mRange;
180 // if we were given a range, we need to expand our range to encompass it
181 if (aStartNode && aEndNode) {
182 cmpResult =
183 status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
184 if (NS_WARN_IF(errorResult.Failed())) {
185 return Err(errorResult.StealNSResult());
187 if (cmpResult < 0) { // given range starts before
188 rv = status->mRange->SetStart(aStartNode, aStartOffset);
189 if (NS_WARN_IF(NS_FAILED(rv))) {
190 return Err(rv);
194 cmpResult =
195 status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
196 if (NS_WARN_IF(errorResult.Failed())) {
197 return Err(errorResult.StealNSResult());
199 if (cmpResult > 0) { // given range ends after
200 rv = status->mRange->SetEnd(aEndNode, aEndOffset);
201 if (NS_WARN_IF(NS_FAILED(rv))) {
202 return Err(rv);
207 return status;
210 // mozInlineSpellStatus::CreateForNavigation
212 // For navigation events, we just need to store the new and old positions.
214 // In some cases, we detect that we shouldn't check. If this event should
215 // not be processed, *aContinue will be false.
217 // static
218 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
219 mozInlineSpellStatus::CreateForNavigation(
220 mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
221 int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
222 uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
223 uint32_t aNewAnchorOffset, bool* aContinue) {
224 UniquePtr<mozInlineSpellStatus> status{
225 /* The constructor is `private`, hence the explicit allocation. */
226 new mozInlineSpellStatus{&aSpellChecker}};
228 status->mOp = eOpNavigation;
230 status->mForceNavigationWordCheck = aForceCheck;
231 status->mNewNavigationPositionOffset = aNewPositionOffset;
233 // get the root node for checking
234 TextEditor* textEditor = status->mSpellChecker->mTextEditor;
235 if (NS_WARN_IF(!textEditor)) {
236 return Err(NS_ERROR_FAILURE);
238 Element* root = textEditor->GetRoot();
239 if (NS_WARN_IF(!root)) {
240 return Err(NS_ERROR_FAILURE);
242 // the anchor node might not be in the DOM anymore, check
243 if (root && aOldAnchorNode &&
244 !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
245 *aContinue = false;
246 return status;
249 status->mOldNavigationAnchorRange =
250 status->PositionToCollapsedRange(aOldAnchorNode, aOldAnchorOffset);
251 if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
252 return Err(NS_ERROR_FAILURE);
254 status->mAnchorRange =
255 status->PositionToCollapsedRange(aNewAnchorNode, aNewAnchorOffset);
256 if (NS_WARN_IF(!status->mAnchorRange)) {
257 return Err(NS_ERROR_FAILURE);
260 *aContinue = true;
261 return status;
264 // mozInlineSpellStatus::CreateForSelection
266 // It is easy for selections since we always re-check the spellcheck
267 // selection.
269 // static
270 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
271 mozInlineSpellChecker& aSpellChecker) {
272 UniquePtr<mozInlineSpellStatus> status{
273 /* The constructor is `private`, hence the explicit allocation. */
274 new mozInlineSpellStatus{&aSpellChecker}};
275 status->mOp = eOpSelection;
276 return status;
279 // mozInlineSpellStatus::CreateForRange
281 // Called to cause the spellcheck of the given range. This will look like
282 // a change operation over the given range.
284 // static
285 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
286 mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
287 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
288 ("%s: range=%p", __FUNCTION__, aRange));
290 UniquePtr<mozInlineSpellStatus> status{
291 /* The constructor is `private`, hence the explicit allocation. */
292 new mozInlineSpellStatus{&aSpellChecker}};
294 status->mOp = eOpChange;
295 status->mRange = aRange;
296 return status;
299 // mozInlineSpellStatus::FinishInitOnEvent
301 // Called when the event is triggered to complete initialization that
302 // might require the WordUtil. This calls to the operation-specific
303 // initializer, and also sets the range to be the entire element if it
304 // is nullptr.
306 // Watch out: the range might still be nullptr if there is nothing to do,
307 // the caller will have to check for this.
309 nsresult mozInlineSpellStatus::FinishInitOnEvent(
310 mozInlineSpellWordUtil& aWordUtil) {
311 nsresult rv;
312 if (!mRange) {
313 rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
314 getter_AddRefs(mRange));
315 NS_ENSURE_SUCCESS(rv, rv);
318 switch (mOp) {
319 case eOpChange:
320 if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
321 break;
322 case eOpChangeDelete:
323 if (mAnchorRange) {
324 rv = FillNoCheckRangeFromAnchor(aWordUtil);
325 NS_ENSURE_SUCCESS(rv, rv);
327 // Delete events will have no range for the changed text (because it was
328 // deleted), and CreateForEditorChange will set it to nullptr. Here, we
329 // select the entire word to cause any underlining to be removed.
330 mRange = mNoCheckRange;
331 break;
332 case eOpNavigation:
333 return FinishNavigationEvent(aWordUtil);
334 case eOpSelection:
335 // this gets special handling in ResumeCheck
336 break;
337 case eOpResume:
338 // everything should be initialized already in this case
339 break;
340 default:
341 MOZ_ASSERT_UNREACHABLE("Bad operation");
342 return NS_ERROR_NOT_INITIALIZED;
344 return NS_OK;
347 // mozInlineSpellStatus::FinishNavigationEvent
349 // This verifies that we need to check the word at the previous caret
350 // position. Now that we have the word util, we can find the word belonging
351 // to the previous caret position. If the new position is inside that word,
352 // we don't want to do anything. In this case, we'll nullptr out mRange so
353 // that the caller will know not to continue.
355 // Notice that we don't set mNoCheckRange. We check here whether the cursor
356 // is in the word that needs checking, so it isn't necessary. Plus, the
357 // spellchecker isn't guaranteed to only check the given word, and it could
358 // remove the underline from the new word under the cursor.
360 nsresult mozInlineSpellStatus::FinishNavigationEvent(
361 mozInlineSpellWordUtil& aWordUtil) {
362 RefPtr<TextEditor> textEditor = mSpellChecker->mTextEditor;
363 if (!textEditor) {
364 return NS_ERROR_FAILURE; // editor is gone
367 NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
369 if (!mOldNavigationAnchorRange->IsPositioned()) {
370 return NS_ERROR_NOT_INITIALIZED;
373 // get the DOM position of the old caret, the range should be collapsed
374 nsCOMPtr<nsINode> oldAnchorNode =
375 mOldNavigationAnchorRange->GetStartContainer();
376 uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
378 // find the word on the old caret position, this is the one that we MAY need
379 // to check
380 RefPtr<nsRange> oldWord;
381 nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
382 static_cast<int32_t>(oldAnchorOffset),
383 getter_AddRefs(oldWord));
384 NS_ENSURE_SUCCESS(rv, rv);
386 // aWordUtil.GetRangeForWord flushes pending notifications, check editor
387 // again.
388 if (!mSpellChecker->mTextEditor) {
389 return NS_ERROR_FAILURE; // editor is gone
392 // get the DOM position of the new caret, the range should be collapsed
393 nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
394 uint32_t newAnchorOffset = mAnchorRange->StartOffset();
396 // see if the new cursor position is in the word of the old cursor position
397 bool isInRange = false;
398 if (!mForceNavigationWordCheck) {
399 ErrorResult err;
400 isInRange = oldWord->IsPointInRange(
401 *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
402 if (NS_WARN_IF(err.Failed())) {
403 return err.StealNSResult();
407 if (isInRange) {
408 // caller should give up
409 mRange = nullptr;
410 } else {
411 // check the old word
412 mRange = oldWord;
414 // Once we've spellchecked the current word, we don't need to spellcheck
415 // for any more navigation events.
416 mSpellChecker->mNeedsCheckAfterNavigation = false;
418 return NS_OK;
421 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
423 // Given the mAnchorRange object, computes the range of the word it is on
424 // (if any) and fills that range into mNoCheckRange. This is used for
425 // change and navigation events to know which word we should skip spell
426 // checking on
428 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
429 mozInlineSpellWordUtil& aWordUtil) {
430 if (!mAnchorRange->IsPositioned()) {
431 return NS_ERROR_NOT_INITIALIZED;
433 nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
434 uint32_t anchorOffset = mAnchorRange->StartOffset();
435 return aWordUtil.GetRangeForWord(anchorNode,
436 static_cast<int32_t>(anchorOffset),
437 getter_AddRefs(mNoCheckRange));
440 // mozInlineSpellStatus::GetDocument
442 // Returns the Document object for the document for the
443 // current spellchecker.
445 Document* mozInlineSpellStatus::GetDocument() const {
446 if (!mSpellChecker->mTextEditor) {
447 return nullptr;
450 return mSpellChecker->mTextEditor->GetDocument();
453 // mozInlineSpellStatus::PositionToCollapsedRange
455 // Converts a given DOM position to a collapsed range covering that
456 // position. We use ranges to store DOM positions becuase they stay
457 // updated as the DOM is changed.
459 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
460 nsINode* aNode, uint32_t aOffset) {
461 if (NS_WARN_IF(!aNode) || NS_WARN_IF(!GetDocument())) {
462 return nullptr;
464 IgnoredErrorResult ignoredError;
465 RefPtr<nsRange> range =
466 nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
467 NS_WARNING_ASSERTION(!ignoredError.Failed(),
468 "Creating collapsed range failed");
469 return range.forget();
472 // mozInlineSpellResume
474 class mozInlineSpellResume : public Runnable {
475 public:
476 mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
477 uint32_t aDisabledAsyncToken)
478 : Runnable("mozInlineSpellResume"),
479 mDisabledAsyncToken(aDisabledAsyncToken),
480 mStatus(std::move(aStatus)) {}
482 nsresult Post() {
483 nsCOMPtr<nsIRunnable> runnable(this);
484 return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
485 EventQueuePriority::Idle);
488 NS_IMETHOD Run() override {
489 // Discard the resumption if the spell checker was disabled after the
490 // resumption was scheduled.
491 if (mDisabledAsyncToken ==
492 mStatus->mSpellChecker->GetDisabledAsyncToken()) {
493 mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
495 return NS_OK;
498 private:
499 uint32_t mDisabledAsyncToken;
500 UniquePtr<mozInlineSpellStatus> mStatus;
503 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
504 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
505 ~InitEditorSpellCheckCallback() {}
507 public:
508 NS_DECL_ISUPPORTS
510 explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
511 : mSpellChecker(aSpellChecker) {}
513 NS_IMETHOD EditorSpellCheckDone() override {
514 return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
517 void Cancel() { mSpellChecker = nullptr; }
519 private:
520 RefPtr<mozInlineSpellChecker> mSpellChecker;
522 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
524 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
525 NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
526 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
527 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
528 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
529 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
530 NS_INTERFACE_MAP_END
532 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
533 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
535 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mTextEditor, mSpellCheck,
536 mCurrentSelectionAnchorNode)
538 mozInlineSpellChecker::SpellCheckingState
539 mozInlineSpellChecker::gCanEnableSpellChecking =
540 mozInlineSpellChecker::SpellCheck_Uninitialized;
542 mozInlineSpellChecker::mozInlineSpellChecker()
543 : mNumWordsInSpellSelection(0),
544 mMaxNumWordsInSpellSelection(250),
545 mNumPendingSpellChecks(0),
546 mNumPendingUpdateCurrentDictionary(0),
547 mDisabledAsyncToken(0),
548 mNeedsCheckAfterNavigation(false),
549 mFullSpellCheckScheduled(false),
550 mIsListeningToEditSubActions(false) {
551 nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
552 if (prefs)
553 prefs->GetIntPref(kMaxSpellCheckSelectionSize,
554 &mMaxNumWordsInSpellSelection);
557 mozInlineSpellChecker::~mozInlineSpellChecker() {}
559 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
560 return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
563 NS_IMETHODIMP
564 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
565 *aSpellCheck = mSpellCheck;
566 NS_IF_ADDREF(*aSpellCheck);
567 return NS_OK;
570 NS_IMETHODIMP
571 mozInlineSpellChecker::Init(nsIEditor* aEditor) {
572 mTextEditor = aEditor ? aEditor->AsTextEditor() : nullptr;
573 return NS_OK;
576 // mozInlineSpellChecker::Cleanup
578 // Called by the editor when the editor is going away. This is important
579 // because we remove listeners. We do NOT clean up anything else in this
580 // function, because it can get called while DoSpellCheck is running!
582 // Getting the style information there can cause DOM notifications to be
583 // flushed, which can cause editors to go away which will bring us here.
584 // We can not do anything that will cause DoSpellCheck to freak out.
586 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
587 mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
588 mNumWordsInSpellSelection = 0;
589 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
590 nsresult rv = NS_OK;
591 if (!spellCheckSelection) {
592 // Ensure we still unregister event listeners (but return a failure code)
593 UnregisterEventListeners();
594 rv = NS_ERROR_FAILURE;
595 } else {
596 if (!aDestroyingFrames) {
597 spellCheckSelection->RemoveAllRanges(IgnoreErrors());
600 rv = UnregisterEventListeners();
603 // Notify ENDED observers now. If we wait to notify as we normally do when
604 // these async operations finish, then in the meantime the editor may create
605 // another inline spell checker and cause more STARTED and ENDED
606 // notifications to be broadcast. Interleaved notifications for the same
607 // editor but different inline spell checkers could easily confuse
608 // observers. They may receive two consecutive STARTED notifications for
609 // example, which we guarantee will not happen.
611 RefPtr<TextEditor> textEditor = std::move(mTextEditor);
612 if (mPendingSpellCheck) {
613 // Cancel the pending editor spell checker initialization.
614 mPendingSpellCheck = nullptr;
615 mPendingInitEditorSpellCheckCallback->Cancel();
616 mPendingInitEditorSpellCheckCallback = nullptr;
617 ChangeNumPendingSpellChecks(-1, textEditor);
620 // Increment this token so that pending UpdateCurrentDictionary calls and
621 // scheduled spell checks are discarded when they finish.
622 mDisabledAsyncToken++;
624 if (mNumPendingUpdateCurrentDictionary > 0) {
625 // Account for pending UpdateCurrentDictionary calls.
626 ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
627 textEditor);
628 mNumPendingUpdateCurrentDictionary = 0;
630 if (mNumPendingSpellChecks > 0) {
631 // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
632 // pending scheduled spell checks.
633 ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, textEditor);
636 mFullSpellCheckScheduled = false;
638 return rv;
641 // mozInlineSpellChecker::CanEnableInlineSpellChecking
643 // This function can be called to see if it seems likely that we can enable
644 // spellchecking before actually creating the InlineSpellChecking objects.
646 // The problem is that we can't get the dictionary list without actually
647 // creating a whole bunch of spellchecking objects. This function tries to
648 // do that and caches the result so we don't have to keep allocating those
649 // objects if there are no dictionaries or spellchecking.
651 // Whenever dictionaries are added or removed at runtime, this value must be
652 // updated before an observer notification is sent out about the change, to
653 // avoid editors getting a wrong cached result.
655 bool // static
656 mozInlineSpellChecker::CanEnableInlineSpellChecking() {
657 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
658 gCanEnableSpellChecking = SpellCheck_NotAvailable;
660 nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
662 bool canSpellCheck = false;
663 nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
664 NS_ENSURE_SUCCESS(rv, false);
666 if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
668 return (gCanEnableSpellChecking == SpellCheck_Available);
671 void // static
672 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
673 gCanEnableSpellChecking = SpellCheck_Uninitialized;
676 // mozInlineSpellChecker::RegisterEventListeners
678 // The inline spell checker listens to mouse events and keyboard navigation
679 // events.
681 nsresult mozInlineSpellChecker::RegisterEventListeners() {
682 if (NS_WARN_IF(!mTextEditor)) {
683 return NS_ERROR_FAILURE;
686 StartToListenToEditSubActions();
688 RefPtr<Document> doc = mTextEditor->GetDocument();
689 if (NS_WARN_IF(!doc)) {
690 return NS_ERROR_FAILURE;
692 doc->AddEventListener(u"blur"_ns, this, true, false);
693 doc->AddEventListener(u"click"_ns, this, false, false);
694 doc->AddEventListener(u"keypress"_ns, this, false, false);
695 return NS_OK;
698 // mozInlineSpellChecker::UnregisterEventListeners
700 nsresult mozInlineSpellChecker::UnregisterEventListeners() {
701 if (NS_WARN_IF(!mTextEditor)) {
702 return NS_ERROR_FAILURE;
705 EndListeningToEditSubActions();
707 RefPtr<Document> doc = mTextEditor->GetDocument();
708 if (NS_WARN_IF(!doc)) {
709 return NS_ERROR_FAILURE;
711 doc->RemoveEventListener(u"blur"_ns, this, true);
712 doc->RemoveEventListener(u"click"_ns, this, false);
713 doc->RemoveEventListener(u"keypress"_ns, this, false);
714 return NS_OK;
717 // mozInlineSpellChecker::GetEnableRealTimeSpell
719 NS_IMETHODIMP
720 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
721 NS_ENSURE_ARG_POINTER(aEnabled);
722 *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
723 return NS_OK;
726 // mozInlineSpellChecker::SetEnableRealTimeSpell
728 NS_IMETHODIMP
729 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
730 if (!aEnabled) {
731 mSpellCheck = nullptr;
732 return Cleanup(false);
735 if (mSpellCheck) {
736 // spellcheck the current contents. SpellCheckRange doesn't supply a created
737 // range to DoSpellCheck, which in our case is the entire range. But this
738 // optimization doesn't matter because there is nothing in the spellcheck
739 // selection when starting, which triggers a better optimization.
740 return SpellCheckRange(nullptr);
743 if (mPendingSpellCheck) {
744 // The editor spell checker is already being initialized.
745 return NS_OK;
748 mPendingSpellCheck = new EditorSpellCheck();
749 mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
751 mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
752 nsresult rv = mPendingSpellCheck->InitSpellChecker(
753 mTextEditor, false, mPendingInitEditorSpellCheckCallback);
754 if (NS_FAILED(rv)) {
755 mPendingSpellCheck = nullptr;
756 mPendingInitEditorSpellCheckCallback = nullptr;
757 NS_ENSURE_SUCCESS(rv, rv);
760 ChangeNumPendingSpellChecks(1);
762 return NS_OK;
765 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
766 nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
767 NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!");
769 // spell checking is enabled, register our event listeners to track navigation
770 RegisterEventListeners();
772 mSpellCheck = mPendingSpellCheck;
773 mPendingSpellCheck = nullptr;
774 mPendingInitEditorSpellCheckCallback = nullptr;
775 ChangeNumPendingSpellChecks(-1);
777 // spellcheck the current contents. SpellCheckRange doesn't supply a created
778 // range to DoSpellCheck, which in our case is the entire range. But this
779 // optimization doesn't matter because there is nothing in the spellcheck
780 // selection when starting, which triggers a better optimization.
781 return SpellCheckRange(nullptr);
784 // Changes the number of pending spell checks by the given delta. If the number
785 // becomes zero or nonzero, observers are notified. See NotifyObservers for
786 // info on the aEditor parameter.
787 void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
788 int32_t aDelta, TextEditor* aTextEditor) {
789 int8_t oldNumPending = mNumPendingSpellChecks;
790 mNumPendingSpellChecks += aDelta;
791 NS_ASSERTION(mNumPendingSpellChecks >= 0,
792 "Unbalanced ChangeNumPendingSpellChecks calls!");
793 if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
794 NotifyObservers(INLINESPELL_STARTED_TOPIC, aTextEditor);
795 } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
796 NotifyObservers(INLINESPELL_ENDED_TOPIC, aTextEditor);
800 // Broadcasts the given topic to observers. aEditor is passed to observers if
801 // nonnull; otherwise mTextEditor is passed.
802 void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
803 TextEditor* aTextEditor) {
804 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
805 if (!os) return;
806 // XXX Do we need to grab the editor here? If it's necessary, each observer
807 // should do it instead.
808 RefPtr<TextEditor> textEditor = aTextEditor ? aTextEditor : mTextEditor.get();
809 os->NotifyObservers(static_cast<nsIEditor*>(textEditor.get()), aTopic,
810 nullptr);
813 // mozInlineSpellChecker::SpellCheckAfterEditorChange
815 // Called by the editor when nearly anything happens to change the content.
817 // The start and end positions specify a range for the thing that happened,
818 // but these are usually nullptr, even when you'd think they would be useful
819 // because you want the range (for example, pasting). We ignore them in
820 // this case.
822 nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
823 EditSubAction aEditSubAction, Selection& aSelection,
824 nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
825 nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
826 uint32_t aEndOffset) {
827 nsresult rv;
828 if (!mSpellCheck) return NS_OK; // disabling spell checking is not an error
830 // this means something has changed, and we never check the current word,
831 // therefore, we should spellcheck for subsequent caret navigations
832 mNeedsCheckAfterNavigation = true;
834 // the anchor node is the position of the caret
835 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
836 mozInlineSpellStatus::CreateForEditorChange(
837 *this, aEditSubAction, aSelection.GetAnchorNode(),
838 aSelection.AnchorOffset(), aPreviousSelectedNode,
839 aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
840 aEndOffset);
841 if (NS_WARN_IF(res.isErr())) {
842 return res.unwrapErr();
845 rv = ScheduleSpellCheck(res.unwrap());
846 NS_ENSURE_SUCCESS(rv, rv);
848 // remember the current caret position after every change
849 SaveCurrentSelectionPosition();
850 return NS_OK;
853 // mozInlineSpellChecker::SpellCheckRange
855 // Spellchecks all the words in the given range.
856 // Supply a nullptr range and this will check the entire editor.
858 nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
859 if (!mSpellCheck) {
860 NS_WARNING_ASSERTION(
861 mPendingSpellCheck,
862 "Trying to spellcheck, but checking seems to be disabled");
863 return NS_ERROR_NOT_INITIALIZED;
866 UniquePtr<mozInlineSpellStatus> status =
867 mozInlineSpellStatus::CreateForRange(*this, aRange);
868 return ScheduleSpellCheck(std::move(status));
871 // mozInlineSpellChecker::GetMisspelledWord
873 NS_IMETHODIMP
874 mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, int32_t aOffset,
875 nsRange** newword) {
876 if (NS_WARN_IF(!aNode)) {
877 return NS_ERROR_INVALID_ARG;
879 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
880 if (NS_WARN_IF(!spellCheckSelection)) {
881 return NS_ERROR_FAILURE;
883 return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
886 // mozInlineSpellChecker::ReplaceWord
888 NS_IMETHODIMP
889 mozInlineSpellChecker::ReplaceWord(nsINode* aNode, int32_t aOffset,
890 const nsAString& aNewWord) {
891 if (NS_WARN_IF(!mTextEditor) || NS_WARN_IF(aNewWord.IsEmpty())) {
892 return NS_ERROR_FAILURE;
895 RefPtr<nsRange> range;
896 nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
897 NS_ENSURE_SUCCESS(res, res);
899 if (!range) {
900 return NS_OK;
903 // In usual cases, any words shouldn't include line breaks, but technically,
904 // they may include and we need to avoid `HTMLTextAreaElement.value` returns
905 // \r. Therefore, we need to handle it here.
906 nsString newWord(aNewWord);
907 if (!mTextEditor->AsHTMLEditor()) {
908 nsContentUtils::PlatformToDOMLineBreaks(newWord);
911 // Blink dispatches cancelable `beforeinput` event at collecting misspelled
912 // word so that we should allow to dispatch cancelable event.
913 RefPtr<TextEditor> textEditor(mTextEditor);
914 DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(
915 newWord, range, TextEditor::AllowBeforeInputEventCancelable::Yes);
916 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
917 return NS_OK;
920 // mozInlineSpellChecker::AddWordToDictionary
922 NS_IMETHODIMP
923 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
924 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
926 nsresult rv = mSpellCheck->AddWordToDictionary(word);
927 NS_ENSURE_SUCCESS(rv, rv);
929 UniquePtr<mozInlineSpellStatus> status =
930 mozInlineSpellStatus::CreateForSelection(*this);
931 return ScheduleSpellCheck(std::move(status));
934 // mozInlineSpellChecker::RemoveWordFromDictionary
936 NS_IMETHODIMP
937 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
938 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
940 nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
941 NS_ENSURE_SUCCESS(rv, rv);
943 UniquePtr<mozInlineSpellStatus> status =
944 mozInlineSpellStatus::CreateForRange(*this, nullptr);
945 return ScheduleSpellCheck(std::move(status));
948 // mozInlineSpellChecker::IgnoreWord
950 NS_IMETHODIMP
951 mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
952 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
954 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
955 NS_ENSURE_SUCCESS(rv, rv);
957 UniquePtr<mozInlineSpellStatus> status =
958 mozInlineSpellStatus::CreateForSelection(*this);
959 return ScheduleSpellCheck(std::move(status));
962 // mozInlineSpellChecker::IgnoreWords
964 NS_IMETHODIMP
965 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
966 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
968 // add each word to the ignore list and then recheck the document
969 for (auto& word : aWordsToIgnore) {
970 mSpellCheck->IgnoreWordAllOccurrences(word);
973 UniquePtr<mozInlineSpellStatus> status =
974 mozInlineSpellStatus::CreateForSelection(*this);
975 return ScheduleSpellCheck(std::move(status));
978 void mozInlineSpellChecker::DidSplitNode(nsINode* aExistingRightNode,
979 nsINode* aNewLeftNode) {
980 if (!mIsListeningToEditSubActions) {
981 return;
983 SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
986 void mozInlineSpellChecker::DidJoinNodes(nsINode& aLeftNode,
987 nsINode& aRightNode) {
988 if (!mIsListeningToEditSubActions) {
989 return;
991 SpellCheckBetweenNodes(&aRightNode, 0, &aRightNode, 0);
994 // mozInlineSpellChecker::MakeSpellCheckRange
996 // Given begin and end positions, this function constructs a range as
997 // required for ScheduleSpellCheck. If the start and end nodes are nullptr,
998 // then the entire range will be selected, and you can supply -1 as the
999 // offset to the end range to select all of that node.
1001 // If the resulting range would be empty, nullptr is put into *aRange and the
1002 // function succeeds.
1004 nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
1005 int32_t aStartOffset,
1006 nsINode* aEndNode,
1007 int32_t aEndOffset,
1008 nsRange** aRange) const {
1009 nsresult rv;
1010 *aRange = nullptr;
1012 if (NS_WARN_IF(!mTextEditor)) {
1013 return NS_ERROR_FAILURE;
1016 RefPtr<Document> doc = mTextEditor->GetDocument();
1017 if (NS_WARN_IF(!doc)) {
1018 return NS_ERROR_FAILURE;
1021 RefPtr<nsRange> range = nsRange::Create(doc);
1023 // possibly use full range of the editor
1024 if (!aStartNode || !aEndNode) {
1025 Element* domRootElement = mTextEditor->GetRoot();
1026 if (NS_WARN_IF(!domRootElement)) {
1027 return NS_ERROR_FAILURE;
1029 aStartNode = aEndNode = domRootElement;
1030 aStartOffset = 0;
1031 aEndOffset = -1;
1034 if (aEndOffset == -1) {
1035 // It's hard to say whether it's better to just do nsINode::GetChildCount or
1036 // get the ChildNodes() and then its length. The latter is faster if we
1037 // keep going through this code for the same nodes (because it caches the
1038 // length). The former is faster if we keep getting different nodes here...
1040 // Let's do the thing which can't end up with bad O(N^2) behavior.
1041 aEndOffset = aEndNode->ChildNodes()->Length();
1044 // sometimes we are are requested to check an empty range (possibly an empty
1045 // document). This will result in assertions later.
1046 if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
1048 if (aEndOffset) {
1049 rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
1050 if (NS_WARN_IF(NS_FAILED(rv))) {
1051 return rv;
1053 } else {
1054 rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
1055 RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
1056 if (NS_WARN_IF(NS_FAILED(rv))) {
1057 return rv;
1061 range.swap(*aRange);
1062 return NS_OK;
1065 nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
1066 int32_t aStartOffset,
1067 nsINode* aEndNode,
1068 int32_t aEndOffset) {
1069 RefPtr<nsRange> range;
1070 nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
1071 aEndOffset, getter_AddRefs(range));
1072 NS_ENSURE_SUCCESS(rv, rv);
1074 if (!range) return NS_OK; // range is empty: nothing to do
1076 UniquePtr<mozInlineSpellStatus> status =
1077 mozInlineSpellStatus::CreateForRange(*this, range);
1078 return ScheduleSpellCheck(std::move(status));
1081 // mozInlineSpellChecker::ShouldSpellCheckNode
1083 // There are certain conditions when we don't want to spell check a node. In
1084 // particular quotations, moz signatures, etc. This routine returns false
1085 // for these cases.
1087 bool mozInlineSpellChecker::ShouldSpellCheckNode(TextEditor* aTextEditor,
1088 nsINode* aNode) const {
1089 MOZ_ASSERT(aNode);
1090 if (!aNode->IsContent()) return false;
1092 nsIContent* content = aNode->AsContent();
1094 if (aTextEditor->IsMailEditor()) {
1095 nsIContent* parent = content->GetParent();
1096 while (parent) {
1097 if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
1098 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1099 nsGkAtoms::cite, eIgnoreCase)) {
1100 return false;
1102 if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
1103 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1104 nsGkAtoms::mozsignature,
1105 eIgnoreCase)) {
1106 return false;
1108 if (parent->IsHTMLElement(nsGkAtoms::div) &&
1109 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1110 nsGkAtoms::mozfwcontainer,
1111 eIgnoreCase)) {
1112 return false;
1115 parent = parent->GetParent();
1117 } else {
1118 // Check spelling only if the node is editable, and GetSpellcheck() is true
1119 // on the nearest HTMLElement ancestor.
1120 if (!content->IsEditable()) {
1121 return false;
1124 // Make sure that we can always turn on spell checking for inputs/textareas.
1125 // Note that because of the previous check, at this point we know that the
1126 // node is editable.
1127 if (content->IsInNativeAnonymousSubtree()) {
1128 nsIContent* node = content->GetParent();
1129 while (node && node->IsInNativeAnonymousSubtree()) {
1130 node = node->GetParent();
1132 if (node && node->IsTextControlElement()) {
1133 return true;
1137 // Get HTML element ancestor (might be aNode itself, although probably that
1138 // has to be a text node in real life here)
1139 nsIContent* parent = content;
1140 while (!parent->IsHTMLElement()) {
1141 parent = parent->GetParent();
1142 if (!parent) {
1143 return true;
1147 // See if it's spellcheckable
1148 return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
1151 return true;
1154 // mozInlineSpellChecker::ScheduleSpellCheck
1156 // This is called by code to do the actual spellchecking. We will set up
1157 // the proper structures for calls to DoSpellCheck.
1159 nsresult mozInlineSpellChecker::ScheduleSpellCheck(
1160 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1161 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1163 if (mFullSpellCheckScheduled) {
1164 // Just ignore this; we're going to spell-check everything anyway
1165 return NS_OK;
1167 // Cache the value because we are going to move aStatus's ownership to
1168 // the new created mozInlineSpellResume instance.
1169 bool isFullSpellCheck = aStatus->IsFullSpellCheck();
1171 RefPtr<mozInlineSpellResume> resume =
1172 new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
1173 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1175 nsresult rv = resume->Post();
1176 if (NS_SUCCEEDED(rv)) {
1177 if (isFullSpellCheck) {
1178 // We're going to check everything. Suppress further spell-check attempts
1179 // until that happens.
1180 mFullSpellCheckScheduled = true;
1182 ChangeNumPendingSpellChecks(1);
1184 return rv;
1187 // mozInlineSpellChecker::DoSpellCheckSelection
1189 // Called to re-check all misspelled words. We iterate over all ranges in
1190 // the selection and call DoSpellCheck on them. This is used when a word
1191 // is ignored or added to the dictionary: all instances of that word should
1192 // be removed from the selection.
1194 // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1195 // Typically, checking this small amount of text is relatively fast, but
1196 // for large numbers of words, a lag may be noticeable.
1198 nsresult mozInlineSpellChecker::DoSpellCheckSelection(
1199 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
1200 nsresult rv;
1202 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1203 mNumWordsInSpellSelection = 0;
1205 // Since we could be modifying the ranges for the spellCheckSelection while
1206 // looping on the spell check selection, keep a separate array of range
1207 // elements inside the selection
1208 nsTArray<RefPtr<nsRange>> ranges;
1210 int32_t count = aSpellCheckSelection->RangeCount();
1212 for (int32_t idx = 0; idx < count; idx++) {
1213 nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
1214 if (range) {
1215 ranges.AppendElement(range);
1219 // We have saved the ranges above. Clearing the spellcheck selection here
1220 // isn't necessary (rechecking each word will modify it as necessary) but
1221 // provides better performance. By ensuring that no ranges need to be
1222 // removed in DoSpellCheck, we can save checking range inclusion which is
1223 // slow.
1224 aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
1226 // We use this state object for all calls, and just update its range. Note
1227 // that we don't need to call FinishInit since we will be filling in the
1228 // necessary information.
1229 UniquePtr<mozInlineSpellStatus> status =
1230 mozInlineSpellStatus::CreateForRange(*this, nullptr);
1232 bool doneChecking;
1233 for (int32_t idx = 0; idx < count; idx++) {
1234 // We can consider this word as "added" since we know it has no spell
1235 // check range over it that needs to be deleted. All the old ranges
1236 // were cleared above. We also need to clear the word count so that we
1237 // check all words instead of stopping early.
1238 status->mRange = ranges[idx];
1239 rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
1240 NS_ENSURE_SUCCESS(rv, rv);
1241 MOZ_ASSERT(
1242 doneChecking,
1243 "We gave the spellchecker one word, but it didn't finish checking?!?!");
1246 return NS_OK;
1249 // mozInlineSpellChecker::DoSpellCheck
1251 // This function checks words intersecting the given range, excluding those
1252 // inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
1253 // will have any spell selection removed (this is used to hide the
1254 // underlining for the word that the caret is in). aNoCheckRange should be
1255 // on word boundaries.
1257 // mResume->mCreatedRange is a possibly nullptr range of new text that was
1258 // inserted. Inside this range, we don't bother to check whether things are
1259 // inside the spellcheck selection, which speeds up large paste operations
1260 // considerably.
1262 // Normal case when editing text by typing
1263 // h e l l o w o r k d h o w a r e y o u
1264 // ^ caret
1265 // [-------] mRange
1266 // [-------] mNoCheckRange
1267 // -> does nothing (range is the same as the no check range)
1269 // Case when pasting:
1270 // [---------- pasted text ----------]
1271 // h e l l o w o r k d h o w a r e y o u
1272 // ^ caret
1273 // [---] aNoCheckRange
1274 // -> recheck all words in range except those in aNoCheckRange
1276 // If checking is complete, *aDoneChecking will be set. If there is more
1277 // but we ran out of time, this will be false and the range will be
1278 // updated with the stuff that still needs checking.
1280 nsresult mozInlineSpellChecker::DoSpellCheck(
1281 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
1282 const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
1283 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1285 *aDoneChecking = true;
1287 if (NS_WARN_IF(!mSpellCheck)) {
1288 return NS_ERROR_NOT_INITIALIZED;
1291 if (IsSpellCheckSelectionFull()) {
1292 return NS_OK;
1295 // get the editor for ShouldSpellCheckNode, this may fail in reasonable
1296 // circumstances since the editor could have gone away
1297 RefPtr<TextEditor> textEditor = mTextEditor;
1298 if (!textEditor || textEditor->Destroyed()) {
1299 return NS_ERROR_FAILURE;
1302 if (aStatus->mRange->Collapsed()) return NS_OK;
1304 // see if the selection has any ranges, if not, then we can optimize checking
1305 // range inclusion later (we have no ranges when we are initially checking or
1306 // when there are no misspelled words yet).
1307 int32_t originalRangeCount = aSpellCheckSelection->RangeCount();
1309 // set the starting DOM position to be the beginning of our range
1311 // Scope for the node/offset pairs here so they don't get
1312 // accidentally used later
1313 nsINode* beginNode = aStatus->mRange->GetStartContainer();
1314 int32_t beginOffset = aStatus->mRange->StartOffset();
1315 nsINode* endNode = aStatus->mRange->GetEndContainer();
1316 int32_t endOffset = aStatus->mRange->EndOffset();
1318 // Now check that we're still looking at a range that's under
1319 // aWordUtil.GetRootNode()
1320 const nsINode* rootNode = aWordUtil.GetRootNode();
1321 if (!beginNode->IsInComposedDoc() || !endNode->IsInComposedDoc() ||
1322 !beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) ||
1323 !endNode->IsShadowIncludingInclusiveDescendantOf(rootNode)) {
1324 // Just bail out and don't try to spell-check this
1325 return NS_OK;
1328 nsresult rv =
1329 aWordUtil.SetPositionAndEnd(beginNode, beginOffset, endNode, endOffset);
1330 if (NS_FAILED(rv)) {
1331 // Just bail out and don't try to spell-check this
1332 return NS_OK;
1336 // aWordUtil.SetPosition flushes pending notifications, check editor again.
1337 if (!mTextEditor) {
1338 return NS_ERROR_FAILURE;
1341 int32_t wordsChecked = 0;
1342 PRTime beginTime = PR_Now();
1344 nsTArray<nsString> words;
1345 nsTArray<NodeOffsetRange> checkRanges;
1346 nsAutoString wordText;
1347 NodeOffsetRange wordNodeOffsetRange;
1348 bool dontCheckWord;
1349 static const size_t requestChunkSize =
1350 INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
1351 while (
1352 aWordUtil.GetNextWord(wordText, &wordNodeOffsetRange, &dontCheckWord)) {
1353 // get the range for the current word.
1354 nsINode* beginNode = wordNodeOffsetRange.Begin().Node();
1355 nsINode* endNode = wordNodeOffsetRange.End().Node();
1356 int32_t beginOffset = wordNodeOffsetRange.Begin().Offset();
1357 int32_t endOffset = wordNodeOffsetRange.End().Offset();
1359 // see if we've done enough words in this round and run out of time.
1360 if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
1361 PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
1362 // stop checking, our time limit has been exceeded.
1363 MOZ_LOG(
1364 sInlineSpellCheckerLog, LogLevel::Verbose,
1365 ("%s: we have run out of time, schedule next round.", __FUNCTION__));
1367 CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words),
1368 std::move(checkRanges));
1370 // move the range to encompass the stuff that needs checking.
1371 nsresult rv = aStatus->mRange->SetStart(beginNode, beginOffset);
1372 if (NS_FAILED(rv)) {
1373 // The range might be unhappy because the beginning is after the
1374 // end. This is possible when the requested end was in the middle
1375 // of a word, just ignore this situation and assume we're done.
1376 return NS_OK;
1378 *aDoneChecking = false;
1379 return NS_OK;
1382 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1383 ("%s: got word \"%s\"%s", __FUNCTION__,
1384 NS_ConvertUTF16toUTF8(wordText).get(),
1385 dontCheckWord ? " (not checking)" : ""));
1387 ErrorResult erv;
1388 // see if there is a spellcheck range that already intersects the word
1389 // and remove it. We only need to remove old ranges, so don't bother if
1390 // there were no ranges when we started out.
1391 if (originalRangeCount > 0) {
1392 // likewise, if this word is inside new text, we won't bother testing
1393 if (!aStatus->GetCreatedRange() ||
1394 !aStatus->GetCreatedRange()->IsPointInRange(*beginNode, beginOffset,
1395 erv)) {
1396 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1397 ("%s: removing ranges for some interval.", __FUNCTION__));
1399 nsTArray<RefPtr<nsRange>> ranges;
1400 aSpellCheckSelection->GetRangesForInterval(
1401 *beginNode, beginOffset, *endNode, endOffset, true, ranges, erv);
1402 ENSURE_SUCCESS(erv, erv.StealNSResult());
1403 for (uint32_t i = 0; i < ranges.Length(); i++)
1404 RemoveRange(aSpellCheckSelection, ranges[i]);
1408 // some words are special and don't need checking
1409 if (dontCheckWord) continue;
1411 // some nodes we don't spellcheck
1412 if (!ShouldSpellCheckNode(textEditor, beginNode)) {
1413 continue;
1416 // Don't check spelling if we're inside the noCheckRange. This needs to
1417 // be done after we clear any old selection because the excluded word
1418 // might have been previously marked.
1420 // We do a simple check to see if the beginning of our word is in the
1421 // exclusion range. Because the exclusion range is a multiple of a word,
1422 // this is sufficient.
1423 if (aStatus->GetNoCheckRange() &&
1424 aStatus->GetNoCheckRange()->IsPointInRange(*beginNode, beginOffset,
1425 erv)) {
1426 continue;
1429 // check spelling and add to selection if misspelled
1430 mozInlineSpellWordUtil::NormalizeWord(wordText);
1431 words.AppendElement(wordText);
1432 checkRanges.AppendElement(wordNodeOffsetRange);
1433 wordsChecked++;
1434 if (words.Length() >= requestChunkSize) {
1435 CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words),
1436 std::move(checkRanges));
1437 // Set new empty data for spellcheck words and range in DOM to avoid
1438 // clang-tidy detection.
1439 words = nsTArray<nsString>();
1440 checkRanges = nsTArray<NodeOffsetRange>();
1444 CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words),
1445 std::move(checkRanges));
1447 return NS_OK;
1450 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
1451 class MOZ_RAII AutoChangeNumPendingSpellChecks final {
1452 public:
1453 explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
1454 int32_t aDelta)
1455 : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
1457 ~AutoChangeNumPendingSpellChecks() {
1458 mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
1461 private:
1462 RefPtr<mozInlineSpellChecker> mSpellChecker;
1463 int32_t mDelta;
1466 void mozInlineSpellChecker::CheckCurrentWordsNoSuggest(
1467 Selection* aSpellCheckSelection, nsTArray<nsString>&& aWords,
1468 nsTArray<NodeOffsetRange>&& aRanges) {
1469 MOZ_ASSERT(aWords.Length() == aRanges.Length());
1471 if (aWords.IsEmpty()) {
1472 return;
1475 ChangeNumPendingSpellChecks(1);
1477 RefPtr<mozInlineSpellChecker> self = this;
1478 RefPtr<Selection> spellCheckerSelection = aSpellCheckSelection;
1479 uint32_t token = mDisabledAsyncToken;
1480 nsTArray<nsString> words = std::move(aWords);
1481 mSpellCheck->CheckCurrentWordsNoSuggest(words)->Then(
1482 GetMainThreadSerialEventTarget(), __func__,
1483 [self, spellCheckerSelection, ranges = std::move(aRanges),
1484 token](const nsTArray<bool>& aIsMisspelled) {
1485 if (token != self->GetDisabledAsyncToken()) {
1486 // This result is never used
1487 return;
1490 if (!self->mTextEditor || self->mTextEditor->Destroyed()) {
1491 return;
1494 AutoChangeNumPendingSpellChecks pendingChecks(self, -1);
1496 if (self->IsSpellCheckSelectionFull()) {
1497 return;
1500 for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
1501 if (!aIsMisspelled[i]) {
1502 continue;
1505 RefPtr<nsRange> wordRange =
1506 mozInlineSpellWordUtil::MakeRange(ranges[i]);
1507 // If we somehow can't make a range for this word, just ignore
1508 // it.
1509 if (wordRange) {
1510 self->AddRange(spellCheckerSelection, wordRange);
1514 [self, token](nsresult aRv) {
1515 if (!self->mTextEditor || self->mTextEditor->Destroyed()) {
1516 return;
1519 if (token != self->GetDisabledAsyncToken()) {
1520 // This result is never used
1521 return;
1524 self->ChangeNumPendingSpellChecks(-1);
1528 // mozInlineSpellChecker::ResumeCheck
1530 // Called by the resume event when it fires. We will try to pick up where
1531 // the last resume left off.
1533 nsresult mozInlineSpellChecker::ResumeCheck(
1534 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1535 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1537 // Observers should be notified that spell check has ended only after spell
1538 // check is done below, but since there are many early returns in this method
1539 // and the number of pending spell checks must be decremented regardless of
1540 // whether the spell check actually happens, use this RAII object.
1541 AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
1543 if (aStatus->IsFullSpellCheck()) {
1544 // Allow posting new spellcheck resume events from inside
1545 // ResumeCheck, now that we're actually firing.
1546 NS_ASSERTION(mFullSpellCheckScheduled,
1547 "How could this be false? The full spell check is "
1548 "calling us!!");
1549 mFullSpellCheckScheduled = false;
1552 if (!mSpellCheck) return NS_OK; // spell checking has been turned off
1554 if (!mTextEditor) {
1555 return NS_OK;
1558 mozInlineSpellWordUtil wordUtil;
1559 nsresult rv = wordUtil.Init(*mTextEditor);
1560 if (NS_FAILED(rv)) return NS_OK; // editor doesn't like us, don't assert
1562 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
1563 if (NS_WARN_IF(!spellCheckSelection)) {
1564 return NS_ERROR_FAILURE;
1567 nsAutoCString currentDictionary;
1568 rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
1569 if (NS_FAILED(rv)) {
1570 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1571 ("%s: no active dictionary.", __FUNCTION__));
1573 // no active dictionary
1574 int32_t count = spellCheckSelection->RangeCount();
1575 for (int32_t index = count - 1; index >= 0; index--) {
1576 RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
1577 if (checkRange) {
1578 RemoveRange(spellCheckSelection, checkRange);
1581 return NS_OK;
1584 CleanupRangesInSelection(spellCheckSelection);
1586 rv = aStatus->FinishInitOnEvent(wordUtil);
1587 NS_ENSURE_SUCCESS(rv, rv);
1588 if (!aStatus->mRange) return NS_OK; // empty range, nothing to do
1590 bool doneChecking = true;
1591 if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
1592 rv = DoSpellCheckSelection(wordUtil, spellCheckSelection);
1593 else
1594 rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
1595 NS_ENSURE_SUCCESS(rv, rv);
1597 if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
1598 return rv;
1601 // mozInlineSpellChecker::IsPointInSelection
1603 // Determines if a given (node,offset) point is inside the given
1604 // selection. If so, the specific range of the selection that
1605 // intersects is places in *aRange. (There may be multiple disjoint
1606 // ranges in a selection.)
1608 // If there is no intersection, *aRange will be nullptr.
1610 // static
1611 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
1612 nsINode* aNode,
1613 int32_t aOffset,
1614 nsRange** aRange) {
1615 *aRange = nullptr;
1617 nsTArray<nsRange*> ranges;
1618 nsresult rv = aSelection.GetRangesForIntervalArray(aNode, aOffset, aNode,
1619 aOffset, true, &ranges);
1620 NS_ENSURE_SUCCESS(rv, rv);
1622 if (ranges.Length() == 0) return NS_OK; // no matches
1624 // there may be more than one range returned, and we don't know what do
1625 // do with that, so just get the first one
1626 NS_ADDREF(*aRange = ranges[0]);
1627 return NS_OK;
1630 nsresult mozInlineSpellChecker::CleanupRangesInSelection(
1631 Selection* aSelection) {
1632 // integrity check - remove ranges that have collapsed to nothing. This
1633 // can happen if the node containing a highlighted word was removed.
1634 if (!aSelection) return NS_ERROR_FAILURE;
1636 int32_t count = aSelection->RangeCount();
1638 for (int32_t index = 0; index < count; index++) {
1639 nsRange* checkRange = aSelection->GetRangeAt(index);
1640 if (checkRange) {
1641 if (checkRange->Collapsed()) {
1642 RemoveRange(aSelection, checkRange);
1643 index--;
1644 count--;
1649 return NS_OK;
1652 // mozInlineSpellChecker::RemoveRange
1654 // For performance reasons, we have an upper bound on the number of word
1655 // ranges in the spell check selection. When removing a range from the
1656 // selection, we need to decrement mNumWordsInSpellSelection
1658 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
1659 nsRange* aRange) {
1660 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1662 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1663 NS_ENSURE_ARG_POINTER(aRange);
1665 ErrorResult rv;
1666 RefPtr<nsRange> range{aRange};
1667 RefPtr<Selection> selection{aSpellCheckSelection};
1668 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
1669 if (!rv.Failed() && mNumWordsInSpellSelection) mNumWordsInSpellSelection--;
1671 return rv.StealNSResult();
1674 // mozInlineSpellChecker::AddRange
1676 // For performance reasons, we have an upper bound on the number of word
1677 // ranges we'll add to the spell check selection. Once we reach that upper
1678 // bound, stop adding the ranges
1680 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
1681 nsRange* aRange) {
1682 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1683 NS_ENSURE_ARG_POINTER(aRange);
1685 nsresult rv = NS_OK;
1687 if (!IsSpellCheckSelectionFull()) {
1688 IgnoredErrorResult err;
1689 aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
1690 err);
1691 if (err.Failed()) {
1692 rv = err.StealNSResult();
1693 } else {
1694 mNumWordsInSpellSelection++;
1698 return rv;
1701 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
1702 if (NS_WARN_IF(!mTextEditor)) {
1703 return nullptr;
1705 RefPtr<Selection> selection =
1706 mTextEditor->GetSelection(SelectionType::eSpellCheck);
1707 if (!selection) {
1708 return nullptr;
1710 return selection.forget();
1713 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
1714 if (NS_WARN_IF(!mTextEditor)) {
1715 return NS_OK; // XXX Why NS_OK?
1718 // figure out the old caret position based on the current selection
1719 RefPtr<Selection> selection = mTextEditor->GetSelection();
1720 if (NS_WARN_IF(!selection)) {
1721 return NS_ERROR_FAILURE;
1724 mCurrentSelectionAnchorNode = selection->GetFocusNode();
1725 mCurrentSelectionOffset = selection->FocusOffset();
1727 return NS_OK;
1730 // mozInlineSpellChecker::HandleNavigationEvent
1732 // Acts upon mouse clicks and keyboard navigation changes, spell checking
1733 // the previous word if the new navigation location moves us to another
1734 // word.
1736 // This is complicated by the fact that our mouse events are happening after
1737 // selection has been changed to account for the mouse click. But keyboard
1738 // events are happening before the caret selection has changed. Working
1739 // around this by letting keyboard events setting forceWordSpellCheck to
1740 // true. aNewPositionOffset also tries to work around this for the
1741 // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1743 nsresult mozInlineSpellChecker::HandleNavigationEvent(
1744 bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
1745 nsresult rv;
1747 // If we already handled the navigation event and there is no possibility
1748 // anything has changed since then, we don't have to do anything. This
1749 // optimization makes a noticeable difference when you hold down a navigation
1750 // key like Page Down.
1751 if (!mNeedsCheckAfterNavigation) return NS_OK;
1753 nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
1754 uint32_t currentAnchorOffset = mCurrentSelectionOffset;
1756 // now remember the new focus position resulting from the event
1757 rv = SaveCurrentSelectionPosition();
1758 NS_ENSURE_SUCCESS(rv, rv);
1760 bool shouldPost;
1761 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
1762 mozInlineSpellStatus::CreateForNavigation(
1763 *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
1764 currentAnchorOffset, mCurrentSelectionAnchorNode,
1765 mCurrentSelectionOffset, &shouldPost);
1767 if (NS_WARN_IF(res.isErr())) {
1768 return res.unwrapErr();
1771 if (shouldPost) {
1772 rv = ScheduleSpellCheck(res.unwrap());
1773 NS_ENSURE_SUCCESS(rv, rv);
1776 return NS_OK;
1779 NS_IMETHODIMP
1780 mozInlineSpellChecker::HandleEvent(Event* aEvent) {
1781 nsAutoString eventType;
1782 aEvent->GetType(eventType);
1784 if (eventType.EqualsLiteral("blur")) {
1785 return OnBlur(aEvent);
1787 if (eventType.EqualsLiteral("click")) {
1788 return OnMouseClick(aEvent);
1790 if (eventType.EqualsLiteral("keypress")) {
1791 return OnKeyPress(aEvent);
1794 return NS_OK;
1797 nsresult mozInlineSpellChecker::OnBlur(Event* aEvent) {
1798 // force spellcheck on blur, for instance when tabbing out of a textbox
1799 HandleNavigationEvent(true);
1800 return NS_OK;
1803 nsresult mozInlineSpellChecker::OnMouseClick(Event* aMouseEvent) {
1804 MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1805 NS_ENSURE_TRUE(mouseEvent, NS_OK);
1807 // ignore any errors from HandleNavigationEvent as we don't want to prevent
1808 // anyone else from seeing this event.
1809 HandleNavigationEvent(mouseEvent->Button() != 0);
1810 return NS_OK;
1813 nsresult mozInlineSpellChecker::OnKeyPress(Event* aKeyEvent) {
1814 RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
1815 NS_ENSURE_TRUE(keyEvent, NS_OK);
1817 uint32_t keyCode = keyEvent->KeyCode();
1819 // we only care about navigation keys that moved selection
1820 switch (keyCode) {
1821 case KeyboardEvent_Binding::DOM_VK_RIGHT:
1822 case KeyboardEvent_Binding::DOM_VK_LEFT:
1823 HandleNavigationEvent(
1824 false, keyCode == KeyboardEvent_Binding::DOM_VK_RIGHT ? 1 : -1);
1825 break;
1826 case KeyboardEvent_Binding::DOM_VK_UP:
1827 case KeyboardEvent_Binding::DOM_VK_DOWN:
1828 case KeyboardEvent_Binding::DOM_VK_HOME:
1829 case KeyboardEvent_Binding::DOM_VK_END:
1830 case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
1831 case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
1832 HandleNavigationEvent(true /* force a spelling correction */);
1833 break;
1836 return NS_OK;
1839 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
1840 class UpdateCurrentDictionaryCallback final
1841 : public nsIEditorSpellCheckCallback {
1842 public:
1843 NS_DECL_ISUPPORTS
1845 explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
1846 uint32_t aDisabledAsyncToken)
1847 : mSpellChecker(aSpellChecker),
1848 mDisabledAsyncToken(aDisabledAsyncToken) {}
1850 NS_IMETHOD EditorSpellCheckDone() override {
1851 // Ignore this callback if SetEnableRealTimeSpell(false) was called after
1852 // the UpdateCurrentDictionary call that triggered it.
1853 return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
1854 ? NS_OK
1855 : mSpellChecker->CurrentDictionaryUpdated();
1858 private:
1859 ~UpdateCurrentDictionaryCallback() {}
1861 RefPtr<mozInlineSpellChecker> mSpellChecker;
1862 uint32_t mDisabledAsyncToken;
1864 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
1866 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
1867 // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
1868 // checker is being initialized. Calling UpdateCurrentDictionary on
1869 // mPendingSpellCheck simply queues the dictionary update after the init.
1870 RefPtr<EditorSpellCheck> spellCheck =
1871 mSpellCheck ? mSpellCheck : mPendingSpellCheck;
1872 if (!spellCheck) {
1873 return NS_OK;
1876 RefPtr<UpdateCurrentDictionaryCallback> cb =
1877 new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
1878 NS_ENSURE_STATE(cb);
1879 nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
1880 if (NS_FAILED(rv)) {
1881 cb = nullptr;
1882 return rv;
1884 mNumPendingUpdateCurrentDictionary++;
1885 ChangeNumPendingSpellChecks(1);
1887 return NS_OK;
1890 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
1891 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
1892 mNumPendingUpdateCurrentDictionary--;
1893 NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0,
1894 "CurrentDictionaryUpdated called without corresponding "
1895 "UpdateCurrentDictionary call!");
1896 ChangeNumPendingSpellChecks(-1);
1898 nsresult rv = SpellCheckRange(nullptr);
1899 NS_ENSURE_SUCCESS(rv, rv);
1901 return NS_OK;
1904 NS_IMETHODIMP
1905 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
1906 *aPending = mNumPendingSpellChecks > 0;
1907 return NS_OK;