Bug 1914115 - Ensure UPLOAD_PATH is set before using it for profile logs r=perftest...
[gecko.git] / extensions / spellcheck / src / mozInlineSpellChecker.cpp
blob3db4be07a39b837748913073daf71aca736f3e3d
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 #include "nsCOMPtr.h"
56 #include "nsCRT.h"
57 #include "nsGenericHTMLElement.h"
58 #include "nsRange.h"
59 #include "nsIPrefBranch.h"
60 #include "nsIPrefService.h"
61 #include "nsIRunnable.h"
62 #include "nsServiceManagerUtils.h"
63 #include "nsString.h"
64 #include "nsThreadUtils.h"
65 #include "nsUnicharUtils.h"
66 #include "nsIContent.h"
67 #include "nsIContentInlines.h"
68 #include "nsRange.h"
69 #include "nsContentUtils.h"
70 #include "nsIObserverService.h"
71 #include "prtime.h"
73 using mozilla::LogLevel;
74 using namespace mozilla;
75 using namespace mozilla::dom;
76 using namespace mozilla::ipc;
78 // the number of milliseconds that we will take at once to do spellchecking
79 #define INLINESPELL_CHECK_TIMEOUT 1
81 // The number of words to check before we look at the time to see if
82 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
83 // stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
84 // be too short to a low-end machine.
85 #define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
87 // The maximum number of words to check word via IPC.
88 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
90 // These notifications are broadcast when spell check starts and ends. STARTED
91 // must always be followed by ENDED.
92 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
93 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
95 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
97 static const PRTime kMaxSpellCheckTimeInUsec =
98 INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
100 mozInlineSpellStatus::mozInlineSpellStatus(
101 mozInlineSpellChecker* aSpellChecker, const Operation aOp,
102 RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
103 RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
104 const int32_t aNewNavigationPositionOffset)
105 : mSpellChecker(aSpellChecker),
106 mRange(std::move(aRange)),
107 mOp(aOp),
108 mCreatedRange(std::move(aCreatedRange)),
109 mAnchorRange(std::move(aAnchorRange)),
110 mForceNavigationWordCheck(aForceNavigationWordCheck),
111 mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}
113 // mozInlineSpellStatus::CreateForEditorChange
115 // This is the most complicated case. For changes, we need to compute the
116 // range of stuff that changed based on the old and new caret positions,
117 // as well as use a range possibly provided by the editor (start and end,
118 // which are usually nullptr) to get a range with the union of these.
120 // static
121 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
122 mozInlineSpellStatus::CreateForEditorChange(
123 mozInlineSpellChecker& aSpellChecker, const EditSubAction aEditSubAction,
124 nsINode* aAnchorNode, uint32_t aAnchorOffset, nsINode* aPreviousNode,
125 uint32_t aPreviousOffset, nsINode* aStartNode, uint32_t aStartOffset,
126 nsINode* aEndNode, uint32_t aEndOffset) {
127 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
129 if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
130 return Err(NS_ERROR_FAILURE);
133 bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
134 if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
135 // IME may remove the previous node if it cancels composition when
136 // there is no text around the composition.
137 deleted = !aPreviousNode->IsInComposedDoc();
140 // save the anchor point as a range so we can find the current word later
141 RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
142 aAnchorNode, aAnchorOffset);
143 if (NS_WARN_IF(!anchorRange)) {
144 return Err(NS_ERROR_FAILURE);
147 // Deletes are easy, the range is just the current anchor. We set the range
148 // to check to be empty, FinishInitOnEvent will fill in the range to be
149 // the current word.
150 RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);
152 // On insert save this range: DoSpellCheck optimizes things in this range.
153 // Otherwise, just leave this nullptr.
154 RefPtr<nsRange> createdRange =
155 (aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;
157 UniquePtr<mozInlineSpellStatus> status{
158 /* The constructor is `private`, hence the explicit allocation. */
159 new mozInlineSpellStatus{&aSpellChecker,
160 deleted ? eOpChangeDelete : eOpChange,
161 std::move(range), std::move(createdRange),
162 std::move(anchorRange), false, 0}};
163 if (deleted) {
164 return status;
167 // ...we need to put the start and end in the correct order
168 ErrorResult errorResult;
169 int16_t cmpResult = status->mAnchorRange->ComparePoint(
170 *aPreviousNode, aPreviousOffset, errorResult);
171 if (NS_WARN_IF(errorResult.Failed())) {
172 return Err(errorResult.StealNSResult());
174 nsresult rv;
175 if (cmpResult < 0) {
176 // previous anchor node is before the current anchor
177 rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
178 aAnchorNode, aAnchorOffset);
179 if (NS_WARN_IF(NS_FAILED(rv))) {
180 return Err(rv);
182 } else {
183 // previous anchor node is after (or the same as) the current anchor
184 rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
185 aPreviousNode, aPreviousOffset);
186 if (NS_WARN_IF(NS_FAILED(rv))) {
187 return Err(rv);
191 // if we were given a range, we need to expand our range to encompass it
192 if (aStartNode && aEndNode) {
193 cmpResult =
194 status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
195 if (NS_WARN_IF(errorResult.Failed())) {
196 return Err(errorResult.StealNSResult());
198 if (cmpResult < 0) { // given range starts before
199 rv = status->mRange->SetStart(aStartNode, aStartOffset);
200 if (NS_WARN_IF(NS_FAILED(rv))) {
201 return Err(rv);
205 cmpResult =
206 status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
207 if (NS_WARN_IF(errorResult.Failed())) {
208 return Err(errorResult.StealNSResult());
210 if (cmpResult > 0) { // given range ends after
211 rv = status->mRange->SetEnd(aEndNode, aEndOffset);
212 if (NS_WARN_IF(NS_FAILED(rv))) {
213 return Err(rv);
218 return status;
221 // mozInlineSpellStatus::CreateForNavigation
223 // For navigation events, we just need to store the new and old positions.
225 // In some cases, we detect that we shouldn't check. If this event should
226 // not be processed, *aContinue will be false.
228 // static
229 Result<UniquePtr<mozInlineSpellStatus>, nsresult>
230 mozInlineSpellStatus::CreateForNavigation(
231 mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
232 int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
233 uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
234 uint32_t aNewAnchorOffset, bool* aContinue) {
235 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
237 RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
238 aNewAnchorNode, aNewAnchorOffset);
239 if (NS_WARN_IF(!anchorRange)) {
240 return Err(NS_ERROR_FAILURE);
243 UniquePtr<mozInlineSpellStatus> status{
244 /* The constructor is `private`, hence the explicit allocation. */
245 new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
246 std::move(anchorRange), aForceCheck,
247 aNewPositionOffset}};
249 // get the root node for checking
250 EditorBase* editorBase = status->mSpellChecker->mEditorBase;
251 if (NS_WARN_IF(!editorBase)) {
252 return Err(NS_ERROR_FAILURE);
254 Element* root = editorBase->GetRoot();
255 if (NS_WARN_IF(!root)) {
256 return Err(NS_ERROR_FAILURE);
258 // the anchor node might not be in the DOM anymore, check
259 if (root && aOldAnchorNode &&
260 !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
261 *aContinue = false;
262 return status;
265 status->mOldNavigationAnchorRange =
266 mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
267 aOldAnchorOffset);
268 if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
269 return Err(NS_ERROR_FAILURE);
272 *aContinue = true;
273 return status;
276 // mozInlineSpellStatus::CreateForSelection
278 // It is easy for selections since we always re-check the spellcheck
279 // selection.
281 // static
282 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
283 mozInlineSpellChecker& aSpellChecker) {
284 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
286 UniquePtr<mozInlineSpellStatus> status{
287 /* The constructor is `private`, hence the explicit allocation. */
288 new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
289 nullptr, false, 0}};
290 return status;
293 // mozInlineSpellStatus::CreateForRange
295 // Called to cause the spellcheck of the given range. This will look like
296 // a change operation over the given range.
298 // static
299 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
300 mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
301 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
302 ("%s: range=%p", __FUNCTION__, aRange));
304 UniquePtr<mozInlineSpellStatus> status{
305 /* The constructor is `private`, hence the explicit allocation. */
306 new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
307 nullptr, false, 0}};
309 status->mRange = aRange;
310 return status;
313 // mozInlineSpellStatus::FinishInitOnEvent
315 // Called when the event is triggered to complete initialization that
316 // might require the WordUtil. This calls to the operation-specific
317 // initializer, and also sets the range to be the entire element if it
318 // is nullptr.
320 // Watch out: the range might still be nullptr if there is nothing to do,
321 // the caller will have to check for this.
323 nsresult mozInlineSpellStatus::FinishInitOnEvent(
324 mozInlineSpellWordUtil& aWordUtil) {
325 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
326 ("%s: mRange=%p", __FUNCTION__, mRange.get()));
328 nsresult rv;
329 if (!mRange) {
330 rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
331 getter_AddRefs(mRange));
332 NS_ENSURE_SUCCESS(rv, rv);
335 switch (mOp) {
336 case eOpChange:
337 if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
338 break;
339 case eOpChangeDelete:
340 if (mAnchorRange) {
341 rv = FillNoCheckRangeFromAnchor(aWordUtil);
342 NS_ENSURE_SUCCESS(rv, rv);
344 // Delete events will have no range for the changed text (because it was
345 // deleted), and CreateForEditorChange will set it to nullptr. Here, we
346 // select the entire word to cause any underlining to be removed.
347 mRange = mNoCheckRange;
348 break;
349 case eOpNavigation:
350 return FinishNavigationEvent(aWordUtil);
351 case eOpSelection:
352 // this gets special handling in ResumeCheck
353 break;
354 case eOpResume:
355 // everything should be initialized already in this case
356 break;
357 default:
358 MOZ_ASSERT_UNREACHABLE("Bad operation");
359 return NS_ERROR_NOT_INITIALIZED;
361 return NS_OK;
364 // mozInlineSpellStatus::FinishNavigationEvent
366 // This verifies that we need to check the word at the previous caret
367 // position. Now that we have the word util, we can find the word belonging
368 // to the previous caret position. If the new position is inside that word,
369 // we don't want to do anything. In this case, we'll nullptr out mRange so
370 // that the caller will know not to continue.
372 // Notice that we don't set mNoCheckRange. We check here whether the cursor
373 // is in the word that needs checking, so it isn't necessary. Plus, the
374 // spellchecker isn't guaranteed to only check the given word, and it could
375 // remove the underline from the new word under the cursor.
377 nsresult mozInlineSpellStatus::FinishNavigationEvent(
378 mozInlineSpellWordUtil& aWordUtil) {
379 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
381 RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
382 if (!editorBase) {
383 return NS_ERROR_FAILURE; // editor is gone
386 MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");
388 if (!mOldNavigationAnchorRange->IsPositioned()) {
389 return NS_ERROR_NOT_INITIALIZED;
392 // get the DOM position of the old caret, the range should be collapsed
393 nsCOMPtr<nsINode> oldAnchorNode =
394 mOldNavigationAnchorRange->GetStartContainer();
395 uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
397 // find the word on the old caret position, this is the one that we MAY need
398 // to check
399 RefPtr<nsRange> oldWord;
400 nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
401 static_cast<int32_t>(oldAnchorOffset),
402 getter_AddRefs(oldWord));
403 NS_ENSURE_SUCCESS(rv, rv);
405 // aWordUtil.GetRangeForWord flushes pending notifications, check editor
406 // again.
407 if (!mSpellChecker->mEditorBase) {
408 return NS_ERROR_FAILURE; // editor is gone
411 // get the DOM position of the new caret, the range should be collapsed
412 nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
413 uint32_t newAnchorOffset = mAnchorRange->StartOffset();
415 // see if the new cursor position is in the word of the old cursor position
416 bool isInRange = false;
417 if (!mForceNavigationWordCheck) {
418 ErrorResult err;
419 isInRange = oldWord->IsPointInRange(
420 *newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
421 if (NS_WARN_IF(err.Failed())) {
422 return err.StealNSResult();
426 if (isInRange) {
427 // caller should give up
428 mRange = nullptr;
429 } else {
430 // check the old word
431 mRange = oldWord;
433 // Once we've spellchecked the current word, we don't need to spellcheck
434 // for any more navigation events.
435 mSpellChecker->mNeedsCheckAfterNavigation = false;
437 return NS_OK;
440 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
442 // Given the mAnchorRange object, computes the range of the word it is on
443 // (if any) and fills that range into mNoCheckRange. This is used for
444 // change and navigation events to know which word we should skip spell
445 // checking on
447 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
448 mozInlineSpellWordUtil& aWordUtil) {
449 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
451 if (!mAnchorRange->IsPositioned()) {
452 return NS_ERROR_NOT_INITIALIZED;
454 nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
455 uint32_t anchorOffset = mAnchorRange->StartOffset();
456 return aWordUtil.GetRangeForWord(anchorNode,
457 static_cast<int32_t>(anchorOffset),
458 getter_AddRefs(mNoCheckRange));
461 // mozInlineSpellStatus::GetDocument
463 // Returns the Document object for the document for the
464 // current spellchecker.
466 Document* mozInlineSpellStatus::GetDocument() const {
467 if (!mSpellChecker->mEditorBase) {
468 return nullptr;
471 return mSpellChecker->mEditorBase->GetDocument();
474 // mozInlineSpellStatus::PositionToCollapsedRange
476 // Converts a given DOM position to a collapsed range covering that
477 // position. We use ranges to store DOM positions becuase they stay
478 // updated as the DOM is changed.
480 // static
481 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
482 nsINode* aNode, uint32_t aOffset) {
483 if (NS_WARN_IF(!aNode)) {
484 return nullptr;
486 IgnoredErrorResult ignoredError;
487 RefPtr<nsRange> range =
488 nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
489 NS_WARNING_ASSERTION(!ignoredError.Failed(),
490 "Creating collapsed range failed");
491 return range.forget();
494 // mozInlineSpellResume
496 class mozInlineSpellResume : public Runnable {
497 public:
498 mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
499 uint32_t aDisabledAsyncToken)
500 : Runnable("mozInlineSpellResume"),
501 mDisabledAsyncToken(aDisabledAsyncToken),
502 mStatus(std::move(aStatus)) {}
504 nsresult Post() {
505 nsCOMPtr<nsIRunnable> runnable(this);
506 return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
507 EventQueuePriority::Idle);
510 NS_IMETHOD Run() override {
511 // Discard the resumption if the spell checker was disabled after the
512 // resumption was scheduled.
513 if (mDisabledAsyncToken ==
514 mStatus->mSpellChecker->GetDisabledAsyncToken()) {
515 mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
517 return NS_OK;
520 private:
521 uint32_t mDisabledAsyncToken;
522 UniquePtr<mozInlineSpellStatus> mStatus;
525 // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
526 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
527 ~InitEditorSpellCheckCallback() {}
529 public:
530 NS_DECL_ISUPPORTS
532 explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
533 : mSpellChecker(aSpellChecker) {}
535 NS_IMETHOD EditorSpellCheckDone() override {
536 return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
539 void Cancel() { mSpellChecker = nullptr; }
541 private:
542 RefPtr<mozInlineSpellChecker> mSpellChecker;
544 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
546 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
547 NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
548 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
549 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
550 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
551 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
552 NS_INTERFACE_MAP_END
554 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
555 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
557 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
558 mCurrentSelectionAnchorNode)
560 mozInlineSpellChecker::SpellCheckingState
561 mozInlineSpellChecker::gCanEnableSpellChecking =
562 mozInlineSpellChecker::SpellCheck_Uninitialized;
564 mozInlineSpellChecker::mozInlineSpellChecker()
565 : mNumWordsInSpellSelection(0),
566 mMaxNumWordsInSpellSelection(
567 StaticPrefs::extensions_spellcheck_inline_max_misspellings()),
568 mNumPendingSpellChecks(0),
569 mNumPendingUpdateCurrentDictionary(0),
570 mDisabledAsyncToken(0),
571 mNeedsCheckAfterNavigation(false),
572 mFullSpellCheckScheduled(false),
573 mIsListeningToEditSubActions(false) {}
575 mozInlineSpellChecker::~mozInlineSpellChecker() {}
577 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
578 return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
581 NS_IMETHODIMP
582 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
583 *aSpellCheck = mSpellCheck;
584 NS_IF_ADDREF(*aSpellCheck);
585 return NS_OK;
588 NS_IMETHODIMP
589 mozInlineSpellChecker::Init(nsIEditor* aEditor) {
590 mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
591 return NS_OK;
594 // mozInlineSpellChecker::Cleanup
596 // Called by the editor when the editor is going away. This is important
597 // because we remove listeners. We do NOT clean up anything else in this
598 // function, because it can get called while DoSpellCheck is running!
600 // Getting the style information there can cause DOM notifications to be
601 // flushed, which can cause editors to go away which will bring us here.
602 // We can not do anything that will cause DoSpellCheck to freak out.
604 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
605 mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
606 mNumWordsInSpellSelection = 0;
607 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
608 nsresult rv = NS_OK;
609 if (!spellCheckSelection) {
610 // Ensure we still unregister event listeners (but return a failure code)
611 UnregisterEventListeners();
612 rv = NS_ERROR_FAILURE;
613 } else {
614 if (!aDestroyingFrames) {
615 spellCheckSelection->RemoveAllRanges(IgnoreErrors());
618 rv = UnregisterEventListeners();
621 // Notify ENDED observers now. If we wait to notify as we normally do when
622 // these async operations finish, then in the meantime the editor may create
623 // another inline spell checker and cause more STARTED and ENDED
624 // notifications to be broadcast. Interleaved notifications for the same
625 // editor but different inline spell checkers could easily confuse
626 // observers. They may receive two consecutive STARTED notifications for
627 // example, which we guarantee will not happen.
629 RefPtr<EditorBase> editorBase = std::move(mEditorBase);
630 if (mPendingSpellCheck) {
631 // Cancel the pending editor spell checker initialization.
632 mPendingSpellCheck = nullptr;
633 mPendingInitEditorSpellCheckCallback->Cancel();
634 mPendingInitEditorSpellCheckCallback = nullptr;
635 ChangeNumPendingSpellChecks(-1, editorBase);
638 // Increment this token so that pending UpdateCurrentDictionary calls and
639 // scheduled spell checks are discarded when they finish.
640 mDisabledAsyncToken++;
642 if (mNumPendingUpdateCurrentDictionary > 0) {
643 // Account for pending UpdateCurrentDictionary calls.
644 ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
645 editorBase);
646 mNumPendingUpdateCurrentDictionary = 0;
648 if (mNumPendingSpellChecks > 0) {
649 // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
650 // pending scheduled spell checks.
651 ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
654 mFullSpellCheckScheduled = false;
656 return rv;
659 // mozInlineSpellChecker::CanEnableInlineSpellChecking
661 // This function can be called to see if it seems likely that we can enable
662 // spellchecking before actually creating the InlineSpellChecking objects.
664 // The problem is that we can't get the dictionary list without actually
665 // creating a whole bunch of spellchecking objects. This function tries to
666 // do that and caches the result so we don't have to keep allocating those
667 // objects if there are no dictionaries or spellchecking.
669 // Whenever dictionaries are added or removed at runtime, this value must be
670 // updated before an observer notification is sent out about the change, to
671 // avoid editors getting a wrong cached result.
673 bool // static
674 mozInlineSpellChecker::CanEnableInlineSpellChecking() {
675 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
676 gCanEnableSpellChecking = SpellCheck_NotAvailable;
678 nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
680 bool canSpellCheck = false;
681 nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
682 NS_ENSURE_SUCCESS(rv, false);
684 if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
686 return (gCanEnableSpellChecking == SpellCheck_Available);
689 void // static
690 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
691 gCanEnableSpellChecking = SpellCheck_Uninitialized;
694 // mozInlineSpellChecker::RegisterEventListeners
696 // The inline spell checker listens to mouse events and keyboard navigation
697 // events.
699 nsresult mozInlineSpellChecker::RegisterEventListeners() {
700 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
701 return NS_ERROR_FAILURE;
704 StartToListenToEditSubActions();
706 RefPtr<Document> doc = mEditorBase->GetDocument();
707 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
708 return NS_ERROR_FAILURE;
710 EventListenerManager* eventListenerManager =
711 doc->GetOrCreateListenerManager();
712 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
713 return NS_ERROR_FAILURE;
715 eventListenerManager->AddEventListenerByType(
716 this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
717 eventListenerManager->AddEventListenerByType(
718 this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
719 eventListenerManager->AddEventListenerByType(
720 this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
721 return NS_OK;
724 // mozInlineSpellChecker::UnregisterEventListeners
726 nsresult mozInlineSpellChecker::UnregisterEventListeners() {
727 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
728 return NS_ERROR_FAILURE;
731 EndListeningToEditSubActions();
733 RefPtr<Document> doc = mEditorBase->GetDocument();
734 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
735 return NS_ERROR_FAILURE;
737 EventListenerManager* eventListenerManager =
738 doc->GetOrCreateListenerManager();
739 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
740 return NS_ERROR_FAILURE;
742 eventListenerManager->RemoveEventListenerByType(
743 this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
744 eventListenerManager->RemoveEventListenerByType(
745 this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
746 eventListenerManager->RemoveEventListenerByType(
747 this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
748 return NS_OK;
751 // mozInlineSpellChecker::GetEnableRealTimeSpell
753 NS_IMETHODIMP
754 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
755 NS_ENSURE_ARG_POINTER(aEnabled);
756 *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
757 return NS_OK;
760 // mozInlineSpellChecker::SetEnableRealTimeSpell
762 NS_IMETHODIMP
763 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
764 if (!aEnabled) {
765 mSpellCheck = nullptr;
766 return Cleanup(false);
769 if (mSpellCheck) {
770 // spellcheck the current contents. SpellCheckRange doesn't supply a created
771 // range to DoSpellCheck, which in our case is the entire range. But this
772 // optimization doesn't matter because there is nothing in the spellcheck
773 // selection when starting, which triggers a better optimization.
774 return SpellCheckRange(nullptr);
777 if (mPendingSpellCheck) {
778 // The editor spell checker is already being initialized.
779 return NS_OK;
782 mPendingSpellCheck = new EditorSpellCheck();
783 mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
785 mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
786 nsresult rv = mPendingSpellCheck->InitSpellChecker(
787 mEditorBase, false, mPendingInitEditorSpellCheckCallback);
788 if (NS_FAILED(rv)) {
789 mPendingSpellCheck = nullptr;
790 mPendingInitEditorSpellCheckCallback = nullptr;
791 NS_ENSURE_SUCCESS(rv, rv);
794 ChangeNumPendingSpellChecks(1);
796 return NS_OK;
799 // Called when nsIEditorSpellCheck::InitSpellChecker completes.
800 nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
801 MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");
803 // spell checking is enabled, register our event listeners to track navigation
804 RegisterEventListeners();
806 mSpellCheck = mPendingSpellCheck;
807 mPendingSpellCheck = nullptr;
808 mPendingInitEditorSpellCheckCallback = nullptr;
809 ChangeNumPendingSpellChecks(-1);
811 // spellcheck the current contents. SpellCheckRange doesn't supply a created
812 // range to DoSpellCheck, which in our case is the entire range. But this
813 // optimization doesn't matter because there is nothing in the spellcheck
814 // selection when starting, which triggers a better optimization.
815 return SpellCheckRange(nullptr);
818 // Changes the number of pending spell checks by the given delta. If the number
819 // becomes zero or nonzero, observers are notified. See NotifyObservers for
820 // info on the aEditor parameter.
821 void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
822 int32_t aDelta, EditorBase* aEditorBase) {
823 int8_t oldNumPending = mNumPendingSpellChecks;
824 mNumPendingSpellChecks += aDelta;
825 MOZ_ASSERT(mNumPendingSpellChecks >= 0,
826 "Unbalanced ChangeNumPendingSpellChecks calls!");
827 if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
828 NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
829 } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
830 NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
834 // Broadcasts the given topic to observers. aEditor is passed to observers if
835 // nonnull; otherwise mEditorBase is passed.
836 void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
837 EditorBase* aEditorBase) {
838 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
839 if (!os) return;
840 // XXX Do we need to grab the editor here? If it's necessary, each observer
841 // should do it instead.
842 RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
843 os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
844 nullptr);
847 // mozInlineSpellChecker::SpellCheckAfterEditorChange
849 // Called by the editor when nearly anything happens to change the content.
851 // The start and end positions specify a range for the thing that happened,
852 // but these are usually nullptr, even when you'd think they would be useful
853 // because you want the range (for example, pasting). We ignore them in
854 // this case.
856 nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
857 EditSubAction aEditSubAction, Selection& aSelection,
858 nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
859 nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
860 uint32_t aEndOffset) {
861 nsresult rv;
862 if (!mSpellCheck) return NS_OK; // disabling spell checking is not an error
864 // this means something has changed, and we never check the current word,
865 // therefore, we should spellcheck for subsequent caret navigations
866 mNeedsCheckAfterNavigation = true;
868 // the anchor node is the position of the caret
869 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
870 mozInlineSpellStatus::CreateForEditorChange(
871 *this, aEditSubAction, aSelection.GetAnchorNode(),
872 aSelection.AnchorOffset(), aPreviousSelectedNode,
873 aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
874 aEndOffset);
875 if (NS_WARN_IF(res.isErr())) {
876 return res.unwrapErr();
879 rv = ScheduleSpellCheck(res.unwrap());
880 NS_ENSURE_SUCCESS(rv, rv);
882 // remember the current caret position after every change
883 SaveCurrentSelectionPosition();
884 return NS_OK;
887 // mozInlineSpellChecker::SpellCheckRange
889 // Spellchecks all the words in the given range.
890 // Supply a nullptr range and this will check the entire editor.
892 nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
893 if (!mSpellCheck) {
894 NS_WARNING_ASSERTION(
895 mPendingSpellCheck,
896 "Trying to spellcheck, but checking seems to be disabled");
897 return NS_ERROR_NOT_INITIALIZED;
900 UniquePtr<mozInlineSpellStatus> status =
901 mozInlineSpellStatus::CreateForRange(*this, aRange);
902 return ScheduleSpellCheck(std::move(status));
905 // mozInlineSpellChecker::GetMisspelledWord
907 NS_IMETHODIMP
908 mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, uint32_t aOffset,
909 nsRange** newword) {
910 if (NS_WARN_IF(!aNode)) {
911 return NS_ERROR_INVALID_ARG;
913 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
914 if (NS_WARN_IF(!spellCheckSelection)) {
915 return NS_ERROR_FAILURE;
917 return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
920 // mozInlineSpellChecker::ReplaceWord
922 NS_IMETHODIMP
923 mozInlineSpellChecker::ReplaceWord(nsINode* aNode, uint32_t aOffset,
924 const nsAString& aNewWord) {
925 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
926 return NS_ERROR_FAILURE;
929 RefPtr<nsRange> range;
930 nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
931 NS_ENSURE_SUCCESS(res, res);
933 if (!range) {
934 return NS_OK;
937 // In usual cases, any words shouldn't include line breaks, but technically,
938 // they may include and we need to avoid `HTMLTextAreaElement.value` returns
939 // \r. Therefore, we need to handle it here.
940 nsString newWord(aNewWord);
941 if (mEditorBase->IsTextEditor()) {
942 nsContentUtils::PlatformToDOMLineBreaks(newWord);
945 // Blink dispatches cancelable `beforeinput` event at collecting misspelled
946 // word so that we should allow to dispatch cancelable event.
947 RefPtr<EditorBase> editorBase(mEditorBase);
948 DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
949 newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
950 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
951 return NS_OK;
954 // mozInlineSpellChecker::AddWordToDictionary
956 NS_IMETHODIMP
957 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
958 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
960 nsresult rv = mSpellCheck->AddWordToDictionary(word);
961 NS_ENSURE_SUCCESS(rv, rv);
963 UniquePtr<mozInlineSpellStatus> status =
964 mozInlineSpellStatus::CreateForSelection(*this);
965 return ScheduleSpellCheck(std::move(status));
968 // mozInlineSpellChecker::RemoveWordFromDictionary
970 NS_IMETHODIMP
971 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
972 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
974 nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
975 NS_ENSURE_SUCCESS(rv, rv);
977 UniquePtr<mozInlineSpellStatus> status =
978 mozInlineSpellStatus::CreateForRange(*this, nullptr);
979 return ScheduleSpellCheck(std::move(status));
982 // mozInlineSpellChecker::IgnoreWord
984 NS_IMETHODIMP
985 mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
986 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
988 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
989 NS_ENSURE_SUCCESS(rv, rv);
991 UniquePtr<mozInlineSpellStatus> status =
992 mozInlineSpellStatus::CreateForSelection(*this);
993 return ScheduleSpellCheck(std::move(status));
996 // mozInlineSpellChecker::IgnoreWords
998 NS_IMETHODIMP
999 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
1000 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1002 // add each word to the ignore list and then recheck the document
1003 for (auto& word : aWordsToIgnore) {
1004 mSpellCheck->IgnoreWordAllOccurrences(word);
1007 UniquePtr<mozInlineSpellStatus> status =
1008 mozInlineSpellStatus::CreateForSelection(*this);
1009 return ScheduleSpellCheck(std::move(status));
1012 // mozInlineSpellChecker::MakeSpellCheckRange
1014 // Given begin and end positions, this function constructs a range as
1015 // required for ScheduleSpellCheck. If the start and end nodes are nullptr,
1016 // then the entire range will be selected, and you can supply -1 as the
1017 // offset to the end range to select all of that node.
1019 // If the resulting range would be empty, nullptr is put into *aRange and the
1020 // function succeeds.
1022 nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
1023 int32_t aStartOffset,
1024 nsINode* aEndNode,
1025 int32_t aEndOffset,
1026 nsRange** aRange) const {
1027 nsresult rv;
1028 *aRange = nullptr;
1030 if (NS_WARN_IF(!mEditorBase)) {
1031 return NS_ERROR_FAILURE;
1034 RefPtr<Document> doc = mEditorBase->GetDocument();
1035 if (NS_WARN_IF(!doc)) {
1036 return NS_ERROR_FAILURE;
1039 RefPtr<nsRange> range = nsRange::Create(doc);
1041 // possibly use full range of the editor
1042 if (!aStartNode || !aEndNode) {
1043 Element* domRootElement = mEditorBase->GetRoot();
1044 if (NS_WARN_IF(!domRootElement)) {
1045 return NS_ERROR_FAILURE;
1047 aStartNode = aEndNode = domRootElement;
1048 aStartOffset = 0;
1049 aEndOffset = -1;
1052 if (aEndOffset == -1) {
1053 // It's hard to say whether it's better to just do nsINode::GetChildCount or
1054 // get the ChildNodes() and then its length. The latter is faster if we
1055 // keep going through this code for the same nodes (because it caches the
1056 // length). The former is faster if we keep getting different nodes here...
1058 // Let's do the thing which can't end up with bad O(N^2) behavior.
1059 aEndOffset = aEndNode->ChildNodes()->Length();
1062 // sometimes we are are requested to check an empty range (possibly an empty
1063 // document). This will result in assertions later.
1064 if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
1066 if (aEndOffset) {
1067 rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
1068 if (NS_WARN_IF(NS_FAILED(rv))) {
1069 return rv;
1071 } else {
1072 rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
1073 RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
1074 if (NS_WARN_IF(NS_FAILED(rv))) {
1075 return rv;
1079 range.swap(*aRange);
1080 return NS_OK;
1083 nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
1084 int32_t aStartOffset,
1085 nsINode* aEndNode,
1086 int32_t aEndOffset) {
1087 RefPtr<nsRange> range;
1088 nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
1089 aEndOffset, getter_AddRefs(range));
1090 NS_ENSURE_SUCCESS(rv, rv);
1092 if (!range) return NS_OK; // range is empty: nothing to do
1094 UniquePtr<mozInlineSpellStatus> status =
1095 mozInlineSpellStatus::CreateForRange(*this, range);
1096 return ScheduleSpellCheck(std::move(status));
1099 // mozInlineSpellChecker::ShouldSpellCheckNode
1101 // There are certain conditions when we don't want to spell check a node. In
1102 // particular quotations, moz signatures, etc. This routine returns false
1103 // for these cases.
1105 // static
1106 bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
1107 nsINode* aNode) {
1108 MOZ_ASSERT(aNode);
1109 if (!aNode->IsContent()) return false;
1111 nsIContent* content = aNode->AsContent();
1113 if (aEditorBase->IsMailEditor()) {
1114 nsIContent* parent = content->GetParent();
1115 while (parent) {
1116 if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
1117 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1118 nsGkAtoms::cite, eIgnoreCase)) {
1119 return false;
1121 if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
1122 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1123 nsGkAtoms::mozsignature,
1124 eIgnoreCase)) {
1125 return false;
1127 if (parent->IsHTMLElement(nsGkAtoms::div) &&
1128 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
1129 nsGkAtoms::mozfwcontainer,
1130 eIgnoreCase)) {
1131 return false;
1134 parent = parent->GetParent();
1136 } else {
1137 // Check spelling only if the node is editable, and GetSpellcheck() is true
1138 // on the nearest HTMLElement ancestor.
1139 if (!content->IsEditable()) {
1140 return false;
1143 // Make sure that we can always turn on spell checking for inputs/textareas.
1144 // Note that because of the previous check, at this point we know that the
1145 // node is editable.
1146 if (content->IsInNativeAnonymousSubtree()) {
1147 nsIContent* node = content->GetParent();
1148 while (node && node->IsInNativeAnonymousSubtree()) {
1149 node = node->GetParent();
1151 if (node && node->IsTextControlElement()) {
1152 return true;
1156 // Get HTML element ancestor (might be aNode itself, although probably that
1157 // has to be a text node in real life here)
1158 nsIContent* parent = content;
1159 while (!parent->IsHTMLElement()) {
1160 parent = parent->GetParent();
1161 if (!parent) {
1162 return true;
1166 // See if it's spellcheckable
1167 return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
1170 return true;
1173 // mozInlineSpellChecker::ScheduleSpellCheck
1175 // This is called by code to do the actual spellchecking. We will set up
1176 // the proper structures for calls to DoSpellCheck.
1178 nsresult mozInlineSpellChecker::ScheduleSpellCheck(
1179 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1180 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1181 ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
1182 mFullSpellCheckScheduled));
1184 if (mFullSpellCheckScheduled) {
1185 // Just ignore this; we're going to spell-check everything anyway
1186 return NS_OK;
1188 // Cache the value because we are going to move aStatus's ownership to
1189 // the new created mozInlineSpellResume instance.
1190 bool isFullSpellCheck = aStatus->IsFullSpellCheck();
1192 RefPtr<mozInlineSpellResume> resume =
1193 new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
1194 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1196 nsresult rv = resume->Post();
1197 if (NS_SUCCEEDED(rv)) {
1198 if (isFullSpellCheck) {
1199 // We're going to check everything. Suppress further spell-check attempts
1200 // until that happens.
1201 mFullSpellCheckScheduled = true;
1203 ChangeNumPendingSpellChecks(1);
1205 return rv;
1208 // mozInlineSpellChecker::DoSpellCheckSelection
1210 // Called to re-check all misspelled words. We iterate over all ranges in
1211 // the selection and call DoSpellCheck on them. This is used when a word
1212 // is ignored or added to the dictionary: all instances of that word should
1213 // be removed from the selection.
1215 // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1216 // Typically, checking this small amount of text is relatively fast, but
1217 // for large numbers of words, a lag may be noticeable.
1219 nsresult mozInlineSpellChecker::DoSpellCheckSelection(
1220 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
1221 nsresult rv;
1223 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1224 mNumWordsInSpellSelection = 0;
1226 // Since we could be modifying the ranges for the spellCheckSelection while
1227 // looping on the spell check selection, keep a separate array of range
1228 // elements inside the selection
1229 nsTArray<RefPtr<nsRange>> ranges;
1231 const uint32_t rangeCount = aSpellCheckSelection->RangeCount();
1232 for (const uint32_t idx : IntegerRange(rangeCount)) {
1233 MOZ_ASSERT(aSpellCheckSelection->RangeCount() == rangeCount);
1234 nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
1235 MOZ_ASSERT(range);
1236 if (MOZ_LIKELY(range)) {
1237 ranges.AppendElement(range);
1241 // We have saved the ranges above. Clearing the spellcheck selection here
1242 // isn't necessary (rechecking each word will modify it as necessary) but
1243 // provides better performance. By ensuring that no ranges need to be
1244 // removed in DoSpellCheck, we can save checking range inclusion which is
1245 // slow.
1246 aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
1248 // We use this state object for all calls, and just update its range. Note
1249 // that we don't need to call FinishInit since we will be filling in the
1250 // necessary information.
1251 UniquePtr<mozInlineSpellStatus> status =
1252 mozInlineSpellStatus::CreateForRange(*this, nullptr);
1254 bool doneChecking;
1255 for (uint32_t idx : IntegerRange(rangeCount)) {
1256 // We can consider this word as "added" since we know it has no spell
1257 // check range over it that needs to be deleted. All the old ranges
1258 // were cleared above. We also need to clear the word count so that we
1259 // check all words instead of stopping early.
1260 status->mRange = ranges[idx];
1261 rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
1262 NS_ENSURE_SUCCESS(rv, rv);
1263 MOZ_ASSERT(
1264 doneChecking,
1265 "We gave the spellchecker one word, but it didn't finish checking?!?!");
1268 return NS_OK;
1271 class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
1272 public:
1274 * @param aStatus must be non-nullptr.
1276 SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
1277 mozInlineSpellWordUtil& aWordUtil,
1278 mozilla::dom::Selection& aSpellCheckSelection,
1279 const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
1280 bool& aDoneChecking)
1281 : mInlineSpellChecker{aInlineSpellChecker},
1282 mWordUtil{aWordUtil},
1283 mSpellCheckSelection{aSpellCheckSelection},
1284 mStatus{aStatus},
1285 mDoneChecking{aDoneChecking} {
1286 MOZ_ASSERT(aStatus);
1289 [[nodiscard]] nsresult Execute();
1291 private:
1292 // Creates an async request to check the words and update the ranges for the
1293 // misspellings.
1295 // @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
1296 // @param aOldRangesForSomeWords ranges from previous spellcheckings which
1297 // might need to be removed. Its length might
1298 // differ from `aWords.Length()`.
1299 // @param aNodeOffsetRangesForWords One range for each word in aWords. So
1300 // `aNodeOffsetRangesForWords.Length() ==
1301 // aWords.Length()`.
1302 void CheckWordsAndUpdateRangesForMisspellings(
1303 const nsTArray<nsString>& aWords,
1304 nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1305 nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);
1307 void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);
1309 bool ShouldSpellCheckRange(const nsRange& aRange) const;
1311 bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;
1313 mozInlineSpellChecker& mInlineSpellChecker;
1314 mozInlineSpellWordUtil& mWordUtil;
1315 mozilla::dom::Selection& mSpellCheckSelection;
1316 const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
1317 bool& mDoneChecking;
1320 bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
1321 const nsRange& aRange) const {
1322 if (aRange.Collapsed()) {
1323 return false;
1326 nsINode* beginNode = aRange.GetStartContainer();
1327 nsINode* endNode = aRange.GetEndContainer();
1329 const nsINode* rootNode = mWordUtil.GetRootNode();
1330 return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
1331 beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
1332 endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
1335 bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
1336 const nsINode& aNode, int32_t aOffset) const {
1337 ErrorResult erv;
1338 return mStatus->GetNoCheckRange() &&
1339 mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
1342 void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
1343 const nsTArray<RefPtr<nsRange>>& aRanges) {
1344 for (uint32_t i = 0; i < aRanges.Length(); i++) {
1345 mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
1349 // mozInlineSpellChecker::SpellCheckerSlice::Execute
1351 // This function checks words intersecting the given range, excluding those
1352 // inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
1353 // will have any spell selection removed (this is used to hide the
1354 // underlining for the word that the caret is in). aNoCheckRange should be
1355 // on word boundaries.
1357 // mResume->mCreatedRange is a possibly nullptr range of new text that was
1358 // inserted. Inside this range, we don't bother to check whether things are
1359 // inside the spellcheck selection, which speeds up large paste operations
1360 // considerably.
1362 // Normal case when editing text by typing
1363 // h e l l o w o r k d h o w a r e y o u
1364 // ^ caret
1365 // [-------] mRange
1366 // [-------] mNoCheckRange
1367 // -> does nothing (range is the same as the no check range)
1369 // Case when pasting:
1370 // [---------- pasted text ----------]
1371 // h e l l o w o r k d h o w a r e y o u
1372 // ^ caret
1373 // [---] aNoCheckRange
1374 // -> recheck all words in range except those in aNoCheckRange
1376 // If checking is complete, *aDoneChecking will be set. If there is more
1377 // but we ran out of time, this will be false and the range will be
1378 // updated with the stuff that still needs checking.
1380 nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
1381 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1383 mDoneChecking = true;
1385 if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
1386 return NS_ERROR_NOT_INITIALIZED;
1389 if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
1390 return NS_OK;
1393 // get the editor for ShouldSpellCheckNode, this may fail in reasonable
1394 // circumstances since the editor could have gone away
1395 RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
1396 if (!editorBase || editorBase->Destroyed()) {
1397 return NS_ERROR_FAILURE;
1400 if (!ShouldSpellCheckRange(*mStatus->mRange)) {
1401 // Just bail out and don't try to spell-check this
1402 return NS_OK;
1405 // see if the selection has any ranges, if not, then we can optimize checking
1406 // range inclusion later (we have no ranges when we are initially checking or
1407 // when there are no misspelled words yet).
1408 const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();
1410 // set the starting DOM position to be the beginning of our range
1411 if (nsresult rv = mWordUtil.SetPositionAndEnd(
1412 mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
1413 mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
1414 NS_FAILED(rv)) {
1415 // Just bail out and don't try to spell-check this
1416 return NS_OK;
1419 // aWordUtil.SetPosition flushes pending notifications, check editor again.
1420 if (!mInlineSpellChecker.mEditorBase) {
1421 return NS_ERROR_FAILURE;
1424 int32_t wordsChecked = 0;
1425 PRTime beginTime = PR_Now();
1427 nsTArray<nsString> normalizedWords;
1428 nsTArray<RefPtr<nsRange>> oldRangesToRemove;
1429 nsTArray<NodeOffsetRange> checkRanges;
1430 mozInlineSpellWordUtil::Word word;
1431 static const size_t requestChunkSize =
1432 INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
1434 while (mWordUtil.GetNextWord(word)) {
1435 // get the range for the current word.
1436 nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
1437 nsINode* const endNode = word.mNodeOffsetRange.End().Node();
1438 // TODO: Make them `uint32_t`
1439 const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
1440 const int32_t endOffset = word.mNodeOffsetRange.End().Offset();
1442 // see if we've done enough words in this round and run out of time.
1443 if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
1444 PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
1445 // stop checking, our time limit has been exceeded.
1446 MOZ_LOG(
1447 sInlineSpellCheckerLog, LogLevel::Verbose,
1448 ("%s: we have run out of time, schedule next round.", __FUNCTION__));
1450 CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1451 std::move(oldRangesToRemove),
1452 std::move(checkRanges));
1454 // move the range to encompass the stuff that needs checking.
1455 nsresult rv = mStatus->mRange->SetStart(
1456 beginNode, AssertedCast<uint32_t>(beginOffset));
1457 if (NS_FAILED(rv)) {
1458 // The range might be unhappy because the beginning is after the
1459 // end. This is possible when the requested end was in the middle
1460 // of a word, just ignore this situation and assume we're done.
1461 return NS_OK;
1463 mDoneChecking = false;
1464 return NS_OK;
1467 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1468 ("%s: got word \"%s\"%s", __FUNCTION__,
1469 NS_ConvertUTF16toUTF8(word.mText).get(),
1470 word.mSkipChecking ? " (not checking)" : ""));
1472 // see if there is a spellcheck range that already intersects the word
1473 // and remove it. We only need to remove old ranges, so don't bother if
1474 // there were no ranges when we started out.
1475 if (originalRangeCount > 0) {
1476 ErrorResult erv;
1477 // likewise, if this word is inside new text, we won't bother testing
1478 if (!mStatus->GetCreatedRange() ||
1479 !mStatus->GetCreatedRange()->IsPointInRange(
1480 *beginNode, AssertedCast<uint32_t>(beginOffset), erv)) {
1481 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1482 ("%s: removing ranges for some interval.", __FUNCTION__));
1484 nsTArray<RefPtr<nsRange>> ranges;
1485 mSpellCheckSelection.GetRangesForInterval(
1486 *beginNode, AssertedCast<uint32_t>(beginOffset), *endNode,
1487 AssertedCast<uint32_t>(endOffset), true, ranges, erv);
1488 RETURN_NSRESULT_ON_FAILURE(erv);
1489 oldRangesToRemove.AppendElements(std::move(ranges));
1493 // some words are special and don't need checking
1494 if (word.mSkipChecking) {
1495 continue;
1498 // some nodes we don't spellcheck
1499 if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
1500 continue;
1503 // Don't check spelling if we're inside the noCheckRange. This needs to
1504 // be done after we clear any old selection because the excluded word
1505 // might have been previously marked.
1507 // We do a simple check to see if the beginning of our word is in the
1508 // exclusion range. Because the exclusion range is a multiple of a word,
1509 // this is sufficient.
1510 if (IsInNoCheckRange(*beginNode, beginOffset)) {
1511 continue;
1514 // check spelling and add to selection if misspelled
1515 mozInlineSpellWordUtil::NormalizeWord(word.mText);
1516 normalizedWords.AppendElement(word.mText);
1517 checkRanges.AppendElement(word.mNodeOffsetRange);
1518 wordsChecked++;
1519 if (normalizedWords.Length() >= requestChunkSize) {
1520 CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
1521 std::move(oldRangesToRemove),
1522 std::move(checkRanges));
1523 normalizedWords.Clear();
1524 oldRangesToRemove = {};
1525 // Set new empty data for spellcheck range in DOM to avoid
1526 // clang-tidy detection.
1527 checkRanges = nsTArray<NodeOffsetRange>();
1531 CheckWordsAndUpdateRangesForMisspellings(
1532 normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));
1534 return NS_OK;
1537 nsresult mozInlineSpellChecker::DoSpellCheck(
1538 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
1539 const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
1540 MOZ_ASSERT(aDoneChecking);
1542 SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
1543 aStatus, *aDoneChecking};
1545 return spellCheckerSlice.Execute();
1548 // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
1549 class MOZ_RAII AutoChangeNumPendingSpellChecks final {
1550 public:
1551 explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
1552 int32_t aDelta)
1553 : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
1555 ~AutoChangeNumPendingSpellChecks() {
1556 mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
1559 private:
1560 RefPtr<mozInlineSpellChecker> mSpellChecker;
1561 int32_t mDelta;
1564 void mozInlineSpellChecker::SpellCheckerSlice::
1565 CheckWordsAndUpdateRangesForMisspellings(
1566 const nsTArray<nsString>& aWords,
1567 nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
1568 nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
1569 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1570 ("%s: aWords.Length()=%i", __FUNCTION__,
1571 static_cast<int>(aWords.Length())));
1573 MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());
1575 // TODO:
1576 // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
1577 // to remove ranges more efficiently.
1579 if (aWords.IsEmpty()) {
1580 RemoveRanges(aOldRangesForSomeWords);
1581 return;
1584 mInlineSpellChecker.ChangeNumPendingSpellChecks(1);
1586 RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
1587 RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
1588 uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
1589 mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
1590 GetMainThreadSerialEventTarget(), __func__,
1591 [inlineSpellChecker, spellCheckerSelection,
1592 nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
1593 oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
1594 token](const nsTArray<bool>& aIsMisspelled) {
1595 if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1596 // This result is never used
1597 return;
1600 if (!inlineSpellChecker->mEditorBase ||
1601 inlineSpellChecker->mEditorBase->Destroyed()) {
1602 return;
1605 AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);
1607 if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
1608 return;
1611 inlineSpellChecker->UpdateRangesForMisspelledWords(
1612 nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
1613 *spellCheckerSelection);
1615 [inlineSpellChecker, token](nsresult aRv) {
1616 if (!inlineSpellChecker->mEditorBase ||
1617 inlineSpellChecker->mEditorBase->Destroyed()) {
1618 return;
1621 if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
1622 // This result is never used
1623 return;
1626 inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
1630 // mozInlineSpellChecker::ResumeCheck
1632 // Called by the resume event when it fires. We will try to pick up where
1633 // the last resume left off.
1635 nsresult mozInlineSpellChecker::ResumeCheck(
1636 UniquePtr<mozInlineSpellStatus>&& aStatus) {
1637 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1639 // Observers should be notified that spell check has ended only after spell
1640 // check is done below, but since there are many early returns in this method
1641 // and the number of pending spell checks must be decremented regardless of
1642 // whether the spell check actually happens, use this RAII object.
1643 AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
1645 if (aStatus->IsFullSpellCheck()) {
1646 // Allow posting new spellcheck resume events from inside
1647 // ResumeCheck, now that we're actually firing.
1648 MOZ_ASSERT(mFullSpellCheckScheduled,
1649 "How could this be false? The full spell check is "
1650 "calling us!!");
1651 mFullSpellCheckScheduled = false;
1654 if (!mSpellCheck) return NS_OK; // spell checking has been turned off
1656 if (!mEditorBase) {
1657 return NS_OK;
1660 Maybe<mozInlineSpellWordUtil> wordUtil{
1661 mozInlineSpellWordUtil::Create(*mEditorBase)};
1662 if (!wordUtil) {
1663 return NS_OK; // editor doesn't like us, don't assert
1666 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
1667 if (NS_WARN_IF(!spellCheckSelection)) {
1668 return NS_ERROR_FAILURE;
1671 nsTArray<nsCString> currentDictionaries;
1672 nsresult rv = mSpellCheck->GetCurrentDictionaries(currentDictionaries);
1673 if (NS_FAILED(rv)) {
1674 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
1675 ("%s: no active dictionary.", __FUNCTION__));
1677 // no active dictionary
1678 for (const uint32_t index :
1679 Reversed(IntegerRange(spellCheckSelection->RangeCount()))) {
1680 RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
1681 if (MOZ_LIKELY(checkRange)) {
1682 RemoveRange(spellCheckSelection, checkRange);
1685 return NS_OK;
1688 CleanupRangesInSelection(spellCheckSelection);
1690 rv = aStatus->FinishInitOnEvent(*wordUtil);
1691 NS_ENSURE_SUCCESS(rv, rv);
1692 if (!aStatus->mRange) return NS_OK; // empty range, nothing to do
1694 bool doneChecking = true;
1695 if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
1696 rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
1697 else
1698 rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
1699 NS_ENSURE_SUCCESS(rv, rv);
1701 if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
1702 return rv;
1705 // mozInlineSpellChecker::IsPointInSelection
1707 // Determines if a given (node,offset) point is inside the given
1708 // selection. If so, the specific range of the selection that
1709 // intersects is places in *aRange. (There may be multiple disjoint
1710 // ranges in a selection.)
1712 // If there is no intersection, *aRange will be nullptr.
1714 // static
1715 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
1716 nsINode* aNode,
1717 uint32_t aOffset,
1718 nsRange** aRange) {
1719 *aRange = nullptr;
1721 nsTArray<nsRange*> ranges;
1722 nsresult rv = aSelection.GetDynamicRangesForIntervalArray(
1723 aNode, aOffset, aNode, aOffset, true, &ranges);
1724 NS_ENSURE_SUCCESS(rv, rv);
1726 if (ranges.Length() == 0) return NS_OK; // no matches
1728 // there may be more than one range returned, and we don't know what do
1729 // do with that, so just get the first one
1730 NS_ADDREF(*aRange = ranges[0]);
1731 return NS_OK;
1734 nsresult mozInlineSpellChecker::CleanupRangesInSelection(
1735 Selection* aSelection) {
1736 // integrity check - remove ranges that have collapsed to nothing. This
1737 // can happen if the node containing a highlighted word was removed.
1738 if (!aSelection) return NS_ERROR_FAILURE;
1740 // TODO: Rewrite this with reversed ranged-loop, it might make this simpler.
1741 int64_t count = aSelection->RangeCount();
1742 for (int64_t index = 0; index < count; index++) {
1743 nsRange* checkRange = aSelection->GetRangeAt(static_cast<uint32_t>(index));
1744 if (MOZ_LIKELY(checkRange)) {
1745 if (checkRange->Collapsed()) {
1746 RemoveRange(aSelection, checkRange);
1747 index--;
1748 count--;
1753 return NS_OK;
1756 // mozInlineSpellChecker::RemoveRange
1758 // For performance reasons, we have an upper bound on the number of word
1759 // ranges in the spell check selection. When removing a range from the
1760 // selection, we need to decrement mNumWordsInSpellSelection
1762 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
1763 nsRange* aRange) {
1764 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
1766 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1767 NS_ENSURE_ARG_POINTER(aRange);
1769 ErrorResult rv;
1770 RefPtr<nsRange> range{aRange};
1771 RefPtr<Selection> selection{aSpellCheckSelection};
1772 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
1773 if (!rv.Failed()) {
1774 if (mNumWordsInSpellSelection) {
1775 mNumWordsInSpellSelection--;
1779 return rv.StealNSResult();
1782 struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange {
1783 static bool Equals(const RefPtr<nsRange>& aRange,
1784 const NodeOffsetRange& aNodeOffsetRange) {
1785 return aNodeOffsetRange == *aRange;
1789 void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
1790 const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords,
1791 const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords,
1792 const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) {
1793 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
1795 MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length());
1797 // When the spellchecker checks text containing words separated by "/", it may
1798 // happen that some words checked in one timeslice, are checked again in a
1799 // following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo"
1800 // and "baz" are checked in one timeslice and two ranges are added for them.
1801 // In the following timeslice "foo" and "baz" are checked again but since
1802 // their corresponding ranges are already in the spellcheck-Selection
1803 // they don't have to be added again and since "foo" and "baz" still contain
1804 // spelling mistakes, they don't have to be removed.
1806 // In this case, it's more efficient to keep the existing ranges.
1808 AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1809 oldRangesMarkedForRemoval;
1810 for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) {
1811 oldRangesMarkedForRemoval.AppendElement(true);
1814 AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
1815 nodeOffsetRangesMarkedForAdding;
1816 for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) {
1817 nodeOffsetRangesMarkedForAdding.AppendElement(false);
1820 for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
1821 if (!aIsMisspelled[i]) {
1822 continue;
1825 const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i];
1826 const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
1827 nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
1828 if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
1829 aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
1830 aSpellCheckerSelection)) {
1831 /** TODO: warn in case the old range doesn't
1832 belong to the selection. This is not critical,
1833 because other code can always remove them
1834 before the actual spellchecking happens. */
1835 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
1836 ("%s: reusing old range.", __FUNCTION__));
1838 oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false;
1839 } else {
1840 nodeOffsetRangesMarkedForAdding[i] = true;
1844 for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) {
1845 if (oldRangesMarkedForRemoval[i]) {
1846 RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]);
1850 // Add ranges after removing the marked old ones, so that the Selection can
1851 // become full again.
1852 for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) {
1853 if (nodeOffsetRangesMarkedForAdding[i]) {
1854 RefPtr<nsRange> wordRange =
1855 mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]);
1856 // If we somehow can't make a range for this word, just ignore
1857 // it.
1858 if (wordRange) {
1859 AddRange(&aSpellCheckerSelection, wordRange);
1865 // mozInlineSpellChecker::AddRange
1867 // For performance reasons, we have an upper bound on the number of word
1868 // ranges we'll add to the spell check selection. Once we reach that upper
1869 // bound, stop adding the ranges
1871 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
1872 nsRange* aRange) {
1873 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1874 NS_ENSURE_ARG_POINTER(aRange);
1876 nsresult rv = NS_OK;
1878 if (!IsSpellCheckSelectionFull()) {
1879 IgnoredErrorResult err;
1880 aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
1881 err);
1882 if (err.Failed()) {
1883 rv = err.StealNSResult();
1884 } else {
1885 mNumWordsInSpellSelection++;
1889 return rv;
1892 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
1893 if (NS_WARN_IF(!mEditorBase)) {
1894 return nullptr;
1896 RefPtr<Selection> selection =
1897 mEditorBase->GetSelection(SelectionType::eSpellCheck);
1898 if (!selection) {
1899 return nullptr;
1901 return selection.forget();
1904 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
1905 if (NS_WARN_IF(!mEditorBase)) {
1906 return NS_OK; // XXX Why NS_OK?
1909 // figure out the old caret position based on the current selection
1910 RefPtr<Selection> selection = mEditorBase->GetSelection();
1911 if (NS_WARN_IF(!selection)) {
1912 return NS_ERROR_FAILURE;
1915 mCurrentSelectionAnchorNode = selection->GetFocusNode();
1916 mCurrentSelectionOffset = selection->FocusOffset();
1918 return NS_OK;
1921 // mozInlineSpellChecker::HandleNavigationEvent
1923 // Acts upon mouse clicks and keyboard navigation changes, spell checking
1924 // the previous word if the new navigation location moves us to another
1925 // word.
1927 // This is complicated by the fact that our mouse events are happening after
1928 // selection has been changed to account for the mouse click. But keyboard
1929 // events are happening before the caret selection has changed. Working
1930 // around this by letting keyboard events setting forceWordSpellCheck to
1931 // true. aNewPositionOffset also tries to work around this for the
1932 // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1934 nsresult mozInlineSpellChecker::HandleNavigationEvent(
1935 bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
1936 nsresult rv;
1938 // If we already handled the navigation event and there is no possibility
1939 // anything has changed since then, we don't have to do anything. This
1940 // optimization makes a noticeable difference when you hold down a navigation
1941 // key like Page Down.
1942 if (!mNeedsCheckAfterNavigation) return NS_OK;
1944 nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
1945 uint32_t currentAnchorOffset = mCurrentSelectionOffset;
1947 // now remember the new focus position resulting from the event
1948 rv = SaveCurrentSelectionPosition();
1949 NS_ENSURE_SUCCESS(rv, rv);
1951 bool shouldPost;
1952 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
1953 mozInlineSpellStatus::CreateForNavigation(
1954 *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
1955 currentAnchorOffset, mCurrentSelectionAnchorNode,
1956 mCurrentSelectionOffset, &shouldPost);
1958 if (NS_WARN_IF(res.isErr())) {
1959 return res.unwrapErr();
1962 if (shouldPost) {
1963 rv = ScheduleSpellCheck(res.unwrap());
1964 NS_ENSURE_SUCCESS(rv, rv);
1967 return NS_OK;
1970 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) {
1971 WidgetEvent* widgetEvent = aEvent->WidgetEventPtr();
1972 if (MOZ_UNLIKELY(!widgetEvent)) {
1973 return NS_OK;
1976 switch (widgetEvent->mMessage) {
1977 case eBlur:
1978 OnBlur(*aEvent);
1979 return NS_OK;
1980 case ePointerClick:
1981 OnPointerClick(*aEvent);
1982 return NS_OK;
1983 case eKeyDown:
1984 OnKeyDown(*aEvent);
1985 return NS_OK;
1986 default:
1987 MOZ_ASSERT_UNREACHABLE("You must forgot to handle new event type");
1988 return NS_OK;
1992 void mozInlineSpellChecker::OnBlur(Event& aEvent) {
1993 // force spellcheck on blur, for instance when tabbing out of a textbox
1994 HandleNavigationEvent(true);
1997 void mozInlineSpellChecker::OnPointerClick(Event& aPointerEvent) {
1998 MouseEvent* const mouseEvent = aPointerEvent.AsMouseEvent();
1999 if (MOZ_UNLIKELY(!mouseEvent)) {
2000 return;
2003 // ignore any errors from HandleNavigationEvent as we don't want to prevent
2004 // anyone else from seeing this event.
2005 HandleNavigationEvent(mouseEvent->Button() != 0);
2008 void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) {
2009 WidgetKeyboardEvent* widgetKeyboardEvent =
2010 aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
2011 if (MOZ_UNLIKELY(!widgetKeyboardEvent)) {
2012 return;
2015 // we only care about navigation keys that moved selection
2016 switch (widgetKeyboardEvent->mKeyNameIndex) {
2017 case KEY_NAME_INDEX_ArrowRight:
2018 // XXX Does this work with RTL text?
2019 HandleNavigationEvent(false, 1);
2020 return;
2021 case KEY_NAME_INDEX_ArrowLeft:
2022 // XXX Does this work with RTL text?
2023 HandleNavigationEvent(false, -1);
2024 return;
2025 case KEY_NAME_INDEX_ArrowUp:
2026 case KEY_NAME_INDEX_ArrowDown:
2027 case KEY_NAME_INDEX_Home:
2028 case KEY_NAME_INDEX_End:
2029 case KEY_NAME_INDEX_PageDown:
2030 case KEY_NAME_INDEX_PageUp:
2031 HandleNavigationEvent(true /* force a spelling correction */);
2032 return;
2033 default:
2034 return;
2038 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
2039 class UpdateCurrentDictionaryCallback final
2040 : public nsIEditorSpellCheckCallback {
2041 public:
2042 NS_DECL_ISUPPORTS
2044 explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
2045 uint32_t aDisabledAsyncToken)
2046 : mSpellChecker(aSpellChecker),
2047 mDisabledAsyncToken(aDisabledAsyncToken) {}
2049 NS_IMETHOD EditorSpellCheckDone() override {
2050 // Ignore this callback if SetEnableRealTimeSpell(false) was called after
2051 // the UpdateCurrentDictionary call that triggered it.
2052 return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
2053 ? NS_OK
2054 : mSpellChecker->CurrentDictionaryUpdated();
2057 private:
2058 ~UpdateCurrentDictionaryCallback() {}
2060 RefPtr<mozInlineSpellChecker> mSpellChecker;
2061 uint32_t mDisabledAsyncToken;
2063 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
2065 NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
2066 // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
2067 // checker is being initialized. Calling UpdateCurrentDictionary on
2068 // mPendingSpellCheck simply queues the dictionary update after the init.
2069 RefPtr<EditorSpellCheck> spellCheck =
2070 mSpellCheck ? mSpellCheck : mPendingSpellCheck;
2071 if (!spellCheck) {
2072 return NS_OK;
2075 RefPtr<UpdateCurrentDictionaryCallback> cb =
2076 new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
2077 NS_ENSURE_STATE(cb);
2078 nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
2079 if (NS_FAILED(rv)) {
2080 cb = nullptr;
2081 return rv;
2083 mNumPendingUpdateCurrentDictionary++;
2084 ChangeNumPendingSpellChecks(1);
2086 return NS_OK;
2089 // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
2090 nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
2091 mNumPendingUpdateCurrentDictionary--;
2092 MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0,
2093 "CurrentDictionaryUpdated called without corresponding "
2094 "UpdateCurrentDictionary call!");
2095 ChangeNumPendingSpellChecks(-1);
2097 nsresult rv = SpellCheckRange(nullptr);
2098 NS_ENSURE_SUCCESS(rv, rv);
2100 return NS_OK;
2103 NS_IMETHODIMP
2104 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
2105 *aPending = mNumPendingSpellChecks > 0;
2106 return NS_OK;