no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / extensions / spellcheck / src / mozInlineSpellChecker.cpp
blobd144fff3171ffaf03f36cbd0c5b2d7ae615212b0
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 * 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.
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.
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.
33 #include "mozInlineSpellChecker.h"
35 #include "mozilla/Assertions.h"
36 #include "mozilla/Attributes.h"
37 #include "mozilla/EditAction.h"
38 #include "mozilla/EditorBase.h"
39 #include "mozilla/EditorDOMPoint.h"
40 #include "mozilla/EditorSpellCheck.h"
41 #include "mozilla/EventListenerManager.h"
42 #include "mozilla/HTMLEditor.h"
43 #include "mozilla/IntegerRange.h"
44 #include "mozilla/Logging.h"
45 #include "mozilla/RangeUtils.h"
46 #include "mozilla/Services.h"
47 #include "mozilla/StaticPrefs_extensions.h"
48 #include "mozilla/TextEvents.h"
49 #include "mozilla/dom/Event.h"
50 #include "mozilla/dom/KeyboardEvent.h"
51 #include "mozilla/dom/KeyboardEventBinding.h"
52 #include "mozilla/dom/MouseEvent.h"
53 #include "mozilla/dom/Selection.h"
54 #include "mozInlineSpellWordUtil.h"
55 #ifdef ACCESSIBILITY
56 # include "nsAccessibilityService.h"
57 #endif
58 #include "nsCOMPtr.h"
59 #include "nsCRT.h"
60 #include "nsGenericHTMLElement.h"
61 #include "nsRange.h"
62 #include "nsIPrefBranch.h"
63 #include "nsIPrefService.h"
64 #include "nsIRunnable.h"
65 #include "nsServiceManagerUtils.h"
66 #include "nsString.h"
67 #include "nsThreadUtils.h"
68 #include "nsUnicharUtils.h"
69 #include "nsIContent.h"
70 #include "nsIContentInlines.h"
71 #include "nsRange.h"
72 #include "nsContentUtils.h"
73 #include "nsIObserverService.h"
74 #include "prtime.h"
76 using mozilla::LogLevel;
77 using namespace mozilla;
78 using namespace mozilla::dom;
79 using namespace mozilla::ipc;
81 // the number of milliseconds that we will take at once to do spellchecking
82 #define INLINESPELL_CHECK_TIMEOUT 1
84 // The number of words to check before we look at the time to see if
85 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
86 // stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
87 // be too short to a low-end machine.
88 #define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
90 // The maximum number of words to check word via IPC.
91 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
93 // These notifications are broadcast when spell check starts and ends. STARTED
94 // must always be followed by ENDED.
95 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
96 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
98 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
100 static const PRTime kMaxSpellCheckTimeInUsec =
101 INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
103 mozInlineSpellStatus::mozInlineSpellStatus(
104 mozInlineSpellChecker* aSpellChecker, const Operation aOp,
105 RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
106 RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
107 const int32_t aNewNavigationPositionOffset)
108 : mSpellChecker(aSpellChecker),
109 mRange(std::move(aRange)),
110 mOp(aOp),
111 mCreatedRange(std::move(aCreatedRange)),
112 mAnchorRange(std::move(aAnchorRange)),
113 mForceNavigationWordCheck(aForceNavigationWordCheck),
114 mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}
116 // mozInlineSpellStatus::CreateForEditorChange
118 // This is the most complicated case. For changes, we need to compute the
119 // range of stuff that changed based on the old and new caret positions,
120 // as well as use a range possibly provided by the editor (start and end,
121 // which are usually nullptr) to get a range with the union of these.
123 // static
124 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
125 mozInlineSpellStatus::CreateForEditorChange(
126 mozInlineSpellChecker& aSpellChecker, const EditSubAction aEditSubAction,
127 nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
128 uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
129 nsINode* aEndNode, uint32_t aEndOffset) {
130 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
132 if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
133 return Err(NS_ERROR_FAILURE);
136 bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
137 if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
138 // IME may remove the previous node if it cancels composition when
139 // there is no text around the composition.
140 deleted = !aPreviousNode->IsInComposedDoc();
143 // save the anchor point as a range so we can find the current word later
144 RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
145 aAnchorNode, aAnchorOffset);
146 if (NS_WARN_IF(!anchorRange)) {
147 return Err(NS_ERROR_FAILURE);
150 // Deletes are easy, the range is just the current anchor. We set the range
151 // to check to be empty, FinishInitOnEvent will fill in the range to be
152 // the current word.
153 RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);
155 // On insert save this range: DoSpellCheck optimizes things in this range.
156 // Otherwise, just leave this nullptr.
157 RefPtr<nsRange> createdRange =
158 (aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;
160 UniquePtr<mozInlineSpellStatus> status{
161 /* The constructor is `private`, hence the explicit allocation. */
162 new mozInlineSpellStatus{&aSpellChecker,
163 deleted ? eOpChangeDelete : eOpChange,
164 std::move(range), std::move(createdRange),
165 std::move(anchorRange), false, 0}};
166 if (deleted) {
167 return status;
170 // ...we need to put the start and end in the correct order
171 ErrorResult errorResult;
172 int16_t cmpResult = status->mAnchorRange->ComparePoint(
173 *aPreviousNode, aPreviousOffset, errorResult);
174 if (NS_WARN_IF(errorResult.Failed())) {
175 return Err(errorResult.StealNSResult());
177 nsresult rv;
178 if (cmpResult < 0) {
179 // previous anchor node is before the current anchor
180 rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
181 aAnchorNode, aAnchorOffset);
182 if (NS_WARN_IF(NS_FAILED(rv))) {
183 return Err(rv);
185 } else {
186 // previous anchor node is after (or the same as) the current anchor
187 rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
188 aPreviousNode, aPreviousOffset);
189 if (NS_WARN_IF(NS_FAILED(rv))) {
190 return Err(rv);
194 // if we were given a range, we need to expand our range to encompass it
195 if (aStartNode && aEndNode) {
196 cmpResult =
197 status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
198 if (NS_WARN_IF(errorResult.Failed())) {
199 return Err(errorResult.StealNSResult());
201 if (cmpResult < 0) { // given range starts before
202 rv = status->mRange->SetStart(aStartNode, aStartOffset);
203 if (NS_WARN_IF(NS_FAILED(rv))) {
204 return Err(rv);
208 cmpResult =
209 status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
210 if (NS_WARN_IF(errorResult.Failed())) {
211 return Err(errorResult.StealNSResult());
213 if (cmpResult > 0) { // given range ends after
214 rv = status->mRange->SetEnd(aEndNode, aEndOffset);
215 if (NS_WARN_IF(NS_FAILED(rv))) {
216 return Err(rv);
221 return status;
224 // mozInlineSpellStatus::CreateForNavigation
226 // For navigation events, we just need to store the new and old positions.
228 // In some cases, we detect that we shouldn't check. If this event should
229 // not be processed, *aContinue will be false.
231 // static
232 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
233 mozInlineSpellStatus::CreateForNavigation(
234 mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
235 int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
236 uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
237 uint32_t aNewAnchorOffset, bool* aContinue) {
238 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
240 RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
241 aNewAnchorNode, aNewAnchorOffset);
242 if (NS_WARN_IF(!anchorRange)) {
243 return Err(NS_ERROR_FAILURE);
246 UniquePtr<mozInlineSpellStatus> status{
247 /* The constructor is `private`, hence the explicit allocation. */
248 new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
249 std::move(anchorRange), aForceCheck,
250 aNewPositionOffset}};
252 // get the root node for checking
253 EditorBase* editorBase = status->mSpellChecker->mEditorBase;
254 if (NS_WARN_IF(!editorBase)) {
255 return Err(NS_ERROR_FAILURE);
257 Element* root = editorBase->GetRoot();
258 if (NS_WARN_IF(!root)) {
259 return Err(NS_ERROR_FAILURE);
261 // the anchor node might not be in the DOM anymore, check
262 if (root && aOldAnchorNode &&
263 !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
264 *aContinue = false;
265 return status;
268 status->mOldNavigationAnchorRange =
269 mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
270 aOldAnchorOffset);
271 if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
272 return Err(NS_ERROR_FAILURE);
275 *aContinue = true;
276 return status;
279 // mozInlineSpellStatus::CreateForSelection
281 // It is easy for selections since we always re-check the spellcheck
282 // selection.
284 // static
285 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
286 mozInlineSpellChecker& aSpellChecker) {
287 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
289 UniquePtr<mozInlineSpellStatus> status{
290 /* The constructor is `private`, hence the explicit allocation. */
291 new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
292 nullptr, false, 0}};
293 return status;
296 // mozInlineSpellStatus::CreateForRange
298 // Called to cause the spellcheck of the given range. This will look like
299 // a change operation over the given range.
301 // static
302 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
303 mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
304 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
305 ("%s: range=%p", __FUNCTION__, aRange));
307 UniquePtr<mozInlineSpellStatus> status{
308 /* The constructor is `private`, hence the explicit allocation. */
309 new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
310 nullptr, false, 0}};
312 status->mRange = aRange;
313 return status;
316 // mozInlineSpellStatus::FinishInitOnEvent
318 // Called when the event is triggered to complete initialization that
319 // might require the WordUtil. This calls to the operation-specific
320 // initializer, and also sets the range to be the entire element if it
321 // is nullptr.
323 // Watch out: the range might still be nullptr if there is nothing to do,
324 // the caller will have to check for this.
326 nsresult mozInlineSpellStatus::FinishInitOnEvent(
327 mozInlineSpellWordUtil& aWordUtil) {
328 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
329 ("%s: mRange=%p", __FUNCTION__, mRange.get()));
331 nsresult rv;
332 if (!mRange) {
333 rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
334 getter_AddRefs(mRange));
335 NS_ENSURE_SUCCESS(rv, rv);
338 switch (mOp) {
339 case eOpChange:
340 if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
341 break;
342 case eOpChangeDelete:
343 if (mAnchorRange) {
344 rv = FillNoCheckRangeFromAnchor(aWordUtil);
345 NS_ENSURE_SUCCESS(rv, rv);
347 // Delete events will have no range for the changed text (because it was
348 // deleted), and CreateForEditorChange will set it to nullptr. Here, we
349 // select the entire word to cause any underlining to be removed.
350 mRange = mNoCheckRange;
351 break;
352 case eOpNavigation:
353 return FinishNavigationEvent(aWordUtil);
354 case eOpSelection:
355 // this gets special handling in ResumeCheck
356 break;
357 case eOpResume:
358 // everything should be initialized already in this case
359 break;
360 default:
361 MOZ_ASSERT_UNREACHABLE("Bad operation");
362 return NS_ERROR_NOT_INITIALIZED;
364 return NS_OK;
367 // mozInlineSpellStatus::FinishNavigationEvent
369 // This verifies that we need to check the word at the previous caret
370 // position. Now that we have the word util, we can find the word belonging
371 // to the previous caret position. If the new position is inside that word,
372 // we don't want to do anything. In this case, we'll nullptr out mRange so
373 // that the caller will know not to continue.
375 // Notice that we don't set mNoCheckRange. We check here whether the cursor
376 // is in the word that needs checking, so it isn't necessary. Plus, the
377 // spellchecker isn't guaranteed to only check the given word, and it could
378 // remove the underline from the new word under the cursor.
380 nsresult mozInlineSpellStatus::FinishNavigationEvent(
381 mozInlineSpellWordUtil& aWordUtil) {
382 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
384 RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
385 if (!editorBase) {
386 return NS_ERROR_FAILURE; // editor is gone
389 MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");
391 if (!mOldNavigationAnchorRange->IsPositioned()) {
392 return NS_ERROR_NOT_INITIALIZED;
395 // get the DOM position of the old caret, the range should be collapsed
396 nsCOMPtr<nsINode> oldAnchorNode =
397 mOldNavigationAnchorRange->GetStartContainer();
398 uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
400 // find the word on the old caret position, this is the one that we MAY need
401 // to check
402 RefPtr<nsRange> oldWord;
403 nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
404 static_cast<int32_t>(oldAnchorOffset),
405 getter_AddRefs(oldWord));
406 NS_ENSURE_SUCCESS(rv, rv);
408 // aWordUtil.GetRangeForWord flushes pending notifications, check editor
409 // again.
410 if (!mSpellChecker->mEditorBase) {
411 return NS_ERROR_FAILURE; // editor is gone
414 // get the DOM position of the new caret, the range should be collapsed
415 nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
416 uint32_t newAnchorOffset = mAnchorRange->StartOffset();
418 // see if the new cursor position is in the word of the old cursor position
419 bool isInRange = false;
420 if (!mForceNavigationWordCheck) {
421 ErrorResult err;
422 isInRange = oldWord->IsPointInRange(
423 *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
424 if (NS_WARN_IF(err.Failed())) {
425 return err.StealNSResult();
429 if (isInRange) {
430 // caller should give up
431 mRange = nullptr;
432 } else {
433 // check the old word
434 mRange = oldWord;
436 // Once we've spellchecked the current word, we don't need to spellcheck
437 // for any more navigation events.
438 mSpellChecker->mNeedsCheckAfterNavigation = false;
440 return NS_OK;
443 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
445 // Given the mAnchorRange object, computes the range of the word it is on
446 // (if any) and fills that range into mNoCheckRange. This is used for
447 // change and navigation events to know which word we should skip spell
448 // checking on
450 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
451 mozInlineSpellWordUtil& aWordUtil) {
452 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
454 if (!mAnchorRange->IsPositioned()) {
455 return NS_ERROR_NOT_INITIALIZED;
457 nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
458 uint32_t anchorOffset = mAnchorRange->StartOffset();
459 return aWordUtil.GetRangeForWord(anchorNode,
460 static_cast<int32_t>(anchorOffset),
461 getter_AddRefs(mNoCheckRange));
464 // mozInlineSpellStatus::GetDocument
466 // Returns the Document object for the document for the
467 // current spellchecker.
469 Document* mozInlineSpellStatus::GetDocument() const {
470 if (!mSpellChecker->mEditorBase) {
471 return nullptr;
474 return mSpellChecker->mEditorBase->GetDocument();
477 // mozInlineSpellStatus::PositionToCollapsedRange
479 // Converts a given DOM position to a collapsed range covering that
480 // position. We use ranges to store DOM positions becuase they stay
481 // updated as the DOM is changed.
483 // static
484 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
485 nsINode* aNode, uint32_t aOffset) {
486 if (NS_WARN_IF(!aNode)) {
487 return nullptr;
489 IgnoredErrorResult ignoredError;
490 RefPtr<nsRange> range =
491 nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
492 NS_WARNING_ASSERTION(!ignoredError.Failed(),
493 "Creating collapsed range failed");
494 return range.forget();
497 // mozInlineSpellResume
499 class mozInlineSpellResume : public Runnable {
500 public:
501 mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
502 uint32_t aDisabledAsyncToken)
503 : Runnable("mozInlineSpellResume"),
504 mDisabledAsyncToken(aDisabledAsyncToken),
505 mStatus(std::move(aStatus)) {}
507 nsresult Post() {
508 nsCOMPtr<nsIRunnable> runnable(this);
509 return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
510 EventQueuePriority::Idle);
513 NS_IMETHOD Run() override {
514 // Discard the resumption if the spell checker was disabled after the
515 // resumption was scheduled.
516 if (mDisabledAsyncToken ==
517 mStatus->mSpellChecker->GetDisabledAsyncToken()) {
518 mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
520 return NS_OK;
523 private:
524 uint32_t mDisabledAsyncToken;
525 UniquePtr<mozInlineSpellStatus> mStatus;
528 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
529 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
530 ~InitEditorSpellCheckCallback() {}
532 public:
533 NS_DECL_ISUPPORTS
535 explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
536 : mSpellChecker(aSpellChecker) {}
538 NS_IMETHOD EditorSpellCheckDone() override {
539 return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
542 void Cancel() { mSpellChecker = nullptr; }
544 private:
545 RefPtr<mozInlineSpellChecker> mSpellChecker;
547 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
549 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
550 NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
551 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
552 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
553 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
554 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
555 NS_INTERFACE_MAP_END
557 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
558 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
560 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
561 mCurrentSelectionAnchorNode)
563 mozInlineSpellChecker::SpellCheckingState
564 mozInlineSpellChecker::gCanEnableSpellChecking =
565 mozInlineSpellChecker::SpellCheck_Uninitialized;
567 mozInlineSpellChecker::mozInlineSpellChecker()
568 : mNumWordsInSpellSelection(0),
569 mMaxNumWordsInSpellSelection(
570 StaticPrefs::extensions_spellcheck_inline_max_misspellings()),
571 mNumPendingSpellChecks(0),
572 mNumPendingUpdateCurrentDictionary(0),
573 mDisabledAsyncToken(0),
574 mNeedsCheckAfterNavigation(false),
575 mFullSpellCheckScheduled(false),
576 mIsListeningToEditSubActions(false) {}
578 mozInlineSpellChecker::~mozInlineSpellChecker() {}
580 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
581 return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
584 NS_IMETHODIMP
585 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
586 *aSpellCheck = mSpellCheck;
587 NS_IF_ADDREF(*aSpellCheck);
588 return NS_OK;
591 NS_IMETHODIMP
592 mozInlineSpellChecker::Init(nsIEditor* aEditor) {
593 mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
594 return NS_OK;
597 // mozInlineSpellChecker::Cleanup
599 // Called by the editor when the editor is going away. This is important
600 // because we remove listeners. We do NOT clean up anything else in this
601 // function, because it can get called while DoSpellCheck is running!
603 // Getting the style information there can cause DOM notifications to be
604 // flushed, which can cause editors to go away which will bring us here.
605 // We can not do anything that will cause DoSpellCheck to freak out.
607 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
608 mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
609 mNumWordsInSpellSelection = 0;
610 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
611 nsresult rv = NS_OK;
612 if (!spellCheckSelection) {
613 // Ensure we still unregister event listeners (but return a failure code)
614 UnregisterEventListeners();
615 rv = NS_ERROR_FAILURE;
616 } else {
617 if (!aDestroyingFrames) {
618 spellCheckSelection->RemoveAllRanges(IgnoreErrors());
621 rv = UnregisterEventListeners();
624 // Notify ENDED observers now. If we wait to notify as we normally do when
625 // these async operations finish, then in the meantime the editor may create
626 // another inline spell checker and cause more STARTED and ENDED
627 // notifications to be broadcast. Interleaved notifications for the same
628 // editor but different inline spell checkers could easily confuse
629 // observers. They may receive two consecutive STARTED notifications for
630 // example, which we guarantee will not happen.
632 RefPtr<EditorBase> editorBase = std::move(mEditorBase);
633 if (mPendingSpellCheck) {
634 // Cancel the pending editor spell checker initialization.
635 mPendingSpellCheck = nullptr;
636 mPendingInitEditorSpellCheckCallback->Cancel();
637 mPendingInitEditorSpellCheckCallback = nullptr;
638 ChangeNumPendingSpellChecks(-1, editorBase);
641 // Increment this token so that pending UpdateCurrentDictionary calls and
642 // scheduled spell checks are discarded when they finish.
643 mDisabledAsyncToken++;
645 if (mNumPendingUpdateCurrentDictionary > 0) {
646 // Account for pending UpdateCurrentDictionary calls.
647 ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
648 editorBase);
649 mNumPendingUpdateCurrentDictionary = 0;
651 if (mNumPendingSpellChecks > 0) {
652 // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
653 // pending scheduled spell checks.
654 ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
657 mFullSpellCheckScheduled = false;
659 return rv;
662 // mozInlineSpellChecker::CanEnableInlineSpellChecking
664 // This function can be called to see if it seems likely that we can enable
665 // spellchecking before actually creating the InlineSpellChecking objects.
667 // The problem is that we can't get the dictionary list without actually
668 // creating a whole bunch of spellchecking objects. This function tries to
669 // do that and caches the result so we don't have to keep allocating those
670 // objects if there are no dictionaries or spellchecking.
672 // Whenever dictionaries are added or removed at runtime, this value must be
673 // updated before an observer notification is sent out about the change, to
674 // avoid editors getting a wrong cached result.
676 bool // static
677 mozInlineSpellChecker::CanEnableInlineSpellChecking() {
678 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
679 gCanEnableSpellChecking = SpellCheck_NotAvailable;
681 nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
683 bool canSpellCheck = false;
684 nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
685 NS_ENSURE_SUCCESS(rv, false);
687 if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
689 return (gCanEnableSpellChecking == SpellCheck_Available);
692 void // static
693 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
694 gCanEnableSpellChecking = SpellCheck_Uninitialized;
697 // mozInlineSpellChecker::RegisterEventListeners
699 // The inline spell checker listens to mouse events and keyboard navigation
700 // events.
702 nsresult mozInlineSpellChecker::RegisterEventListeners() {
703 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
704 return NS_ERROR_FAILURE;
707 StartToListenToEditSubActions();
709 RefPtr<Document> doc = mEditorBase->GetDocument();
710 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
711 return NS_ERROR_FAILURE;
713 EventListenerManager* eventListenerManager =
714 doc->GetOrCreateListenerManager();
715 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
716 return NS_ERROR_FAILURE;
718 eventListenerManager->AddEventListenerByType(
719 this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
720 eventListenerManager->AddEventListenerByType(
721 this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
722 eventListenerManager->AddEventListenerByType(
723 this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
724 return NS_OK;
727 // mozInlineSpellChecker::UnregisterEventListeners
729 nsresult mozInlineSpellChecker::UnregisterEventListeners() {
730 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
731 return NS_ERROR_FAILURE;
734 EndListeningToEditSubActions();
736 RefPtr<Document> doc = mEditorBase->GetDocument();
737 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
738 return NS_ERROR_FAILURE;
740 EventListenerManager* eventListenerManager =
741 doc->GetOrCreateListenerManager();
742 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
743 return NS_ERROR_FAILURE;
745 eventListenerManager->RemoveEventListenerByType(
746 this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
747 eventListenerManager->RemoveEventListenerByType(
748 this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
749 eventListenerManager->RemoveEventListenerByType(
750 this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
751 return NS_OK;
754 // mozInlineSpellChecker::GetEnableRealTimeSpell
756 NS_IMETHODIMP
757 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
758 NS_ENSURE_ARG_POINTER(aEnabled);
759 *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
760 return NS_OK;
763 // mozInlineSpellChecker::SetEnableRealTimeSpell
765 NS_IMETHODIMP
766 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
767 if (!aEnabled) {
768 mSpellCheck = nullptr;
769 return Cleanup(false);
772 if (mSpellCheck) {
773 // spellcheck the current contents. SpellCheckRange doesn't supply a created
774 // range to DoSpellCheck, which in our case is the entire range. But this
775 // optimization doesn't matter because there is nothing in the spellcheck
776 // selection when starting, which triggers a better optimization.
777 return SpellCheckRange(nullptr);
780 if (mPendingSpellCheck) {
781 // The editor spell checker is already being initialized.
782 return NS_OK;
785 mPendingSpellCheck = new EditorSpellCheck();
786 mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
788 mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
789 nsresult rv = mPendingSpellCheck->InitSpellChecker(
790 mEditorBase, false, mPendingInitEditorSpellCheckCallback);
791 if (NS_FAILED(rv)) {
792 mPendingSpellCheck = nullptr;
793 mPendingInitEditorSpellCheckCallback = nullptr;
794 NS_ENSURE_SUCCESS(rv, rv);
797 ChangeNumPendingSpellChecks(1);
799 return NS_OK;
802 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
803 nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
804 MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");
806 // spell checking is enabled, register our event listeners to track navigation
807 RegisterEventListeners();
809 mSpellCheck = mPendingSpellCheck;
810 mPendingSpellCheck = nullptr;
811 mPendingInitEditorSpellCheckCallback = nullptr;
812 ChangeNumPendingSpellChecks(-1);
814 // spellcheck the current contents. SpellCheckRange doesn't supply a created
815 // range to DoSpellCheck, which in our case is the entire range. But this
816 // optimization doesn't matter because there is nothing in the spellcheck
817 // selection when starting, which triggers a better optimization.
818 return SpellCheckRange(nullptr);
821 // Changes the number of pending spell checks by the given delta. If the number
822 // becomes zero or nonzero, observers are notified. See NotifyObservers for
823 // info on the aEditor parameter.
824 void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
825 int32_t aDelta, EditorBase* aEditorBase) {
826 int8_t oldNumPending = mNumPendingSpellChecks;
827 mNumPendingSpellChecks += aDelta;
828 MOZ_ASSERT(mNumPendingSpellChecks >= 0,
829 "Unbalanced ChangeNumPendingSpellChecks calls!");
830 if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
831 NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
832 } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
833 NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
837 // Broadcasts the given topic to observers. aEditor is passed to observers if
838 // nonnull; otherwise mEditorBase is passed.
839 void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
840 EditorBase* aEditorBase) {
841 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
842 if (!os) return;
843 // XXX Do we need to grab the editor here? If it's necessary, each observer
844 // should do it instead.
845 RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
846 os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
847 nullptr);
850 // mozInlineSpellChecker::SpellCheckAfterEditorChange
852 // Called by the editor when nearly anything happens to change the content.
854 // The start and end positions specify a range for the thing that happened,
855 // but these are usually nullptr, even when you'd think they would be useful
856 // because you want the range (for example, pasting). We ignore them in
857 // this case.
859 nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
860 EditSubAction aEditSubAction, Selection& aSelection,
861 nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
862 nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
863 uint32_t aEndOffset) {
864 nsresult rv;
865 if (!mSpellCheck) return NS_OK; // disabling spell checking is not an error
867 // this means something has changed, and we never check the current word,
868 // therefore, we should spellcheck for subsequent caret navigations
869 mNeedsCheckAfterNavigation = true;
871 // the anchor node is the position of the caret
872 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
873 mozInlineSpellStatus::CreateForEditorChange(
874 *this, aEditSubAction, aSelection.GetAnchorNode(),
875 aSelection.AnchorOffset(), aPreviousSelectedNode,
876 aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
877 aEndOffset);
878 if (NS_WARN_IF(res.isErr())) {
879 return res.unwrapErr();
882 rv = ScheduleSpellCheck(res.unwrap());
883 NS_ENSURE_SUCCESS(rv, rv);
885 // remember the current caret position after every change
886 SaveCurrentSelectionPosition();
887 return NS_OK;
890 // mozInlineSpellChecker::SpellCheckRange
892 // Spellchecks all the words in the given range.
893 // Supply a nullptr range and this will check the entire editor.
895 nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
896 if (!mSpellCheck) {
897 NS_WARNING_ASSERTION(
898 mPendingSpellCheck,
899 "Trying to spellcheck, but checking seems to be disabled");
900 return NS_ERROR_NOT_INITIALIZED;
903 UniquePtr<mozInlineSpellStatus> status =
904 mozInlineSpellStatus::CreateForRange(*this, aRange);
905 return ScheduleSpellCheck(std::move(status));
908 // mozInlineSpellChecker::GetMisspelledWord
910 NS_IMETHODIMP
911 mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, uint32_t aOffset,
912 nsRange** newword) {
913 if (NS_WARN_IF(!aNode)) {
914 return NS_ERROR_INVALID_ARG;
916 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
917 if (NS_WARN_IF(!spellCheckSelection)) {
918 return NS_ERROR_FAILURE;
920 return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
923 // mozInlineSpellChecker::ReplaceWord
925 NS_IMETHODIMP
926 mozInlineSpellChecker::ReplaceWord(nsINode* aNode, uint32_t aOffset,
927 const nsAString& aNewWord) {
928 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
929 return NS_ERROR_FAILURE;
932 RefPtr<nsRange> range;
933 nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
934 NS_ENSURE_SUCCESS(res, res);
936 if (!range) {
937 return NS_OK;
940 // In usual cases, any words shouldn't include line breaks, but technically,
941 // they may include and we need to avoid `HTMLTextAreaElement.value` returns
942 // \r. Therefore, we need to handle it here.
943 nsString newWord(aNewWord);
944 if (mEditorBase->IsTextEditor()) {
945 nsContentUtils::PlatformToDOMLineBreaks(newWord);
948 // Blink dispatches cancelable `beforeinput` event at collecting misspelled
949 // word so that we should allow to dispatch cancelable event.
950 RefPtr<EditorBase> editorBase(mEditorBase);
951 DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
952 newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
953 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
954 return NS_OK;
957 // mozInlineSpellChecker::AddWordToDictionary
959 NS_IMETHODIMP
960 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
961 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
963 nsresult rv = mSpellCheck->AddWordToDictionary(word);
964 NS_ENSURE_SUCCESS(rv, rv);
966 UniquePtr<mozInlineSpellStatus> status =
967 mozInlineSpellStatus::CreateForSelection(*this);
968 return ScheduleSpellCheck(std::move(status));
971 // mozInlineSpellChecker::RemoveWordFromDictionary
973 NS_IMETHODIMP
974 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
975 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
977 nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
978 NS_ENSURE_SUCCESS(rv, rv);
980 UniquePtr<mozInlineSpellStatus> status =
981 mozInlineSpellStatus::CreateForRange(*this, nullptr);
982 return ScheduleSpellCheck(std::move(status));
985 // mozInlineSpellChecker::IgnoreWord
987 NS_IMETHODIMP
988 mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
989 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
991 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
992 NS_ENSURE_SUCCESS(rv, rv);
994 UniquePtr<mozInlineSpellStatus> status =
995 mozInlineSpellStatus::CreateForSelection(*this);
996 return ScheduleSpellCheck(std::move(status));
999 // mozInlineSpellChecker::IgnoreWords
1001 NS_IMETHODIMP
1002 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
1003 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1005 // add each word to the ignore list and then recheck the document
1006 for (auto& word : aWordsToIgnore) {
1007 mSpellCheck->IgnoreWordAllOccurrences(word);
1010 UniquePtr<mozInlineSpellStatus> status =
1011 mozInlineSpellStatus::CreateForSelection(*this);
1012 return ScheduleSpellCheck(std::move(status));
1015 // mozInlineSpellChecker::MakeSpellCheckRange
1017 // Given begin and end positions, this function constructs a range as
1018 // required for ScheduleSpellCheck. If the start and end nodes are nullptr,
1019 // then the entire range will be selected, and you can supply -1 as the
1020 // offset to the end range to select all of that node.
1022 // If the resulting range would be empty, nullptr is put into *aRange and the
1023 // function succeeds.
1025 nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
1026 int32_t aStartOffset,
1027 nsINode* aEndNode,
1028 int32_t aEndOffset,
1029 nsRange** aRange) const {
1030 nsresult rv;
1031 *aRange = nullptr;
1033 if (NS_WARN_IF(!mEditorBase)) {
1034 return NS_ERROR_FAILURE;
1037 RefPtr<Document> doc = mEditorBase->GetDocument();
1038 if (NS_WARN_IF(!doc)) {
1039 return NS_ERROR_FAILURE;
1042 RefPtr<nsRange> range = nsRange::Create(doc);
1044 // possibly use full range of the editor
1045 if (!aStartNode || !aEndNode) {
1046 Element* domRootElement = mEditorBase->GetRoot();
1047 if (NS_WARN_IF(!domRootElement)) {
1048 return NS_ERROR_FAILURE;
1050 aStartNode = aEndNode = domRootElement;
1051 aStartOffset = 0;
1052 aEndOffset = -1;
1055 if (aEndOffset == -1) {
1056 // It's hard to say whether it's better to just do nsINode::GetChildCount or
1057 // get the ChildNodes() and then its length. The latter is faster if we
1058 // keep going through this code for the same nodes (because it caches the
1059 // length). The former is faster if we keep getting different nodes here...
1061 // Let's do the thing which can't end up with bad O(N^2) behavior.
1062 aEndOffset = aEndNode->ChildNodes()->Length();
1065 // sometimes we are are requested to check an empty range (possibly an empty
1066 // document). This will result in assertions later.
1067 if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
1069 if (aEndOffset) {
1070 rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
1071 if (NS_WARN_IF(NS_FAILED(rv))) {
1072 return rv;
1074 } else {
1075 rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
1076 RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
1077 if (NS_WARN_IF(NS_FAILED(rv))) {
1078 return rv;
1082 range.swap(*aRange);
1083 return NS_OK;
1086 nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
1087 int32_t aStartOffset,
1088 nsINode* aEndNode,
1089 int32_t aEndOffset) {
1090 RefPtr<nsRange> range;
1091 nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
1092 aEndOffset, getter_AddRefs(range));
1093 NS_ENSURE_SUCCESS(rv, rv);
1095 if (!range) return NS_OK; // range is empty: nothing to do
1097 UniquePtr<mozInlineSpellStatus> status =
1098 mozInlineSpellStatus::CreateForRange(*this, range);
1099 return ScheduleSpellCheck(std::move(status));
1102 // mozInlineSpellChecker::ShouldSpellCheckNode
1104 // There are certain conditions when we don't want to spell check a node. In
1105 // particular quotations, moz signatures, etc. This routine returns false
1106 // for these cases.
1108 // static
1109 bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
1110 nsINode* aNode) {
1111 MOZ_ASSERT(aNode);
1112 if (!aNode->IsContent()) return false;
1114 nsIContent* content = aNode->AsContent();
1116 if (aEditorBase->IsMailEditor()) {
1117 nsIContent* parent = content->GetParent();
1118 while (parent) {
1119 if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
1120 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1121 nsGkAtoms::cite, eIgnoreCase)) {
1122 return false;
1124 if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
1125 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1126 nsGkAtoms::mozsignature,
1127 eIgnoreCase)) {
1128 return false;
1130 if (parent->IsHTMLElement(nsGkAtoms::div) &&
1131 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1132 nsGkAtoms::mozfwcontainer,
1133 eIgnoreCase)) {
1134 return false;
1137 parent = parent->GetParent();
1139 } else {
1140 // Check spelling only if the node is editable, and GetSpellcheck() is true
1141 // on the nearest HTMLElement ancestor.
1142 if (!content->IsEditable()) {
1143 return false;
1146 // Make sure that we can always turn on spell checking for inputs/textareas.
1147 // Note that because of the previous check, at this point we know that the
1148 // node is editable.
1149 if (content->IsInNativeAnonymousSubtree()) {
1150 nsIContent* node = content->GetParent();
1151 while (node && node->IsInNativeAnonymousSubtree()) {
1152 node = node->GetParent();
1154 if (node && node->IsTextControlElement()) {
1155 return true;
1159 // Get HTML element ancestor (might be aNode itself, although probably that
1160 // has to be a text node in real life here)
1161 nsIContent* parent = content;
1162 while (!parent->IsHTMLElement()) {
1163 parent = parent->GetParent();
1164 if (!parent) {
1165 return true;
1169 // See if it's spellcheckable
1170 return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
1173 return true;
1176 // mozInlineSpellChecker::ScheduleSpellCheck
1178 // This is called by code to do the actual spellchecking. We will set up
1179 // the proper structures for calls to DoSpellCheck.
1181 nsresult mozInlineSpellChecker::ScheduleSpellCheck(
1182 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1183 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1184 ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
1185 mFullSpellCheckScheduled));
1187 if (mFullSpellCheckScheduled) {
1188 // Just ignore this; we're going to spell-check everything anyway
1189 return NS_OK;
1191 // Cache the value because we are going to move aStatus's ownership to
1192 // the new created mozInlineSpellResume instance.
1193 bool isFullSpellCheck = aStatus->IsFullSpellCheck();
1195 RefPtr<mozInlineSpellResume> resume =
1196 new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
1197 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1199 nsresult rv = resume->Post();
1200 if (NS_SUCCEEDED(rv)) {
1201 if (isFullSpellCheck) {
1202 // We're going to check everything. Suppress further spell-check attempts
1203 // until that happens.
1204 mFullSpellCheckScheduled = true;
1206 ChangeNumPendingSpellChecks(1);
1208 return rv;
1211 // mozInlineSpellChecker::DoSpellCheckSelection
1213 // Called to re-check all misspelled words. We iterate over all ranges in
1214 // the selection and call DoSpellCheck on them. This is used when a word
1215 // is ignored or added to the dictionary: all instances of that word should
1216 // be removed from the selection.
1218 // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1219 // Typically, checking this small amount of text is relatively fast, but
1220 // for large numbers of words, a lag may be noticeable.
1222 nsresult mozInlineSpellChecker::DoSpellCheckSelection(
1223 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
1224 nsresult rv;
1226 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1227 mNumWordsInSpellSelection = 0;
1229 // Since we could be modifying the ranges for the spellCheckSelection while
1230 // looping on the spell check selection, keep a separate array of range
1231 // elements inside the selection
1232 nsTArray<RefPtr<nsRange>> ranges;
1234 const uint32_t rangeCount = aSpellCheckSelection->RangeCount();
1235 for (const uint32_t idx : IntegerRange(rangeCount)) {
1236 MOZ_ASSERT(aSpellCheckSelection->RangeCount() == rangeCount);
1237 nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
1238 MOZ_ASSERT(range);
1239 if (MOZ_LIKELY(range)) {
1240 ranges.AppendElement(range);
1244 // We have saved the ranges above. Clearing the spellcheck selection here
1245 // isn't necessary (rechecking each word will modify it as necessary) but
1246 // provides better performance. By ensuring that no ranges need to be
1247 // removed in DoSpellCheck, we can save checking range inclusion which is
1248 // slow.
1249 aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
1251 // We use this state object for all calls, and just update its range. Note
1252 // that we don't need to call FinishInit since we will be filling in the
1253 // necessary information.
1254 UniquePtr<mozInlineSpellStatus> status =
1255 mozInlineSpellStatus::CreateForRange(*this, nullptr);
1257 bool doneChecking;
1258 for (uint32_t idx : IntegerRange(rangeCount)) {
1259 // We can consider this word as "added" since we know it has no spell
1260 // check range over it that needs to be deleted. All the old ranges
1261 // were cleared above. We also need to clear the word count so that we
1262 // check all words instead of stopping early.
1263 status->mRange = ranges[idx];
1264 rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
1265 NS_ENSURE_SUCCESS(rv, rv);
1266 MOZ_ASSERT(
1267 doneChecking,
1268 "We gave the spellchecker one word, but it didn't finish checking?!?!");
1271 return NS_OK;
1274 class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
1275 public:
1277 * @param aStatus must be non-nullptr.
1279 SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
1280 mozInlineSpellWordUtil& aWordUtil,
1281 mozilla::dom::Selection& aSpellCheckSelection,
1282 const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
1283 bool& aDoneChecking)
1284 : mInlineSpellChecker{aInlineSpellChecker},
1285 mWordUtil{aWordUtil},
1286 mSpellCheckSelection{aSpellCheckSelection},
1287 mStatus{aStatus},
1288 mDoneChecking{aDoneChecking} {
1289 MOZ_ASSERT(aStatus);
1292 [[nodiscard]] nsresult Execute();
1294 private:
1295 // Creates an async request to check the words and update the ranges for the
1296 // misspellings.
1298 // @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
1299 // @param aOldRangesForSomeWords ranges from previous spellcheckings which
1300 // might need to be removed. Its length might
1301 // differ from `aWords.Length()`.
1302 // @param aNodeOffsetRangesForWords One range for each word in aWords. So
1303 // `aNodeOffsetRangesForWords.Length() ==
1304 // aWords.Length()`.
1305 void CheckWordsAndUpdateRangesForMisspellings(
1306 const nsTArray<nsString>& aWords,
1307 nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1308 nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);
1310 void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);
1312 bool ShouldSpellCheckRange(const nsRange& aRange) const;
1314 bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;
1316 mozInlineSpellChecker& mInlineSpellChecker;
1317 mozInlineSpellWordUtil& mWordUtil;
1318 mozilla::dom::Selection& mSpellCheckSelection;
1319 const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
1320 bool& mDoneChecking;
1323 bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
1324 const nsRange& aRange) const {
1325 if (aRange.Collapsed()) {
1326 return false;
1329 nsINode* beginNode = aRange.GetStartContainer();
1330 nsINode* endNode = aRange.GetEndContainer();
1332 const nsINode* rootNode = mWordUtil.GetRootNode();
1333 return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
1334 beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
1335 endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
1338 bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
1339 const nsINode& aNode, int32_t aOffset) const {
1340 ErrorResult erv;
1341 return mStatus->GetNoCheckRange() &&
1342 mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
1345 void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
1346 const nsTArray<RefPtr<nsRange>>& aRanges) {
1347 for (uint32_t i = 0; i < aRanges.Length(); i++) {
1348 mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
1352 // mozInlineSpellChecker::SpellCheckerSlice::Execute
1354 // This function checks words intersecting the given range, excluding those
1355 // inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
1356 // will have any spell selection removed (this is used to hide the
1357 // underlining for the word that the caret is in). aNoCheckRange should be
1358 // on word boundaries.
1360 // mResume->mCreatedRange is a possibly nullptr range of new text that was
1361 // inserted. Inside this range, we don't bother to check whether things are
1362 // inside the spellcheck selection, which speeds up large paste operations
1363 // considerably.
1365 // Normal case when editing text by typing
1366 // h e l l o w o r k d h o w a r e y o u
1367 // ^ caret
1368 // [-------] mRange
1369 // [-------] mNoCheckRange
1370 // -> does nothing (range is the same as the no check range)
1372 // Case when pasting:
1373 // [---------- pasted text ----------]
1374 // h e l l o w o r k d h o w a r e y o u
1375 // ^ caret
1376 // [---] aNoCheckRange
1377 // -> recheck all words in range except those in aNoCheckRange
1379 // If checking is complete, *aDoneChecking will be set. If there is more
1380 // but we ran out of time, this will be false and the range will be
1381 // updated with the stuff that still needs checking.
1383 nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
1384 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1386 mDoneChecking = true;
1388 if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
1389 return NS_ERROR_NOT_INITIALIZED;
1392 if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
1393 return NS_OK;
1396 // get the editor for ShouldSpellCheckNode, this may fail in reasonable
1397 // circumstances since the editor could have gone away
1398 RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
1399 if (!editorBase || editorBase->Destroyed()) {
1400 return NS_ERROR_FAILURE;
1403 if (!ShouldSpellCheckRange(*mStatus->mRange)) {
1404 // Just bail out and don't try to spell-check this
1405 return NS_OK;
1408 // see if the selection has any ranges, if not, then we can optimize checking
1409 // range inclusion later (we have no ranges when we are initially checking or
1410 // when there are no misspelled words yet).
1411 const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();
1413 // set the starting DOM position to be the beginning of our range
1414 if (nsresult rv = mWordUtil.SetPositionAndEnd(
1415 mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
1416 mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
1417 NS_FAILED(rv)) {
1418 // Just bail out and don't try to spell-check this
1419 return NS_OK;
1422 // aWordUtil.SetPosition flushes pending notifications, check editor again.
1423 if (!mInlineSpellChecker.mEditorBase) {
1424 return NS_ERROR_FAILURE;
1427 int32_t wordsChecked = 0;
1428 PRTime beginTime = PR_Now();
1430 nsTArray<nsString> normalizedWords;
1431 nsTArray<RefPtr<nsRange>> oldRangesToRemove;
1432 nsTArray<NodeOffsetRange> checkRanges;
1433 mozInlineSpellWordUtil::Word word;
1434 static const size_t requestChunkSize =
1435 INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
1437 while (mWordUtil.GetNextWord(word)) {
1438 // get the range for the current word.
1439 nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
1440 nsINode* const endNode = word.mNodeOffsetRange.End().Node();
1441 // TODO: Make them `uint32_t`
1442 const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
1443 const int32_t endOffset = word.mNodeOffsetRange.End().Offset();
1445 // see if we've done enough words in this round and run out of time.
1446 if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
1447 PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
1448 // stop checking, our time limit has been exceeded.
1449 MOZ_LOG(
1450 sInlineSpellCheckerLog, LogLevel::Verbose,
1451 ("%s: we have run out of time, schedule next round.", __FUNCTION__));
1453 CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1454 std::move(oldRangesToRemove),
1455 std::move(checkRanges));
1457 // move the range to encompass the stuff that needs checking.
1458 nsresult rv = mStatus->mRange->SetStart(
1459 beginNode, AssertedCast<uint32_t>(beginOffset));
1460 if (NS_FAILED(rv)) {
1461 // The range might be unhappy because the beginning is after the
1462 // end. This is possible when the requested end was in the middle
1463 // of a word, just ignore this situation and assume we're done.
1464 return NS_OK;
1466 mDoneChecking = false;
1467 return NS_OK;
1470 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1471 ("%s: got word \"%s\"%s", __FUNCTION__,
1472 NS_ConvertUTF16toUTF8(word.mText).get(),
1473 word.mSkipChecking ? " (not checking)" : ""));
1475 // see if there is a spellcheck range that already intersects the word
1476 // and remove it. We only need to remove old ranges, so don't bother if
1477 // there were no ranges when we started out.
1478 if (originalRangeCount > 0) {
1479 ErrorResult erv;
1480 // likewise, if this word is inside new text, we won't bother testing
1481 if (!mStatus->GetCreatedRange() ||
1482 !mStatus->GetCreatedRange()->IsPointInRange(
1483 *beginNode, AssertedCast<uint32_t>(beginOffset), erv)) {
1484 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1485 ("%s: removing ranges for some interval.", __FUNCTION__));
1487 nsTArray<RefPtr<nsRange>> ranges;
1488 mSpellCheckSelection.GetRangesForInterval(
1489 *beginNode, AssertedCast<uint32_t>(beginOffset), *endNode,
1490 AssertedCast<uint32_t>(endOffset), true, ranges, erv);
1491 ENSURE_SUCCESS(erv, erv.StealNSResult());
1492 oldRangesToRemove.AppendElements(std::move(ranges));
1496 // some words are special and don't need checking
1497 if (word.mSkipChecking) {
1498 continue;
1501 // some nodes we don't spellcheck
1502 if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
1503 continue;
1506 // Don't check spelling if we're inside the noCheckRange. This needs to
1507 // be done after we clear any old selection because the excluded word
1508 // might have been previously marked.
1510 // We do a simple check to see if the beginning of our word is in the
1511 // exclusion range. Because the exclusion range is a multiple of a word,
1512 // this is sufficient.
1513 if (IsInNoCheckRange(*beginNode, beginOffset)) {
1514 continue;
1517 // check spelling and add to selection if misspelled
1518 mozInlineSpellWordUtil::NormalizeWord(word.mText);
1519 normalizedWords.AppendElement(word.mText);
1520 checkRanges.AppendElement(word.mNodeOffsetRange);
1521 wordsChecked++;
1522 if (normalizedWords.Length() >= requestChunkSize) {
1523 CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1524 std::move(oldRangesToRemove),
1525 std::move(checkRanges));
1526 normalizedWords.Clear();
1527 oldRangesToRemove = {};
1528 // Set new empty data for spellcheck range in DOM to avoid
1529 // clang-tidy detection.
1530 checkRanges = nsTArray<NodeOffsetRange>();
1534 CheckWordsAndUpdateRangesForMisspellings(
1535 normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));
1537 return NS_OK;
1540 nsresult mozInlineSpellChecker::DoSpellCheck(
1541 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
1542 const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
1543 MOZ_ASSERT(aDoneChecking);
1545 SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
1546 aStatus, *aDoneChecking};
1548 return spellCheckerSlice.Execute();
1551 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
1552 class MOZ_RAII AutoChangeNumPendingSpellChecks final {
1553 public:
1554 explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
1555 int32_t aDelta)
1556 : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
1558 ~AutoChangeNumPendingSpellChecks() {
1559 mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
1562 private:
1563 RefPtr<mozInlineSpellChecker> mSpellChecker;
1564 int32_t mDelta;
1567 void mozInlineSpellChecker::SpellCheckerSlice::
1568 CheckWordsAndUpdateRangesForMisspellings(
1569 const nsTArray<nsString>& aWords,
1570 nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1571 nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
1572 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1573 ("%s: aWords.Length()=%i", __FUNCTION__,
1574 static_cast<int>(aWords.Length())));
1576 MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());
1578 // TODO:
1579 // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
1580 // to remove ranges more efficiently.
1582 if (aWords.IsEmpty()) {
1583 RemoveRanges(aOldRangesForSomeWords);
1584 return;
1587 mInlineSpellChecker.ChangeNumPendingSpellChecks(1);
1589 RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
1590 RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
1591 uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
1592 mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
1593 GetMainThreadSerialEventTarget(), __func__,
1594 [inlineSpellChecker, spellCheckerSelection,
1595 nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
1596 oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
1597 token](const nsTArray<bool>& aIsMisspelled) {
1598 if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1599 // This result is never used
1600 return;
1603 if (!inlineSpellChecker->mEditorBase ||
1604 inlineSpellChecker->mEditorBase->Destroyed()) {
1605 return;
1608 AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);
1610 if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
1611 return;
1614 inlineSpellChecker->UpdateRangesForMisspelledWords(
1615 nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
1616 *spellCheckerSelection);
1618 [inlineSpellChecker, token](nsresult aRv) {
1619 if (!inlineSpellChecker->mEditorBase ||
1620 inlineSpellChecker->mEditorBase->Destroyed()) {
1621 return;
1624 if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1625 // This result is never used
1626 return;
1629 inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
1633 // mozInlineSpellChecker::ResumeCheck
1635 // Called by the resume event when it fires. We will try to pick up where
1636 // the last resume left off.
1638 nsresult mozInlineSpellChecker::ResumeCheck(
1639 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1640 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1642 // Observers should be notified that spell check has ended only after spell
1643 // check is done below, but since there are many early returns in this method
1644 // and the number of pending spell checks must be decremented regardless of
1645 // whether the spell check actually happens, use this RAII object.
1646 AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
1648 if (aStatus->IsFullSpellCheck()) {
1649 // Allow posting new spellcheck resume events from inside
1650 // ResumeCheck, now that we're actually firing.
1651 MOZ_ASSERT(mFullSpellCheckScheduled,
1652 "How could this be false? The full spell check is "
1653 "calling us!!");
1654 mFullSpellCheckScheduled = false;
1657 if (!mSpellCheck) return NS_OK; // spell checking has been turned off
1659 if (!mEditorBase) {
1660 return NS_OK;
1663 Maybe<mozInlineSpellWordUtil> wordUtil{
1664 mozInlineSpellWordUtil::Create(*mEditorBase)};
1665 if (!wordUtil) {
1666 return NS_OK; // editor doesn't like us, don't assert
1669 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
1670 if (NS_WARN_IF(!spellCheckSelection)) {
1671 return NS_ERROR_FAILURE;
1674 nsTArray<nsCString> currentDictionaries;
1675 nsresult rv = mSpellCheck->GetCurrentDictionaries(currentDictionaries);
1676 if (NS_FAILED(rv)) {
1677 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1678 ("%s: no active dictionary.", __FUNCTION__));
1680 // no active dictionary
1681 for (const uint32_t index :
1682 Reversed(IntegerRange(spellCheckSelection->RangeCount()))) {
1683 RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
1684 if (MOZ_LIKELY(checkRange)) {
1685 RemoveRange(spellCheckSelection, checkRange);
1688 return NS_OK;
1691 CleanupRangesInSelection(spellCheckSelection);
1693 rv = aStatus->FinishInitOnEvent(*wordUtil);
1694 NS_ENSURE_SUCCESS(rv, rv);
1695 if (!aStatus->mRange) return NS_OK; // empty range, nothing to do
1697 bool doneChecking = true;
1698 if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
1699 rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
1700 else
1701 rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
1702 NS_ENSURE_SUCCESS(rv, rv);
1704 if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
1705 return rv;
1708 // mozInlineSpellChecker::IsPointInSelection
1710 // Determines if a given (node,offset) point is inside the given
1711 // selection. If so, the specific range of the selection that
1712 // intersects is places in *aRange. (There may be multiple disjoint
1713 // ranges in a selection.)
1715 // If there is no intersection, *aRange will be nullptr.
1717 // static
1718 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
1719 nsINode* aNode,
1720 uint32_t aOffset,
1721 nsRange** aRange) {
1722 *aRange = nullptr;
1724 nsTArray<nsRange*> ranges;
1725 nsresult rv = aSelection.GetDynamicRangesForIntervalArray(
1726 aNode, aOffset, aNode, aOffset, true, &ranges);
1727 NS_ENSURE_SUCCESS(rv, rv);
1729 if (ranges.Length() == 0) return NS_OK; // no matches
1731 // there may be more than one range returned, and we don't know what do
1732 // do with that, so just get the first one
1733 NS_ADDREF(*aRange = ranges[0]);
1734 return NS_OK;
1737 nsresult mozInlineSpellChecker::CleanupRangesInSelection(
1738 Selection* aSelection) {
1739 // integrity check - remove ranges that have collapsed to nothing. This
1740 // can happen if the node containing a highlighted word was removed.
1741 if (!aSelection) return NS_ERROR_FAILURE;
1743 // TODO: Rewrite this with reversed ranged-loop, it might make this simpler.
1744 int64_t count = aSelection->RangeCount();
1745 for (int64_t index = 0; index < count; index++) {
1746 nsRange* checkRange = aSelection->GetRangeAt(static_cast<uint32_t>(index));
1747 if (MOZ_LIKELY(checkRange)) {
1748 if (checkRange->Collapsed()) {
1749 RemoveRange(aSelection, checkRange);
1750 index--;
1751 count--;
1756 return NS_OK;
1759 // mozInlineSpellChecker::RemoveRange
1761 // For performance reasons, we have an upper bound on the number of word
1762 // ranges in the spell check selection. When removing a range from the
1763 // selection, we need to decrement mNumWordsInSpellSelection
1765 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
1766 nsRange* aRange) {
1767 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1769 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1770 NS_ENSURE_ARG_POINTER(aRange);
1772 ErrorResult rv;
1773 RefPtr<nsRange> range{aRange};
1774 RefPtr<Selection> selection{aSpellCheckSelection};
1775 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
1776 if (!rv.Failed()) {
1777 if (mNumWordsInSpellSelection) {
1778 mNumWordsInSpellSelection--;
1780 #ifdef ACCESSIBILITY
1781 if (nsAccessibilityService* accService = GetAccService()) {
1782 accService->SpellCheckRangeChanged(*aRange);
1784 #endif
1787 return rv.StealNSResult();
1790 struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange {
1791 static bool Equals(const RefPtr<nsRange>& aRange,
1792 const NodeOffsetRange& aNodeOffsetRange) {
1793 return aNodeOffsetRange == *aRange;
1797 void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
1798 const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords,
1799 const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords,
1800 const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) {
1801 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
1803 MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length());
1805 // When the spellchecker checks text containing words separated by "/", it may
1806 // happen that some words checked in one timeslice, are checked again in a
1807 // following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo"
1808 // and "baz" are checked in one timeslice and two ranges are added for them.
1809 // In the following timeslice "foo" and "baz" are checked again but since
1810 // their corresponding ranges are already in the spellcheck-Selection
1811 // they don't have to be added again and since "foo" and "baz" still contain
1812 // spelling mistakes, they don't have to be removed.
1814 // In this case, it's more efficient to keep the existing ranges.
1816 AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1817 oldRangesMarkedForRemoval;
1818 for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) {
1819 oldRangesMarkedForRemoval.AppendElement(true);
1822 AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1823 nodeOffsetRangesMarkedForAdding;
1824 for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) {
1825 nodeOffsetRangesMarkedForAdding.AppendElement(false);
1828 for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
1829 if (!aIsMisspelled[i]) {
1830 continue;
1833 const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i];
1834 const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
1835 nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
1836 if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
1837 aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
1838 aSpellCheckerSelection)) {
1839 /** TODO: warn in case the old range doesn't
1840 belong to the selection. This is not critical,
1841 because other code can always remove them
1842 before the actual spellchecking happens. */
1843 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1844 ("%s: reusing old range.", __FUNCTION__));
1846 oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false;
1847 } else {
1848 nodeOffsetRangesMarkedForAdding[i] = true;
1852 for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) {
1853 if (oldRangesMarkedForRemoval[i]) {
1854 RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]);
1858 // Add ranges after removing the marked old ones, so that the Selection can
1859 // become full again.
1860 for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) {
1861 if (nodeOffsetRangesMarkedForAdding[i]) {
1862 RefPtr<nsRange> wordRange =
1863 mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]);
1864 // If we somehow can't make a range for this word, just ignore
1865 // it.
1866 if (wordRange) {
1867 AddRange(&aSpellCheckerSelection, wordRange);
1873 // mozInlineSpellChecker::AddRange
1875 // For performance reasons, we have an upper bound on the number of word
1876 // ranges we'll add to the spell check selection. Once we reach that upper
1877 // bound, stop adding the ranges
1879 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
1880 nsRange* aRange) {
1881 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1882 NS_ENSURE_ARG_POINTER(aRange);
1884 nsresult rv = NS_OK;
1886 if (!IsSpellCheckSelectionFull()) {
1887 IgnoredErrorResult err;
1888 aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
1889 err);
1890 if (err.Failed()) {
1891 rv = err.StealNSResult();
1892 } else {
1893 mNumWordsInSpellSelection++;
1894 #ifdef ACCESSIBILITY
1895 if (nsAccessibilityService* accService = GetAccService()) {
1896 accService->SpellCheckRangeChanged(*aRange);
1898 #endif
1902 return rv;
1905 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
1906 if (NS_WARN_IF(!mEditorBase)) {
1907 return nullptr;
1909 RefPtr<Selection> selection =
1910 mEditorBase->GetSelection(SelectionType::eSpellCheck);
1911 if (!selection) {
1912 return nullptr;
1914 return selection.forget();
1917 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
1918 if (NS_WARN_IF(!mEditorBase)) {
1919 return NS_OK; // XXX Why NS_OK?
1922 // figure out the old caret position based on the current selection
1923 RefPtr<Selection> selection = mEditorBase->GetSelection();
1924 if (NS_WARN_IF(!selection)) {
1925 return NS_ERROR_FAILURE;
1928 mCurrentSelectionAnchorNode = selection->GetFocusNode();
1929 mCurrentSelectionOffset = selection->FocusOffset();
1931 return NS_OK;
1934 // mozInlineSpellChecker::HandleNavigationEvent
1936 // Acts upon mouse clicks and keyboard navigation changes, spell checking
1937 // the previous word if the new navigation location moves us to another
1938 // word.
1940 // This is complicated by the fact that our mouse events are happening after
1941 // selection has been changed to account for the mouse click. But keyboard
1942 // events are happening before the caret selection has changed. Working
1943 // around this by letting keyboard events setting forceWordSpellCheck to
1944 // true. aNewPositionOffset also tries to work around this for the
1945 // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1947 nsresult mozInlineSpellChecker::HandleNavigationEvent(
1948 bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
1949 nsresult rv;
1951 // If we already handled the navigation event and there is no possibility
1952 // anything has changed since then, we don't have to do anything. This
1953 // optimization makes a noticeable difference when you hold down a navigation
1954 // key like Page Down.
1955 if (!mNeedsCheckAfterNavigation) return NS_OK;
1957 nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
1958 uint32_t currentAnchorOffset = mCurrentSelectionOffset;
1960 // now remember the new focus position resulting from the event
1961 rv = SaveCurrentSelectionPosition();
1962 NS_ENSURE_SUCCESS(rv, rv);
1964 bool shouldPost;
1965 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
1966 mozInlineSpellStatus::CreateForNavigation(
1967 *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
1968 currentAnchorOffset, mCurrentSelectionAnchorNode,
1969 mCurrentSelectionOffset, &shouldPost);
1971 if (NS_WARN_IF(res.isErr())) {
1972 return res.unwrapErr();
1975 if (shouldPost) {
1976 rv = ScheduleSpellCheck(res.unwrap());
1977 NS_ENSURE_SUCCESS(rv, rv);
1980 return NS_OK;
1983 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) {
1984 WidgetEvent* widgetEvent = aEvent->WidgetEventPtr();
1985 if (MOZ_UNLIKELY(!widgetEvent)) {
1986 return NS_OK;
1989 switch (widgetEvent->mMessage) {
1990 case eBlur:
1991 OnBlur(*aEvent);
1992 return NS_OK;
1993 case eMouseClick:
1994 OnMouseClick(*aEvent);
1995 return NS_OK;
1996 case eKeyDown:
1997 OnKeyDown(*aEvent);
1998 return NS_OK;
1999 default:
2000 MOZ_ASSERT_UNREACHABLE("You must forgot to handle new event type");
2001 return NS_OK;
2005 void mozInlineSpellChecker::OnBlur(Event& aEvent) {
2006 // force spellcheck on blur, for instance when tabbing out of a textbox
2007 HandleNavigationEvent(true);
2010 void mozInlineSpellChecker::OnMouseClick(Event& aMouseEvent) {
2011 MouseEvent* mouseEvent = aMouseEvent.AsMouseEvent();
2012 if (MOZ_UNLIKELY(!mouseEvent)) {
2013 return;
2016 // ignore any errors from HandleNavigationEvent as we don't want to prevent
2017 // anyone else from seeing this event.
2018 HandleNavigationEvent(mouseEvent->Button() != 0);
2021 void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) {
2022 WidgetKeyboardEvent* widgetKeyboardEvent =
2023 aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
2024 if (MOZ_UNLIKELY(!widgetKeyboardEvent)) {
2025 return;
2028 // we only care about navigation keys that moved selection
2029 switch (widgetKeyboardEvent->mKeyNameIndex) {
2030 case KEY_NAME_INDEX_ArrowRight:
2031 // XXX Does this work with RTL text?
2032 HandleNavigationEvent(false, 1);
2033 return;
2034 case KEY_NAME_INDEX_ArrowLeft:
2035 // XXX Does this work with RTL text?
2036 HandleNavigationEvent(false, -1);
2037 return;
2038 case KEY_NAME_INDEX_ArrowUp:
2039 case KEY_NAME_INDEX_ArrowDown:
2040 case KEY_NAME_INDEX_Home:
2041 case KEY_NAME_INDEX_End:
2042 case KEY_NAME_INDEX_PageDown:
2043 case KEY_NAME_INDEX_PageUp:
2044 HandleNavigationEvent(true /* force a spelling correction */);
2045 return;
2046 default:
2047 return;
2051 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
2052 class UpdateCurrentDictionaryCallback final
2053 : public nsIEditorSpellCheckCallback {
2054 public:
2055 NS_DECL_ISUPPORTS
2057 explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
2058 uint32_t aDisabledAsyncToken)
2059 : mSpellChecker(aSpellChecker),
2060 mDisabledAsyncToken(aDisabledAsyncToken) {}
2062 NS_IMETHOD EditorSpellCheckDone() override {
2063 // Ignore this callback if SetEnableRealTimeSpell(false) was called after
2064 // the UpdateCurrentDictionary call that triggered it.
2065 return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
2066 ? NS_OK
2067 : mSpellChecker->CurrentDictionaryUpdated();
2070 private:
2071 ~UpdateCurrentDictionaryCallback() {}
2073 RefPtr<mozInlineSpellChecker> mSpellChecker;
2074 uint32_t mDisabledAsyncToken;
2076 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
2078 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
2079 // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
2080 // checker is being initialized. Calling UpdateCurrentDictionary on
2081 // mPendingSpellCheck simply queues the dictionary update after the init.
2082 RefPtr<EditorSpellCheck> spellCheck =
2083 mSpellCheck ? mSpellCheck : mPendingSpellCheck;
2084 if (!spellCheck) {
2085 return NS_OK;
2088 RefPtr<UpdateCurrentDictionaryCallback> cb =
2089 new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
2090 NS_ENSURE_STATE(cb);
2091 nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
2092 if (NS_FAILED(rv)) {
2093 cb = nullptr;
2094 return rv;
2096 mNumPendingUpdateCurrentDictionary++;
2097 ChangeNumPendingSpellChecks(1);
2099 return NS_OK;
2102 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
2103 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
2104 mNumPendingUpdateCurrentDictionary--;
2105 MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0,
2106 "CurrentDictionaryUpdated called without corresponding "
2107 "UpdateCurrentDictionary call!");
2108 ChangeNumPendingSpellChecks(-1);
2110 nsresult rv = SpellCheckRange(nullptr);
2111 NS_ENSURE_SUCCESS(rv, rv);
2113 return NS_OK;
2116 NS_IMETHODIMP
2117 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
2118 *aPending = mNumPendingSpellChecks > 0;
2119 return NS_OK;