mozInlineSpellChecker.cpp (76667B)
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/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" 72 73 using mozilla::LogLevel; 74 using namespace mozilla; 75 using namespace mozilla::dom; 76 using namespace mozilla::ipc; 77 78 // the number of milliseconds that we will take at once to do spellchecking 79 #define INLINESPELL_CHECK_TIMEOUT 1 80 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 86 87 // The maximum number of words to check word via IPC. 88 #define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25 89 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" 94 95 static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker"); 96 97 static const PRTime kMaxSpellCheckTimeInUsec = 98 INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC; 99 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) {} 112 113 // mozInlineSpellStatus::CreateForEditorChange 114 // 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. 119 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__)); 128 129 if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) { 130 return Err(NS_ERROR_FAILURE); 131 } 132 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(); 138 } 139 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); 145 } 146 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); 151 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; 156 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; 165 } 166 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()); 173 } 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); 181 } 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); 188 } 189 } 190 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()); 197 } 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); 202 } 203 } 204 205 cmpResult = 206 status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult); 207 if (NS_WARN_IF(errorResult.Failed())) { 208 return Err(errorResult.StealNSResult()); 209 } 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); 214 } 215 } 216 } 217 218 return status; 219 } 220 221 // mozInlineSpellStatus::CreateForNavigation 222 // 223 // For navigation events, we just need to store the new and old positions. 224 // 225 // In some cases, we detect that we shouldn't check. If this event should 226 // not be processed, *aContinue will be false. 227 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__)); 236 237 RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange( 238 aNewAnchorNode, aNewAnchorOffset); 239 if (NS_WARN_IF(!anchorRange)) { 240 return Err(NS_ERROR_FAILURE); 241 } 242 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}}; 248 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); 253 } 254 Element* root = editorBase->GetRoot(); 255 if (NS_WARN_IF(!root)) { 256 return Err(NS_ERROR_FAILURE); 257 } 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; 263 } 264 265 status->mOldNavigationAnchorRange = 266 mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode, 267 aOldAnchorOffset); 268 if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) { 269 return Err(NS_ERROR_FAILURE); 270 } 271 272 *aContinue = true; 273 return status; 274 } 275 276 // mozInlineSpellStatus::CreateForSelection 277 // 278 // It is easy for selections since we always re-check the spellcheck 279 // selection. 280 281 // static 282 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection( 283 mozInlineSpellChecker& aSpellChecker) { 284 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__)); 285 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; 291 } 292 293 // mozInlineSpellStatus::CreateForRange 294 // 295 // Called to cause the spellcheck of the given range. This will look like 296 // a change operation over the given range. 297 298 // static 299 UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange( 300 mozInlineSpellChecker& aSpellChecker, nsRange* aRange) { 301 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, 302 ("%s: range=%p", __FUNCTION__, aRange)); 303 304 UniquePtr<mozInlineSpellStatus> status{ 305 /* The constructor is `private`, hence the explicit allocation. */ 306 new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr, 307 nullptr, false, 0}}; 308 309 status->mRange = aRange; 310 return status; 311 } 312 313 // mozInlineSpellStatus::FinishInitOnEvent 314 // 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. 319 // 320 // Watch out: the range might still be nullptr if there is nothing to do, 321 // the caller will have to check for this. 322 323 nsresult mozInlineSpellStatus::FinishInitOnEvent( 324 mozInlineSpellWordUtil& aWordUtil) { 325 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, 326 ("%s: mRange=%p", __FUNCTION__, mRange.get())); 327 328 nsresult rv; 329 if (!mRange) { 330 rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0, 331 getter_AddRefs(mRange)); 332 NS_ENSURE_SUCCESS(rv, rv); 333 } 334 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); 343 } 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; 360 } 361 return NS_OK; 362 } 363 364 // mozInlineSpellStatus::FinishNavigationEvent 365 // 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. 371 // 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. 376 377 nsresult mozInlineSpellStatus::FinishNavigationEvent( 378 mozInlineSpellWordUtil& aWordUtil) { 379 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__)); 380 381 RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase; 382 if (!editorBase) { 383 return NS_ERROR_FAILURE; // editor is gone 384 } 385 386 MOZ_ASSERT(mAnchorRange, "No anchor for navigation!"); 387 388 if (!mOldNavigationAnchorRange->IsPositioned()) { 389 return NS_ERROR_NOT_INITIALIZED; 390 } 391 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(); 396 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); 404 405 // aWordUtil.GetRangeForWord flushes pending notifications, check editor 406 // again. 407 if (!mSpellChecker->mEditorBase) { 408 return NS_ERROR_FAILURE; // editor is gone 409 } 410 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(); 414 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(); 423 } 424 } 425 426 if (isInRange) { 427 // caller should give up 428 mRange = nullptr; 429 } else { 430 // check the old word 431 mRange = oldWord; 432 433 // Once we've spellchecked the current word, we don't need to spellcheck 434 // for any more navigation events. 435 mSpellChecker->mNeedsCheckAfterNavigation = false; 436 } 437 return NS_OK; 438 } 439 440 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor 441 // 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 446 447 nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor( 448 mozInlineSpellWordUtil& aWordUtil) { 449 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__)); 450 451 if (!mAnchorRange->IsPositioned()) { 452 return NS_ERROR_NOT_INITIALIZED; 453 } 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)); 459 } 460 461 // mozInlineSpellStatus::GetDocument 462 // 463 // Returns the Document object for the document for the 464 // current spellchecker. 465 466 Document* mozInlineSpellStatus::GetDocument() const { 467 if (!mSpellChecker->mEditorBase) { 468 return nullptr; 469 } 470 471 return mSpellChecker->mEditorBase->GetDocument(); 472 } 473 474 // mozInlineSpellStatus::PositionToCollapsedRange 475 // 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. 479 480 // static 481 already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange( 482 nsINode* aNode, uint32_t aOffset) { 483 if (NS_WARN_IF(!aNode)) { 484 return nullptr; 485 } 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(); 492 } 493 494 // mozInlineSpellResume 495 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)) {} 503 504 nsresult Post() { 505 nsCOMPtr<nsIRunnable> runnable(this); 506 return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000, 507 EventQueuePriority::Idle); 508 } 509 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)); 516 } 517 return NS_OK; 518 } 519 520 private: 521 uint32_t mDisabledAsyncToken; 522 UniquePtr<mozInlineSpellStatus> mStatus; 523 }; 524 525 // Used as the nsIEditorSpellCheck::InitSpellChecker callback. 526 class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback { 527 ~InitEditorSpellCheckCallback() {} 528 529 public: 530 NS_DECL_ISUPPORTS 531 532 explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker) 533 : mSpellChecker(aSpellChecker) {} 534 535 NS_IMETHOD EditorSpellCheckDone() override { 536 return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK; 537 } 538 539 void Cancel() { mSpellChecker = nullptr; } 540 541 private: 542 RefPtr<mozInlineSpellChecker> mSpellChecker; 543 }; 544 NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback) 545 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 553 554 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker) 555 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker) 556 557 NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck, 558 mCurrentSelectionAnchorNode) 559 560 mozInlineSpellChecker::SpellCheckingState 561 mozInlineSpellChecker::gCanEnableSpellChecking = 562 mozInlineSpellChecker::SpellCheck_Uninitialized; 563 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) {} 574 575 mozInlineSpellChecker::~mozInlineSpellChecker() {} 576 577 EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() { 578 return mSpellCheck ? mSpellCheck : mPendingSpellCheck; 579 } 580 581 NS_IMETHODIMP 582 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) { 583 *aSpellCheck = mSpellCheck; 584 NS_IF_ADDREF(*aSpellCheck); 585 return NS_OK; 586 } 587 588 NS_IMETHODIMP 589 mozInlineSpellChecker::Init(nsIEditor* aEditor) { 590 mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr; 591 return NS_OK; 592 } 593 594 // mozInlineSpellChecker::Cleanup 595 // 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! 599 // 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. 603 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()); 616 } 617 618 rv = UnregisterEventListeners(); 619 } 620 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. 628 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); 636 } 637 638 // Increment this token so that pending UpdateCurrentDictionary calls and 639 // scheduled spell checks are discarded when they finish. 640 mDisabledAsyncToken++; 641 642 if (mNumPendingUpdateCurrentDictionary > 0) { 643 // Account for pending UpdateCurrentDictionary calls. 644 ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, 645 editorBase); 646 mNumPendingUpdateCurrentDictionary = 0; 647 } 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); 652 } 653 654 mFullSpellCheckScheduled = false; 655 656 return rv; 657 } 658 659 // mozInlineSpellChecker::CanEnableInlineSpellChecking 660 // 661 // This function can be called to see if it seems likely that we can enable 662 // spellchecking before actually creating the InlineSpellChecking objects. 663 // 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. 668 // 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. 672 673 bool // static 674 mozInlineSpellChecker::CanEnableInlineSpellChecking() { 675 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) { 676 gCanEnableSpellChecking = SpellCheck_NotAvailable; 677 678 nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck(); 679 680 bool canSpellCheck = false; 681 nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck); 682 NS_ENSURE_SUCCESS(rv, false); 683 684 if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available; 685 } 686 return (gCanEnableSpellChecking == SpellCheck_Available); 687 } 688 689 void // static 690 mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() { 691 gCanEnableSpellChecking = SpellCheck_Uninitialized; 692 } 693 694 // mozInlineSpellChecker::RegisterEventListeners 695 // 696 // The inline spell checker listens to mouse events and keyboard navigation 697 // events. 698 699 nsresult mozInlineSpellChecker::RegisterEventListeners() { 700 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) { 701 return NS_ERROR_FAILURE; 702 } 703 704 StartToListenToEditSubActions(); 705 706 RefPtr<Document> doc = mEditorBase->GetDocument(); 707 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) { 708 return NS_ERROR_FAILURE; 709 } 710 EventListenerManager* eventListenerManager = 711 doc->GetOrCreateListenerManager(); 712 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) { 713 return NS_ERROR_FAILURE; 714 } 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; 722 } 723 724 // mozInlineSpellChecker::UnregisterEventListeners 725 726 nsresult mozInlineSpellChecker::UnregisterEventListeners() { 727 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) { 728 return NS_ERROR_FAILURE; 729 } 730 731 EndListeningToEditSubActions(); 732 733 RefPtr<Document> doc = mEditorBase->GetDocument(); 734 if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) { 735 return NS_ERROR_FAILURE; 736 } 737 EventListenerManager* eventListenerManager = 738 doc->GetOrCreateListenerManager(); 739 if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) { 740 return NS_ERROR_FAILURE; 741 } 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; 749 } 750 751 // mozInlineSpellChecker::GetEnableRealTimeSpell 752 753 NS_IMETHODIMP 754 mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) { 755 NS_ENSURE_ARG_POINTER(aEnabled); 756 *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr; 757 return NS_OK; 758 } 759 760 // mozInlineSpellChecker::SetEnableRealTimeSpell 761 762 NS_IMETHODIMP 763 mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) { 764 if (!aEnabled) { 765 mSpellCheck = nullptr; 766 return Cleanup(false); 767 } 768 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); 775 } 776 777 if (mPendingSpellCheck) { 778 // The editor spell checker is already being initialized. 779 return NS_OK; 780 } 781 782 mPendingSpellCheck = new EditorSpellCheck(); 783 mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL); 784 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); 792 } 793 794 ChangeNumPendingSpellChecks(1); 795 796 return NS_OK; 797 } 798 799 // Called when nsIEditorSpellCheck::InitSpellChecker completes. 800 nsresult mozInlineSpellChecker::EditorSpellCheckInited() { 801 MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!"); 802 803 // spell checking is enabled, register our event listeners to track navigation 804 RegisterEventListeners(); 805 806 mSpellCheck = mPendingSpellCheck; 807 mPendingSpellCheck = nullptr; 808 mPendingInitEditorSpellCheckCallback = nullptr; 809 ChangeNumPendingSpellChecks(-1); 810 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); 816 } 817 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); 831 } 832 } 833 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); 845 } 846 847 // mozInlineSpellChecker::SpellCheckAfterEditorChange 848 // 849 // Called by the editor when nearly anything happens to change the content. 850 // 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. 855 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 863 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; 867 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(); 877 } 878 879 rv = ScheduleSpellCheck(res.unwrap()); 880 NS_ENSURE_SUCCESS(rv, rv); 881 882 // remember the current caret position after every change 883 SaveCurrentSelectionPosition(); 884 return NS_OK; 885 } 886 887 // mozInlineSpellChecker::SpellCheckRange 888 // 889 // Spellchecks all the words in the given range. 890 // Supply a nullptr range and this will check the entire editor. 891 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; 898 } 899 900 UniquePtr<mozInlineSpellStatus> status = 901 mozInlineSpellStatus::CreateForRange(*this, aRange); 902 return ScheduleSpellCheck(std::move(status)); 903 } 904 905 // mozInlineSpellChecker::GetMisspelledWord 906 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; 912 } 913 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection(); 914 if (NS_WARN_IF(!spellCheckSelection)) { 915 return NS_ERROR_FAILURE; 916 } 917 return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword); 918 } 919 920 // mozInlineSpellChecker::ReplaceWord 921 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; 927 } 928 929 RefPtr<nsRange> range; 930 nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range)); 931 NS_ENSURE_SUCCESS(res, res); 932 933 if (!range) { 934 return NS_OK; 935 } 936 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); 943 } 944 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; 952 } 953 954 // mozInlineSpellChecker::AddWordToDictionary 955 956 NS_IMETHODIMP 957 mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) { 958 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); 959 960 nsresult rv = mSpellCheck->AddWordToDictionary(word); 961 NS_ENSURE_SUCCESS(rv, rv); 962 963 UniquePtr<mozInlineSpellStatus> status = 964 mozInlineSpellStatus::CreateForSelection(*this); 965 return ScheduleSpellCheck(std::move(status)); 966 } 967 968 // mozInlineSpellChecker::RemoveWordFromDictionary 969 970 NS_IMETHODIMP 971 mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) { 972 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); 973 974 nsresult rv = mSpellCheck->RemoveWordFromDictionary(word); 975 NS_ENSURE_SUCCESS(rv, rv); 976 977 UniquePtr<mozInlineSpellStatus> status = 978 mozInlineSpellStatus::CreateForRange(*this, nullptr); 979 return ScheduleSpellCheck(std::move(status)); 980 } 981 982 // mozInlineSpellChecker::IgnoreWord 983 984 NS_IMETHODIMP 985 mozInlineSpellChecker::IgnoreWord(const nsAString& word) { 986 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); 987 988 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word); 989 NS_ENSURE_SUCCESS(rv, rv); 990 991 UniquePtr<mozInlineSpellStatus> status = 992 mozInlineSpellStatus::CreateForSelection(*this); 993 return ScheduleSpellCheck(std::move(status)); 994 } 995 996 // mozInlineSpellChecker::IgnoreWords 997 998 NS_IMETHODIMP 999 mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) { 1000 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); 1001 1002 // add each word to the ignore list and then recheck the document 1003 for (auto& word : aWordsToIgnore) { 1004 mSpellCheck->IgnoreWordAllOccurrences(word); 1005 } 1006 1007 UniquePtr<mozInlineSpellStatus> status = 1008 mozInlineSpellStatus::CreateForSelection(*this); 1009 return ScheduleSpellCheck(std::move(status)); 1010 } 1011 1012 // mozInlineSpellChecker::MakeSpellCheckRange 1013 // 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. 1018 // 1019 // If the resulting range would be empty, nullptr is put into *aRange and the 1020 // function succeeds. 1021 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; 1029 1030 if (NS_WARN_IF(!mEditorBase)) { 1031 return NS_ERROR_FAILURE; 1032 } 1033 1034 RefPtr<Document> doc = mEditorBase->GetDocument(); 1035 if (NS_WARN_IF(!doc)) { 1036 return NS_ERROR_FAILURE; 1037 } 1038 1039 RefPtr<nsRange> range = nsRange::Create(doc); 1040 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; 1046 } 1047 aStartNode = aEndNode = domRootElement; 1048 aStartOffset = 0; 1049 aEndOffset = -1; 1050 } 1051 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... 1057 // 1058 // Let's do the thing which can't end up with bad O(N^2) behavior. 1059 aEndOffset = aEndNode->ChildNodes()->Length(); 1060 } 1061 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; 1065 1066 if (aEndOffset) { 1067 rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset); 1068 if (NS_WARN_IF(NS_FAILED(rv))) { 1069 return rv; 1070 } 1071 } else { 1072 rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset), 1073 RangeUtils::GetRawRangeBoundaryAfter(aEndNode)); 1074 if (NS_WARN_IF(NS_FAILED(rv))) { 1075 return rv; 1076 } 1077 } 1078 1079 range.swap(*aRange); 1080 return NS_OK; 1081 } 1082 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); 1091 1092 if (!range) return NS_OK; // range is empty: nothing to do 1093 1094 UniquePtr<mozInlineSpellStatus> status = 1095 mozInlineSpellStatus::CreateForRange(*this, range); 1096 return ScheduleSpellCheck(std::move(status)); 1097 } 1098 1099 // mozInlineSpellChecker::ShouldSpellCheckNode 1100 // 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. 1104 1105 // static 1106 bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase, 1107 nsINode* aNode) { 1108 MOZ_ASSERT(aNode); 1109 if (!aNode->IsContent()) return false; 1110 1111 nsIContent* content = aNode->AsContent(); 1112 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; 1120 } 1121 if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) && 1122 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class, 1123 nsGkAtoms::mozsignature, 1124 eIgnoreCase)) { 1125 return false; 1126 } 1127 if (parent->IsHTMLElement(nsGkAtoms::div) && 1128 parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class, 1129 nsGkAtoms::mozfwcontainer, 1130 eIgnoreCase)) { 1131 return false; 1132 } 1133 1134 parent = parent->GetParent(); 1135 } 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; 1141 } 1142 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(); 1150 } 1151 if (node && node->IsTextControlElement()) { 1152 return true; 1153 } 1154 } 1155 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; 1163 } 1164 } 1165 1166 // See if it's spellcheckable 1167 return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck(); 1168 } 1169 1170 return true; 1171 } 1172 1173 // mozInlineSpellChecker::ScheduleSpellCheck 1174 // 1175 // This is called by code to do the actual spellchecking. We will set up 1176 // the proper structures for calls to DoSpellCheck. 1177 1178 nsresult mozInlineSpellChecker::ScheduleSpellCheck( 1179 UniquePtr<mozInlineSpellStatus>&& aStatus) { 1180 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, 1181 ("%s: mFullSpellCheckScheduled=%i", __FUNCTION__, 1182 mFullSpellCheckScheduled)); 1183 1184 if (mFullSpellCheckScheduled) { 1185 // Just ignore this; we're going to spell-check everything anyway 1186 return NS_OK; 1187 } 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(); 1191 1192 RefPtr<mozInlineSpellResume> resume = 1193 new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken); 1194 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY); 1195 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; 1202 } 1203 ChangeNumPendingSpellChecks(1); 1204 } 1205 return rv; 1206 } 1207 1208 // mozInlineSpellChecker::DoSpellCheckSelection 1209 // 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. 1214 // 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. 1218 1219 nsresult mozInlineSpellChecker::DoSpellCheckSelection( 1220 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) { 1221 nsresult rv; 1222 1223 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges. 1224 mNumWordsInSpellSelection = 0; 1225 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; 1230 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); 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 (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?!?!"); 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 */ 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 1320 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 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); 1340 } 1341 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 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 // TODO: Make them `uint32_t` 1439 const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset(); 1440 const int32_t endOffset = word.mNodeOffsetRange.End().Offset(); 1441 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__)); 1449 1450 CheckWordsAndUpdateRangesForMisspellings(normalizedWords, 1451 std::move(oldRangesToRemove), 1452 std::move(checkRanges)); 1453 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; 1462 } 1463 mDoneChecking = false; 1464 return NS_OK; 1465 } 1466 1467 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, 1468 ("%s: got word \"%s\"%s", __FUNCTION__, 1469 NS_ConvertUTF16toUTF8(word.mText).get(), 1470 word.mSkipChecking ? " (not checking)" : "")); 1471 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__)); 1483 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)); 1490 } 1491 } 1492 1493 // some words are special and don't need checking 1494 if (word.mSkipChecking) { 1495 continue; 1496 } 1497 1498 // some nodes we don't spellcheck 1499 if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) { 1500 continue; 1501 } 1502 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. 1506 // 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; 1512 } 1513 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>(); 1528 } 1529 } 1530 1531 CheckWordsAndUpdateRangesForMisspellings( 1532 normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges)); 1533 1534 return NS_OK; 1535 } 1536 1537 nsresult mozInlineSpellChecker::DoSpellCheck( 1538 mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection, 1539 const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) { 1540 MOZ_ASSERT(aDoneChecking); 1541 1542 SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection, 1543 aStatus, *aDoneChecking}; 1544 1545 return spellCheckerSlice.Execute(); 1546 } 1547 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) {} 1554 1555 ~AutoChangeNumPendingSpellChecks() { 1556 mSpellChecker->ChangeNumPendingSpellChecks(mDelta); 1557 } 1558 1559 private: 1560 RefPtr<mozInlineSpellChecker> mSpellChecker; 1561 int32_t mDelta; 1562 }; 1563 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()))); 1572 1573 MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length()); 1574 1575 // TODO: 1576 // aOldRangesForSomeWords is sorted in the same order as aWords. Could be used 1577 // to remove ranges more efficiently. 1578 1579 if (aWords.IsEmpty()) { 1580 RemoveRanges(aOldRangesForSomeWords); 1581 return; 1582 } 1583 1584 mInlineSpellChecker.ChangeNumPendingSpellChecks(1); 1585 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; 1598 } 1599 1600 if (!inlineSpellChecker->mEditorBase || 1601 inlineSpellChecker->mEditorBase->Destroyed()) { 1602 return; 1603 } 1604 1605 AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1); 1606 1607 if (inlineSpellChecker->IsSpellCheckSelectionFull()) { 1608 return; 1609 } 1610 1611 inlineSpellChecker->UpdateRangesForMisspelledWords( 1612 nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled, 1613 *spellCheckerSelection); 1614 }, 1615 [inlineSpellChecker, token](nsresult aRv) { 1616 if (!inlineSpellChecker->mEditorBase || 1617 inlineSpellChecker->mEditorBase->Destroyed()) { 1618 return; 1619 } 1620 1621 if (token != inlineSpellChecker->GetDisabledAsyncToken()) { 1622 // This result is never used 1623 return; 1624 } 1625 1626 inlineSpellChecker->ChangeNumPendingSpellChecks(-1); 1627 }); 1628 } 1629 1630 // mozInlineSpellChecker::ResumeCheck 1631 // 1632 // Called by the resume event when it fires. We will try to pick up where 1633 // the last resume left off. 1634 1635 nsresult mozInlineSpellChecker::ResumeCheck( 1636 UniquePtr<mozInlineSpellStatus>&& aStatus) { 1637 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__)); 1638 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); 1644 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; 1652 } 1653 1654 if (!mSpellCheck) return NS_OK; // spell checking has been turned off 1655 1656 if (!mEditorBase) { 1657 return NS_OK; 1658 } 1659 1660 Maybe<mozInlineSpellWordUtil> wordUtil{ 1661 mozInlineSpellWordUtil::Create(*mEditorBase)}; 1662 if (!wordUtil) { 1663 return NS_OK; // editor doesn't like us, don't assert 1664 } 1665 1666 RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection(); 1667 if (NS_WARN_IF(!spellCheckSelection)) { 1668 return NS_ERROR_FAILURE; 1669 } 1670 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__)); 1676 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); 1683 } 1684 } 1685 return NS_OK; 1686 } 1687 1688 CleanupRangesInSelection(spellCheckSelection); 1689 1690 rv = aStatus->FinishInitOnEvent(*wordUtil); 1691 NS_ENSURE_SUCCESS(rv, rv); 1692 if (!aStatus->mRange) return NS_OK; // empty range, nothing to do 1693 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); 1700 1701 if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus)); 1702 return rv; 1703 } 1704 1705 // mozInlineSpellChecker::IsPointInSelection 1706 // 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.) 1711 // 1712 // If there is no intersection, *aRange will be nullptr. 1713 1714 // static 1715 nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection, 1716 nsINode* aNode, 1717 uint32_t aOffset, 1718 nsRange** aRange) { 1719 *aRange = nullptr; 1720 1721 nsTArray<nsRange*> ranges; 1722 nsresult rv = aSelection.GetDynamicRangesForIntervalArray( 1723 aNode, aOffset, aNode, aOffset, true, &ranges); 1724 NS_ENSURE_SUCCESS(rv, rv); 1725 1726 if (ranges.Length() == 0) return NS_OK; // no matches 1727 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; 1732 } 1733 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; 1739 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--; 1749 } 1750 } 1751 } 1752 1753 return NS_OK; 1754 } 1755 1756 // mozInlineSpellChecker::RemoveRange 1757 // 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 1761 1762 nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection, 1763 nsRange* aRange) { 1764 MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__)); 1765 1766 NS_ENSURE_ARG_POINTER(aSpellCheckSelection); 1767 NS_ENSURE_ARG_POINTER(aRange); 1768 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--; 1776 } 1777 } 1778 1779 return rv.StealNSResult(); 1780 } 1781 1782 struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange { 1783 static bool Equals(const RefPtr<nsRange>& aRange, 1784 const NodeOffsetRange& aNodeOffsetRange) { 1785 return aNodeOffsetRange == *aRange; 1786 } 1787 }; 1788 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__)); 1794 1795 MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length()); 1796 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. 1805 // 1806 // In this case, it's more efficient to keep the existing ranges. 1807 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); 1812 } 1813 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); 1818 } 1819 1820 for (size_t i = 0; i < aIsMisspelled.Length(); i++) { 1821 if (!aIsMisspelled[i]) { 1822 continue; 1823 } 1824 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__)); 1837 1838 oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false; 1839 } else { 1840 nodeOffsetRangesMarkedForAdding[i] = true; 1841 } 1842 } 1843 1844 for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) { 1845 if (oldRangesMarkedForRemoval[i]) { 1846 RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]); 1847 } 1848 } 1849 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); 1860 } 1861 } 1862 } 1863 } 1864 1865 // mozInlineSpellChecker::AddRange 1866 // 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 1870 1871 nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection, 1872 nsRange* aRange) { 1873 NS_ENSURE_ARG_POINTER(aSpellCheckSelection); 1874 NS_ENSURE_ARG_POINTER(aRange); 1875 1876 nsresult rv = NS_OK; 1877 1878 if (!IsSpellCheckSelectionFull()) { 1879 IgnoredErrorResult err; 1880 aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, 1881 err); 1882 if (err.Failed()) { 1883 rv = err.StealNSResult(); 1884 } else { 1885 mNumWordsInSpellSelection++; 1886 } 1887 } 1888 1889 return rv; 1890 } 1891 1892 already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() { 1893 if (NS_WARN_IF(!mEditorBase)) { 1894 return nullptr; 1895 } 1896 RefPtr<Selection> selection = 1897 mEditorBase->GetSelection(SelectionType::eSpellCheck); 1898 if (!selection) { 1899 return nullptr; 1900 } 1901 return selection.forget(); 1902 } 1903 1904 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() { 1905 if (NS_WARN_IF(!mEditorBase)) { 1906 return NS_OK; // XXX Why NS_OK? 1907 } 1908 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; 1913 } 1914 1915 mCurrentSelectionAnchorNode = selection->GetFocusNode(); 1916 mCurrentSelectionOffset = selection->FocusOffset(); 1917 1918 return NS_OK; 1919 } 1920 1921 // mozInlineSpellChecker::HandleNavigationEvent 1922 // 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. 1926 // 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. 1933 1934 nsresult mozInlineSpellChecker::HandleNavigationEvent( 1935 bool aForceWordSpellCheck, int32_t aNewPositionOffset) { 1936 nsresult rv; 1937 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; 1943 1944 nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode; 1945 uint32_t currentAnchorOffset = mCurrentSelectionOffset; 1946 1947 // now remember the new focus position resulting from the event 1948 rv = SaveCurrentSelectionPosition(); 1949 NS_ENSURE_SUCCESS(rv, rv); 1950 1951 bool shouldPost; 1952 Result<UniquePtr<mozInlineSpellStatus>, nsresult> res = 1953 mozInlineSpellStatus::CreateForNavigation( 1954 *this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode, 1955 currentAnchorOffset, mCurrentSelectionAnchorNode, 1956 mCurrentSelectionOffset, &shouldPost); 1957 1958 if (NS_WARN_IF(res.isErr())) { 1959 return res.unwrapErr(); 1960 } 1961 1962 if (shouldPost) { 1963 rv = ScheduleSpellCheck(res.unwrap()); 1964 NS_ENSURE_SUCCESS(rv, rv); 1965 } 1966 1967 return NS_OK; 1968 } 1969 1970 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) { 1971 WidgetEvent* widgetEvent = aEvent->WidgetEventPtr(); 1972 if (MOZ_UNLIKELY(!widgetEvent)) { 1973 return NS_OK; 1974 } 1975 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; 1989 } 1990 } 1991 1992 void mozInlineSpellChecker::OnBlur(Event& aEvent) { 1993 // force spellcheck on blur, for instance when tabbing out of a textbox 1994 HandleNavigationEvent(true); 1995 } 1996 1997 void mozInlineSpellChecker::OnPointerClick(Event& aPointerEvent) { 1998 MouseEvent* const mouseEvent = aPointerEvent.AsMouseEvent(); 1999 if (MOZ_UNLIKELY(!mouseEvent)) { 2000 return; 2001 } 2002 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); 2006 } 2007 2008 void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) { 2009 WidgetKeyboardEvent* widgetKeyboardEvent = 2010 aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); 2011 if (MOZ_UNLIKELY(!widgetKeyboardEvent)) { 2012 return; 2013 } 2014 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; 2035 } 2036 } 2037 2038 // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback. 2039 class UpdateCurrentDictionaryCallback final 2040 : public nsIEditorSpellCheckCallback { 2041 public: 2042 NS_DECL_ISUPPORTS 2043 2044 explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker, 2045 uint32_t aDisabledAsyncToken) 2046 : mSpellChecker(aSpellChecker), 2047 mDisabledAsyncToken(aDisabledAsyncToken) {} 2048 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(); 2055 } 2056 2057 private: 2058 ~UpdateCurrentDictionaryCallback() {} 2059 2060 RefPtr<mozInlineSpellChecker> mSpellChecker; 2061 uint32_t mDisabledAsyncToken; 2062 }; 2063 NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback) 2064 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; 2073 } 2074 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; 2082 } 2083 mNumPendingUpdateCurrentDictionary++; 2084 ChangeNumPendingSpellChecks(1); 2085 2086 return NS_OK; 2087 } 2088 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); 2096 2097 nsresult rv = SpellCheckRange(nullptr); 2098 NS_ENSURE_SUCCESS(rv, rv); 2099 2100 return NS_OK; 2101 } 2102 2103 NS_IMETHODIMP 2104 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) { 2105 *aPending = mNumPendingSpellChecks > 0; 2106 return NS_OK; 2107 }