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