Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=2 sts=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This class is called by the editor to handle spellchecking after various
* events. The main entrypoint is SpellCheckAfterEditorChange, which is called
* when the text is changed.
*
* It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
* notifications to be flushed when we are called from the editor. This is
* because the call might originate from a frame, and flushing the
* notifications might cause that frame to be deleted.
*
* We post an event and do all of the spellchecking in that event handler.
* We store all DOM pointers in ranges because they are kept up-to-date with
* DOM changes that may have happened while the event was on the queue.
*
* We also allow the spellcheck to be suspended and resumed later. This makes
* large pastes or initializations with a lot of text not hang the browser UI.
*
* An optimization is the mNeedsCheckAfterNavigation flag. This is set to
* true when we get any change, and false once there is no possibility
* something changed that we need to check on navigation. Navigation events
* tend to be a little tricky because we want to check the current word on
* exit if something has changed. If we navigate inside the word, we don't want
* to do anything. As a result, this flag is cleared in FinishNavigationEvent
* when we know that we are checking as a result of navigation.
*/
#include "mozInlineSpellChecker.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorBase.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorSpellCheck.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/Logging.h"
#include "mozilla/RangeUtils.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/KeyboardEvent.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/Selection.h"
#include "mozInlineSpellWordUtil.h"
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
#include "nsCOMPtr.h"
#include "nsCRT.h"
#include "nsGenericHTMLElement.h"
#include "nsRange.h"
#include "nsIPrefBranch.h"
#include "nsIPrefService.h"
#include "nsIRunnable.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "nsUnicharUtils.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsRange.h"
#include "nsContentUtils.h"
#include "nsIObserverService.h"
#include "prtime.h"
using mozilla::LogLevel;
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::ipc;
// the number of milliseconds that we will take at once to do spellchecking
#define INLINESPELL_CHECK_TIMEOUT 1
// The number of words to check before we look at the time to see if
// INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
// stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
// be too short to a low-end machine.
#define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
// The maximum number of words to check word via IPC.
#define INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK 25
// These notifications are broadcast when spell check starts and ends. STARTED
// must always be followed by ENDED.
#define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
#define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
static mozilla::LazyLogModule sInlineSpellCheckerLog("InlineSpellChecker");
static const PRTime kMaxSpellCheckTimeInUsec =
INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
mozInlineSpellStatus::mozInlineSpellStatus(
mozInlineSpellChecker* aSpellChecker, const Operation aOp,
RefPtr<nsRange>&& aRange, RefPtr<nsRange>&& aCreatedRange,
RefPtr<nsRange>&& aAnchorRange, const bool aForceNavigationWordCheck,
const int32_t aNewNavigationPositionOffset)
: mSpellChecker(aSpellChecker),
mRange(std::move(aRange)),
mOp(aOp),
mCreatedRange(std::move(aCreatedRange)),
mAnchorRange(std::move(aAnchorRange)),
mForceNavigationWordCheck(aForceNavigationWordCheck),
mNewNavigationPositionOffset(aNewNavigationPositionOffset) {}
// mozInlineSpellStatus::CreateForEditorChange
//
// This is the most complicated case. For changes, we need to compute the
// range of stuff that changed based on the old and new caret positions,
// as well as use a range possibly provided by the editor (start and end,
// which are usually nullptr) to get a range with the union of these.
// static
Result<UniquePtr<mozInlineSpellStatus>, nsresult>
mozInlineSpellStatus::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) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) {
return Err(NS_ERROR_FAILURE);
}
bool deleted = aEditSubAction == EditSubAction::eDeleteSelectedContent;
if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
// IME may remove the previous node if it cancels composition when
// there is no text around the composition.
deleted = !aPreviousNode->IsInComposedDoc();
}
// save the anchor point as a range so we can find the current word later
RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
aAnchorNode, aAnchorOffset);
if (NS_WARN_IF(!anchorRange)) {
return Err(NS_ERROR_FAILURE);
}
// Deletes are easy, the range is just the current anchor. We set the range
// to check to be empty, FinishInitOnEvent will fill in the range to be
// the current word.
RefPtr<nsRange> range = deleted ? nullptr : nsRange::Create(aPreviousNode);
// On insert save this range: DoSpellCheck optimizes things in this range.
// Otherwise, just leave this nullptr.
RefPtr<nsRange> createdRange =
(aEditSubAction == EditSubAction::eInsertText) ? range : nullptr;
UniquePtr<mozInlineSpellStatus> status{
/* The constructor is `private`, hence the explicit allocation. */
new mozInlineSpellStatus{&aSpellChecker,
deleted ? eOpChangeDelete : eOpChange,
std::move(range), std::move(createdRange),
std::move(anchorRange), false, 0}};
if (deleted) {
return status;
}
// ...we need to put the start and end in the correct order
ErrorResult errorResult;
int16_t cmpResult = status->mAnchorRange->ComparePoint(
*aPreviousNode, aPreviousOffset, errorResult);
if (NS_WARN_IF(errorResult.Failed())) {
return Err(errorResult.StealNSResult());
}
nsresult rv;
if (cmpResult < 0) {
// previous anchor node is before the current anchor
rv = status->mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset,
aAnchorNode, aAnchorOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
} else {
// previous anchor node is after (or the same as) the current anchor
rv = status->mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset,
aPreviousNode, aPreviousOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
}
// if we were given a range, we need to expand our range to encompass it
if (aStartNode && aEndNode) {
cmpResult =
status->mRange->ComparePoint(*aStartNode, aStartOffset, errorResult);
if (NS_WARN_IF(errorResult.Failed())) {
return Err(errorResult.StealNSResult());
}
if (cmpResult < 0) { // given range starts before
rv = status->mRange->SetStart(aStartNode, aStartOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
}
cmpResult =
status->mRange->ComparePoint(*aEndNode, aEndOffset, errorResult);
if (NS_WARN_IF(errorResult.Failed())) {
return Err(errorResult.StealNSResult());
}
if (cmpResult > 0) { // given range ends after
rv = status->mRange->SetEnd(aEndNode, aEndOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
}
}
return status;
}
// mozInlineSpellStatus::CreateForNavigation
//
// For navigation events, we just need to store the new and old positions.
//
// In some cases, we detect that we shouldn't check. If this event should
// not be processed, *aContinue will be false.
// static
Result<UniquePtr<mozInlineSpellStatus>, nsresult>
mozInlineSpellStatus::CreateForNavigation(
mozInlineSpellChecker& aSpellChecker, bool aForceCheck,
int32_t aNewPositionOffset, nsINode* aOldAnchorNode,
uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode,
uint32_t aNewAnchorOffset, bool* aContinue) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
RefPtr<nsRange> anchorRange = mozInlineSpellStatus::PositionToCollapsedRange(
aNewAnchorNode, aNewAnchorOffset);
if (NS_WARN_IF(!anchorRange)) {
return Err(NS_ERROR_FAILURE);
}
UniquePtr<mozInlineSpellStatus> status{
/* The constructor is `private`, hence the explicit allocation. */
new mozInlineSpellStatus{&aSpellChecker, eOpNavigation, nullptr, nullptr,
std::move(anchorRange), aForceCheck,
aNewPositionOffset}};
// get the root node for checking
EditorBase* editorBase = status->mSpellChecker->mEditorBase;
if (NS_WARN_IF(!editorBase)) {
return Err(NS_ERROR_FAILURE);
}
Element* root = editorBase->GetRoot();
if (NS_WARN_IF(!root)) {
return Err(NS_ERROR_FAILURE);
}
// the anchor node might not be in the DOM anymore, check
if (root && aOldAnchorNode &&
!aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) {
*aContinue = false;
return status;
}
status->mOldNavigationAnchorRange =
mozInlineSpellStatus::PositionToCollapsedRange(aOldAnchorNode,
aOldAnchorOffset);
if (NS_WARN_IF(!status->mOldNavigationAnchorRange)) {
return Err(NS_ERROR_FAILURE);
}
*aContinue = true;
return status;
}
// mozInlineSpellStatus::CreateForSelection
//
// It is easy for selections since we always re-check the spellcheck
// selection.
// static
UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForSelection(
mozInlineSpellChecker& aSpellChecker) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
UniquePtr<mozInlineSpellStatus> status{
/* The constructor is `private`, hence the explicit allocation. */
new mozInlineSpellStatus{&aSpellChecker, eOpSelection, nullptr, nullptr,
nullptr, false, 0}};
return status;
}
// mozInlineSpellStatus::CreateForRange
//
// Called to cause the spellcheck of the given range. This will look like
// a change operation over the given range.
// static
UniquePtr<mozInlineSpellStatus> mozInlineSpellStatus::CreateForRange(
mozInlineSpellChecker& aSpellChecker, nsRange* aRange) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
("%s: range=%p", __FUNCTION__, aRange));
UniquePtr<mozInlineSpellStatus> status{
/* The constructor is `private`, hence the explicit allocation. */
new mozInlineSpellStatus{&aSpellChecker, eOpChange, nullptr, nullptr,
nullptr, false, 0}};
status->mRange = aRange;
return status;
}
// mozInlineSpellStatus::FinishInitOnEvent
//
// Called when the event is triggered to complete initialization that
// might require the WordUtil. This calls to the operation-specific
// initializer, and also sets the range to be the entire element if it
// is nullptr.
//
// Watch out: the range might still be nullptr if there is nothing to do,
// the caller will have to check for this.
nsresult mozInlineSpellStatus::FinishInitOnEvent(
mozInlineSpellWordUtil& aWordUtil) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
("%s: mRange=%p", __FUNCTION__, mRange.get()));
nsresult rv;
if (!mRange) {
rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
getter_AddRefs(mRange));
NS_ENSURE_SUCCESS(rv, rv);
}
switch (mOp) {
case eOpChange:
if (mAnchorRange) return FillNoCheckRangeFromAnchor(aWordUtil);
break;
case eOpChangeDelete:
if (mAnchorRange) {
rv = FillNoCheckRangeFromAnchor(aWordUtil);
NS_ENSURE_SUCCESS(rv, rv);
}
// Delete events will have no range for the changed text (because it was
// deleted), and CreateForEditorChange will set it to nullptr. Here, we
// select the entire word to cause any underlining to be removed.
mRange = mNoCheckRange;
break;
case eOpNavigation:
return FinishNavigationEvent(aWordUtil);
case eOpSelection:
// this gets special handling in ResumeCheck
break;
case eOpResume:
// everything should be initialized already in this case
break;
default:
MOZ_ASSERT_UNREACHABLE("Bad operation");
return NS_ERROR_NOT_INITIALIZED;
}
return NS_OK;
}
// mozInlineSpellStatus::FinishNavigationEvent
//
// This verifies that we need to check the word at the previous caret
// position. Now that we have the word util, we can find the word belonging
// to the previous caret position. If the new position is inside that word,
// we don't want to do anything. In this case, we'll nullptr out mRange so
// that the caller will know not to continue.
//
// Notice that we don't set mNoCheckRange. We check here whether the cursor
// is in the word that needs checking, so it isn't necessary. Plus, the
// spellchecker isn't guaranteed to only check the given word, and it could
// remove the underline from the new word under the cursor.
nsresult mozInlineSpellStatus::FinishNavigationEvent(
mozInlineSpellWordUtil& aWordUtil) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
RefPtr<EditorBase> editorBase = mSpellChecker->mEditorBase;
if (!editorBase) {
return NS_ERROR_FAILURE; // editor is gone
}
MOZ_ASSERT(mAnchorRange, "No anchor for navigation!");
if (!mOldNavigationAnchorRange->IsPositioned()) {
return NS_ERROR_NOT_INITIALIZED;
}
// get the DOM position of the old caret, the range should be collapsed
nsCOMPtr<nsINode> oldAnchorNode =
mOldNavigationAnchorRange->GetStartContainer();
uint32_t oldAnchorOffset = mOldNavigationAnchorRange->StartOffset();
// find the word on the old caret position, this is the one that we MAY need
// to check
RefPtr<nsRange> oldWord;
nsresult rv = aWordUtil.GetRangeForWord(oldAnchorNode,
static_cast<int32_t>(oldAnchorOffset),
getter_AddRefs(oldWord));
NS_ENSURE_SUCCESS(rv, rv);
// aWordUtil.GetRangeForWord flushes pending notifications, check editor
// again.
if (!mSpellChecker->mEditorBase) {
return NS_ERROR_FAILURE; // editor is gone
}
// get the DOM position of the new caret, the range should be collapsed
nsCOMPtr<nsINode> newAnchorNode = mAnchorRange->GetStartContainer();
uint32_t newAnchorOffset = mAnchorRange->StartOffset();
// see if the new cursor position is in the word of the old cursor position
bool isInRange = false;
if (!mForceNavigationWordCheck) {
ErrorResult err;
isInRange = oldWord->IsPointInRange(
*newAnchorNode, newAnchorOffset + mNewNavigationPositionOffset, err);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
}
if (isInRange) {
// caller should give up
mRange = nullptr;
} else {
// check the old word
mRange = oldWord;
// Once we've spellchecked the current word, we don't need to spellcheck
// for any more navigation events.
mSpellChecker->mNeedsCheckAfterNavigation = false;
}
return NS_OK;
}
// mozInlineSpellStatus::FillNoCheckRangeFromAnchor
//
// Given the mAnchorRange object, computes the range of the word it is on
// (if any) and fills that range into mNoCheckRange. This is used for
// change and navigation events to know which word we should skip spell
// checking on
nsresult mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
mozInlineSpellWordUtil& aWordUtil) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
if (!mAnchorRange->IsPositioned()) {
return NS_ERROR_NOT_INITIALIZED;
}
nsCOMPtr<nsINode> anchorNode = mAnchorRange->GetStartContainer();
uint32_t anchorOffset = mAnchorRange->StartOffset();
return aWordUtil.GetRangeForWord(anchorNode,
static_cast<int32_t>(anchorOffset),
getter_AddRefs(mNoCheckRange));
}
// mozInlineSpellStatus::GetDocument
//
// Returns the Document object for the document for the
// current spellchecker.
Document* mozInlineSpellStatus::GetDocument() const {
if (!mSpellChecker->mEditorBase) {
return nullptr;
}
return mSpellChecker->mEditorBase->GetDocument();
}
// mozInlineSpellStatus::PositionToCollapsedRange
//
// Converts a given DOM position to a collapsed range covering that
// position. We use ranges to store DOM positions becuase they stay
// updated as the DOM is changed.
// static
already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange(
nsINode* aNode, uint32_t aOffset) {
if (NS_WARN_IF(!aNode)) {
return nullptr;
}
IgnoredErrorResult ignoredError;
RefPtr<nsRange> range =
nsRange::Create(aNode, aOffset, aNode, aOffset, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Creating collapsed range failed");
return range.forget();
}
// mozInlineSpellResume
class mozInlineSpellResume : public Runnable {
public:
mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
uint32_t aDisabledAsyncToken)
: Runnable("mozInlineSpellResume"),
mDisabledAsyncToken(aDisabledAsyncToken),
mStatus(std::move(aStatus)) {}
nsresult Post() {
nsCOMPtr<nsIRunnable> runnable(this);
return NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
EventQueuePriority::Idle);
}
NS_IMETHOD Run() override {
// Discard the resumption if the spell checker was disabled after the
// resumption was scheduled.
if (mDisabledAsyncToken ==
mStatus->mSpellChecker->GetDisabledAsyncToken()) {
mStatus->mSpellChecker->ResumeCheck(std::move(mStatus));
}
return NS_OK;
}
private:
uint32_t mDisabledAsyncToken;
UniquePtr<mozInlineSpellStatus> mStatus;
};
// Used as the nsIEditorSpellCheck::InitSpellChecker callback.
class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
~InitEditorSpellCheckCallback() {}
public:
NS_DECL_ISUPPORTS
explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
: mSpellChecker(aSpellChecker) {}
NS_IMETHOD EditorSpellCheckDone() override {
return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
}
void Cancel() { mSpellChecker = nullptr; }
private:
RefPtr<mozInlineSpellChecker> mSpellChecker;
};
NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
NS_IMPL_CYCLE_COLLECTION_WEAK(mozInlineSpellChecker, mEditorBase, mSpellCheck,
mCurrentSelectionAnchorNode)
mozInlineSpellChecker::SpellCheckingState
mozInlineSpellChecker::gCanEnableSpellChecking =
mozInlineSpellChecker::SpellCheck_Uninitialized;
mozInlineSpellChecker::mozInlineSpellChecker()
: mNumWordsInSpellSelection(0),
mMaxNumWordsInSpellSelection(
StaticPrefs::extensions_spellcheck_inline_max_misspellings()),
mNumPendingSpellChecks(0),
mNumPendingUpdateCurrentDictionary(0),
mDisabledAsyncToken(0),
mNeedsCheckAfterNavigation(false),
mFullSpellCheckScheduled(false),
mIsListeningToEditSubActions(false) {}
mozInlineSpellChecker::~mozInlineSpellChecker() {}
EditorSpellCheck* mozInlineSpellChecker::GetEditorSpellCheck() {
return mSpellCheck ? mSpellCheck : mPendingSpellCheck;
}
NS_IMETHODIMP
mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck** aSpellCheck) {
*aSpellCheck = mSpellCheck;
NS_IF_ADDREF(*aSpellCheck);
return NS_OK;
}
NS_IMETHODIMP
mozInlineSpellChecker::Init(nsIEditor* aEditor) {
mEditorBase = aEditor ? aEditor->AsEditorBase() : nullptr;
return NS_OK;
}
// mozInlineSpellChecker::Cleanup
//
// Called by the editor when the editor is going away. This is important
// because we remove listeners. We do NOT clean up anything else in this
// function, because it can get called while DoSpellCheck is running!
//
// Getting the style information there can cause DOM notifications to be
// flushed, which can cause editors to go away which will bring us here.
// We can not do anything that will cause DoSpellCheck to freak out.
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
mozInlineSpellChecker::Cleanup(bool aDestroyingFrames) {
mNumWordsInSpellSelection = 0;
RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
nsresult rv = NS_OK;
if (!spellCheckSelection) {
// Ensure we still unregister event listeners (but return a failure code)
UnregisterEventListeners();
rv = NS_ERROR_FAILURE;
} else {
if (!aDestroyingFrames) {
spellCheckSelection->RemoveAllRanges(IgnoreErrors());
}
rv = UnregisterEventListeners();
}
// Notify ENDED observers now. If we wait to notify as we normally do when
// these async operations finish, then in the meantime the editor may create
// another inline spell checker and cause more STARTED and ENDED
// notifications to be broadcast. Interleaved notifications for the same
// editor but different inline spell checkers could easily confuse
// observers. They may receive two consecutive STARTED notifications for
// example, which we guarantee will not happen.
RefPtr<EditorBase> editorBase = std::move(mEditorBase);
if (mPendingSpellCheck) {
// Cancel the pending editor spell checker initialization.
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback->Cancel();
mPendingInitEditorSpellCheckCallback = nullptr;
ChangeNumPendingSpellChecks(-1, editorBase);
}
// Increment this token so that pending UpdateCurrentDictionary calls and
// scheduled spell checks are discarded when they finish.
mDisabledAsyncToken++;
if (mNumPendingUpdateCurrentDictionary > 0) {
// Account for pending UpdateCurrentDictionary calls.
ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary,
editorBase);
mNumPendingUpdateCurrentDictionary = 0;
}
if (mNumPendingSpellChecks > 0) {
// If mNumPendingSpellChecks is still > 0 at this point, the remainder is
// pending scheduled spell checks.
ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editorBase);
}
mFullSpellCheckScheduled = false;
return rv;
}
// mozInlineSpellChecker::CanEnableInlineSpellChecking
//
// This function can be called to see if it seems likely that we can enable
// spellchecking before actually creating the InlineSpellChecking objects.
//
// The problem is that we can't get the dictionary list without actually
// creating a whole bunch of spellchecking objects. This function tries to
// do that and caches the result so we don't have to keep allocating those
// objects if there are no dictionaries or spellchecking.
//
// Whenever dictionaries are added or removed at runtime, this value must be
// updated before an observer notification is sent out about the change, to
// avoid editors getting a wrong cached result.
bool // static
mozInlineSpellChecker::CanEnableInlineSpellChecking() {
if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
gCanEnableSpellChecking = SpellCheck_NotAvailable;
nsCOMPtr<nsIEditorSpellCheck> spellchecker = new EditorSpellCheck();
bool canSpellCheck = false;
nsresult rv = spellchecker->CanSpellCheck(&canSpellCheck);
NS_ENSURE_SUCCESS(rv, false);
if (canSpellCheck) gCanEnableSpellChecking = SpellCheck_Available;
}
return (gCanEnableSpellChecking == SpellCheck_Available);
}
void // static
mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking() {
gCanEnableSpellChecking = SpellCheck_Uninitialized;
}
// mozInlineSpellChecker::RegisterEventListeners
//
// The inline spell checker listens to mouse events and keyboard navigation
// events.
nsresult mozInlineSpellChecker::RegisterEventListeners() {
if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
return NS_ERROR_FAILURE;
}
StartToListenToEditSubActions();
RefPtr<Document> doc = mEditorBase->GetDocument();
if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
return NS_ERROR_FAILURE;
}
EventListenerManager* eventListenerManager =
doc->GetOrCreateListenerManager();
if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
return NS_ERROR_FAILURE;
}
eventListenerManager->AddEventListenerByType(
this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
return NS_OK;
}
// mozInlineSpellChecker::UnregisterEventListeners
nsresult mozInlineSpellChecker::UnregisterEventListeners() {
if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase))) {
return NS_ERROR_FAILURE;
}
EndListeningToEditSubActions();
RefPtr<Document> doc = mEditorBase->GetDocument();
if (MOZ_UNLIKELY(NS_WARN_IF(!doc))) {
return NS_ERROR_FAILURE;
}
EventListenerManager* eventListenerManager =
doc->GetOrCreateListenerManager();
if (MOZ_UNLIKELY(NS_WARN_IF(!eventListenerManager))) {
return NS_ERROR_FAILURE;
}
eventListenerManager->RemoveEventListenerByType(
this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"click"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
return NS_OK;
}
// mozInlineSpellChecker::GetEnableRealTimeSpell
NS_IMETHODIMP
mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled) {
NS_ENSURE_ARG_POINTER(aEnabled);
*aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
return NS_OK;
}
// mozInlineSpellChecker::SetEnableRealTimeSpell
NS_IMETHODIMP
mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled) {
if (!aEnabled) {
mSpellCheck = nullptr;
return Cleanup(false);
}
if (mSpellCheck) {
// spellcheck the current contents. SpellCheckRange doesn't supply a created
// range to DoSpellCheck, which in our case is the entire range. But this
// optimization doesn't matter because there is nothing in the spellcheck
// selection when starting, which triggers a better optimization.
return SpellCheckRange(nullptr);
}
if (mPendingSpellCheck) {
// The editor spell checker is already being initialized.
return NS_OK;
}
mPendingSpellCheck = new EditorSpellCheck();
mPendingSpellCheck->SetFilterType(nsIEditorSpellCheck::FILTERTYPE_MAIL);
mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
nsresult rv = mPendingSpellCheck->InitSpellChecker(
mEditorBase, false, mPendingInitEditorSpellCheckCallback);
if (NS_FAILED(rv)) {
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback = nullptr;
NS_ENSURE_SUCCESS(rv, rv);
}
ChangeNumPendingSpellChecks(1);
return NS_OK;
}
// Called when nsIEditorSpellCheck::InitSpellChecker completes.
nsresult mozInlineSpellChecker::EditorSpellCheckInited() {
MOZ_ASSERT(mPendingSpellCheck, "Spell check should be pending!");
// spell checking is enabled, register our event listeners to track navigation
RegisterEventListeners();
mSpellCheck = mPendingSpellCheck;
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback = nullptr;
ChangeNumPendingSpellChecks(-1);
// spellcheck the current contents. SpellCheckRange doesn't supply a created
// range to DoSpellCheck, which in our case is the entire range. But this
// optimization doesn't matter because there is nothing in the spellcheck
// selection when starting, which triggers a better optimization.
return SpellCheckRange(nullptr);
}
// Changes the number of pending spell checks by the given delta. If the number
// becomes zero or nonzero, observers are notified. See NotifyObservers for
// info on the aEditor parameter.
void mozInlineSpellChecker::ChangeNumPendingSpellChecks(
int32_t aDelta, EditorBase* aEditorBase) {
int8_t oldNumPending = mNumPendingSpellChecks;
mNumPendingSpellChecks += aDelta;
MOZ_ASSERT(mNumPendingSpellChecks >= 0,
"Unbalanced ChangeNumPendingSpellChecks calls!");
if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditorBase);
} else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditorBase);
}
}
// Broadcasts the given topic to observers. aEditor is passed to observers if
// nonnull; otherwise mEditorBase is passed.
void mozInlineSpellChecker::NotifyObservers(const char* aTopic,
EditorBase* aEditorBase) {
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (!os) return;
// XXX Do we need to grab the editor here? If it's necessary, each observer
// should do it instead.
RefPtr<EditorBase> editorBase = aEditorBase ? aEditorBase : mEditorBase.get();
os->NotifyObservers(static_cast<nsIEditor*>(editorBase.get()), aTopic,
nullptr);
}
// mozInlineSpellChecker::SpellCheckAfterEditorChange
//
// Called by the editor when nearly anything happens to change the content.
//
// The start and end positions specify a range for the thing that happened,
// but these are usually nullptr, even when you'd think they would be useful
// because you want the range (for example, pasting). We ignore them in
// this case.
nsresult mozInlineSpellChecker::SpellCheckAfterEditorChange(
EditSubAction aEditSubAction, Selection& aSelection,
nsINode* aPreviousSelectedNode, uint32_t aPreviousSelectedOffset,
nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
uint32_t aEndOffset) {
nsresult rv;
if (!mSpellCheck) return NS_OK; // disabling spell checking is not an error
// this means something has changed, and we never check the current word,
// therefore, we should spellcheck for subsequent caret navigations
mNeedsCheckAfterNavigation = true;
// the anchor node is the position of the caret
Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
mozInlineSpellStatus::CreateForEditorChange(
*this, aEditSubAction, aSelection.GetAnchorNode(),
aSelection.AnchorOffset(), aPreviousSelectedNode,
aPreviousSelectedOffset, aStartNode, aStartOffset, aEndNode,
aEndOffset);
if (NS_WARN_IF(res.isErr())) {
return res.unwrapErr();
}
rv = ScheduleSpellCheck(res.unwrap());
NS_ENSURE_SUCCESS(rv, rv);
// remember the current caret position after every change
SaveCurrentSelectionPosition();
return NS_OK;
}
// mozInlineSpellChecker::SpellCheckRange
//
// Spellchecks all the words in the given range.
// Supply a nullptr range and this will check the entire editor.
nsresult mozInlineSpellChecker::SpellCheckRange(nsRange* aRange) {
if (!mSpellCheck) {
NS_WARNING_ASSERTION(
mPendingSpellCheck,
"Trying to spellcheck, but checking seems to be disabled");
return NS_ERROR_NOT_INITIALIZED;
}
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForRange(*this, aRange);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::GetMisspelledWord
NS_IMETHODIMP
mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, uint32_t aOffset,
nsRange** newword) {
if (NS_WARN_IF(!aNode)) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
if (NS_WARN_IF(!spellCheckSelection)) {
return NS_ERROR_FAILURE;
}
return IsPointInSelection(*spellCheckSelection, aNode, aOffset, newword);
}
// mozInlineSpellChecker::ReplaceWord
NS_IMETHODIMP
mozInlineSpellChecker::ReplaceWord(nsINode* aNode, uint32_t aOffset,
const nsAString& aNewWord) {
if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(aNewWord.IsEmpty())) {
return NS_ERROR_FAILURE;
}
RefPtr<nsRange> range;
nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
NS_ENSURE_SUCCESS(res, res);
if (!range) {
return NS_OK;
}
// In usual cases, any words shouldn't include line breaks, but technically,
// they may include and we need to avoid `HTMLTextAreaElement.value` returns
// \r. Therefore, we need to handle it here.
nsString newWord(aNewWord);
if (mEditorBase->IsTextEditor()) {
nsContentUtils::PlatformToDOMLineBreaks(newWord);
}
// Blink dispatches cancelable `beforeinput` event at collecting misspelled
// word so that we should allow to dispatch cancelable event.
RefPtr<EditorBase> editorBase(mEditorBase);
DebugOnly<nsresult> rv = editorBase->ReplaceTextAsAction(
newWord, range, EditorBase::AllowBeforeInputEventCancelable::Yes);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
return NS_OK;
}
// mozInlineSpellChecker::AddWordToDictionary
NS_IMETHODIMP
mozInlineSpellChecker::AddWordToDictionary(const nsAString& word) {
NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
nsresult rv = mSpellCheck->AddWordToDictionary(word);
NS_ENSURE_SUCCESS(rv, rv);
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForSelection(*this);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::RemoveWordFromDictionary
NS_IMETHODIMP
mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString& word) {
NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
nsresult rv = mSpellCheck->RemoveWordFromDictionary(word);
NS_ENSURE_SUCCESS(rv, rv);
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForRange(*this, nullptr);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::IgnoreWord
NS_IMETHODIMP
mozInlineSpellChecker::IgnoreWord(const nsAString& word) {
NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(word);
NS_ENSURE_SUCCESS(rv, rv);
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForSelection(*this);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::IgnoreWords
NS_IMETHODIMP
mozInlineSpellChecker::IgnoreWords(const nsTArray<nsString>& aWordsToIgnore) {
NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
// add each word to the ignore list and then recheck the document
for (auto& word : aWordsToIgnore) {
mSpellCheck->IgnoreWordAllOccurrences(word);
}
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForSelection(*this);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::MakeSpellCheckRange
//
// Given begin and end positions, this function constructs a range as
// required for ScheduleSpellCheck. If the start and end nodes are nullptr,
// then the entire range will be selected, and you can supply -1 as the
// offset to the end range to select all of that node.
//
// If the resulting range would be empty, nullptr is put into *aRange and the
// function succeeds.
nsresult mozInlineSpellChecker::MakeSpellCheckRange(nsINode* aStartNode,
int32_t aStartOffset,
nsINode* aEndNode,
int32_t aEndOffset,
nsRange** aRange) const {
nsresult rv;
*aRange = nullptr;
if (NS_WARN_IF(!mEditorBase)) {
return NS_ERROR_FAILURE;
}
RefPtr<Document> doc = mEditorBase->GetDocument();
if (NS_WARN_IF(!doc)) {
return NS_ERROR_FAILURE;
}
RefPtr<nsRange> range = nsRange::Create(doc);
// possibly use full range of the editor
if (!aStartNode || !aEndNode) {
Element* domRootElement = mEditorBase->GetRoot();
if (NS_WARN_IF(!domRootElement)) {
return NS_ERROR_FAILURE;
}
aStartNode = aEndNode = domRootElement;
aStartOffset = 0;
aEndOffset = -1;
}
if (aEndOffset == -1) {
// It's hard to say whether it's better to just do nsINode::GetChildCount or
// get the ChildNodes() and then its length. The latter is faster if we
// keep going through this code for the same nodes (because it caches the
// length). The former is faster if we keep getting different nodes here...
//
// Let's do the thing which can't end up with bad O(N^2) behavior.
aEndOffset = aEndNode->ChildNodes()->Length();
}
// sometimes we are are requested to check an empty range (possibly an empty
// document). This will result in assertions later.
if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK;
if (aEndOffset) {
rv = range->SetStartAndEnd(aStartNode, aStartOffset, aEndNode, aEndOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = range->SetStartAndEnd(RawRangeBoundary(aStartNode, aStartOffset),
RangeUtils::GetRawRangeBoundaryAfter(aEndNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
range.swap(*aRange);
return NS_OK;
}
nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsINode* aStartNode,
int32_t aStartOffset,
nsINode* aEndNode,
int32_t aEndOffset) {
RefPtr<nsRange> range;
nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, aEndNode,
aEndOffset, getter_AddRefs(range));
NS_ENSURE_SUCCESS(rv, rv);
if (!range) return NS_OK; // range is empty: nothing to do
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForRange(*this, range);
return ScheduleSpellCheck(std::move(status));
}
// mozInlineSpellChecker::ShouldSpellCheckNode
//
// There are certain conditions when we don't want to spell check a node. In
// particular quotations, moz signatures, etc. This routine returns false
// for these cases.
// static
bool mozInlineSpellChecker::ShouldSpellCheckNode(EditorBase* aEditorBase,
nsINode* aNode) {
MOZ_ASSERT(aNode);
if (!aNode->IsContent()) return false;
nsIContent* content = aNode->AsContent();
if (aEditorBase->IsMailEditor()) {
nsIContent* parent = content->GetParent();
while (parent) {
if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::cite, eIgnoreCase)) {
return false;
}
if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
nsGkAtoms::mozsignature,
eIgnoreCase)) {
return false;
}
if (parent->IsHTMLElement(nsGkAtoms::div) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
nsGkAtoms::mozfwcontainer,
eIgnoreCase)) {
return false;
}
parent = parent->GetParent();
}
} else {
// Check spelling only if the node is editable, and GetSpellcheck() is true
// on the nearest HTMLElement ancestor.
if (!content->IsEditable()) {
return false;
}
// Make sure that we can always turn on spell checking for inputs/textareas.
// Note that because of the previous check, at this point we know that the
// node is editable.
if (content->IsInNativeAnonymousSubtree()) {
nsIContent* node = content->GetParent();
while (node && node->IsInNativeAnonymousSubtree()) {
node = node->GetParent();
}
if (node && node->IsTextControlElement()) {
return true;
}
}
// Get HTML element ancestor (might be aNode itself, although probably that
// has to be a text node in real life here)
nsIContent* parent = content;
while (!parent->IsHTMLElement()) {
parent = parent->GetParent();
if (!parent) {
return true;
}
}
// See if it's spellcheckable
return static_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
}
return true;
}
// mozInlineSpellChecker::ScheduleSpellCheck
//
// This is called by code to do the actual spellchecking. We will set up
// the proper structures for calls to DoSpellCheck.
nsresult mozInlineSpellChecker::ScheduleSpellCheck(
UniquePtr<mozInlineSpellStatus>&& aStatus) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
("%s: mFullSpellCheckScheduled=%i", __FUNCTION__,
mFullSpellCheckScheduled));
if (mFullSpellCheckScheduled) {
// Just ignore this; we're going to spell-check everything anyway
return NS_OK;
}
// Cache the value because we are going to move aStatus's ownership to
// the new created mozInlineSpellResume instance.
bool isFullSpellCheck = aStatus->IsFullSpellCheck();
RefPtr<mozInlineSpellResume> resume =
new mozInlineSpellResume(std::move(aStatus), mDisabledAsyncToken);
NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = resume->Post();
if (NS_SUCCEEDED(rv)) {
if (isFullSpellCheck) {
// We're going to check everything. Suppress further spell-check attempts
// until that happens.
mFullSpellCheckScheduled = true;
}
ChangeNumPendingSpellChecks(1);
}
return rv;
}
// mozInlineSpellChecker::DoSpellCheckSelection
//
// Called to re-check all misspelled words. We iterate over all ranges in
// the selection and call DoSpellCheck on them. This is used when a word
// is ignored or added to the dictionary: all instances of that word should
// be removed from the selection.
//
// FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
// Typically, checking this small amount of text is relatively fast, but
// for large numbers of words, a lag may be noticeable.
nsresult mozInlineSpellChecker::DoSpellCheckSelection(
mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection) {
nsresult rv;
// clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
mNumWordsInSpellSelection = 0;
// Since we could be modifying the ranges for the spellCheckSelection while
// looping on the spell check selection, keep a separate array of range
// elements inside the selection
nsTArray<RefPtr<nsRange>> ranges;
const uint32_t rangeCount = aSpellCheckSelection->RangeCount();
for (const uint32_t idx : IntegerRange(rangeCount)) {
MOZ_ASSERT(aSpellCheckSelection->RangeCount() == rangeCount);
nsRange* range = aSpellCheckSelection->GetRangeAt(idx);
MOZ_ASSERT(range);
if (MOZ_LIKELY(range)) {
ranges.AppendElement(range);
}
}
// We have saved the ranges above. Clearing the spellcheck selection here
// isn't necessary (rechecking each word will modify it as necessary) but
// provides better performance. By ensuring that no ranges need to be
// removed in DoSpellCheck, we can save checking range inclusion which is
// slow.
aSpellCheckSelection->RemoveAllRanges(IgnoreErrors());
// We use this state object for all calls, and just update its range. Note
// that we don't need to call FinishInit since we will be filling in the
// necessary information.
UniquePtr<mozInlineSpellStatus> status =
mozInlineSpellStatus::CreateForRange(*this, nullptr);
bool doneChecking;
for (uint32_t idx : IntegerRange(rangeCount)) {
// We can consider this word as "added" since we know it has no spell
// check range over it that needs to be deleted. All the old ranges
// were cleared above. We also need to clear the word count so that we
// check all words instead of stopping early.
status->mRange = ranges[idx];
rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status, &doneChecking);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(
doneChecking,
"We gave the spellchecker one word, but it didn't finish checking?!?!");
}
return NS_OK;
}
class MOZ_STACK_CLASS mozInlineSpellChecker::SpellCheckerSlice {
public:
/**
* @param aStatus must be non-nullptr.
*/
SpellCheckerSlice(mozInlineSpellChecker& aInlineSpellChecker,
mozInlineSpellWordUtil& aWordUtil,
mozilla::dom::Selection& aSpellCheckSelection,
const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus,
bool& aDoneChecking)
: mInlineSpellChecker{aInlineSpellChecker},
mWordUtil{aWordUtil},
mSpellCheckSelection{aSpellCheckSelection},
mStatus{aStatus},
mDoneChecking{aDoneChecking} {
MOZ_ASSERT(aStatus);
}
[[nodiscard]] nsresult Execute();
private:
// Creates an async request to check the words and update the ranges for the
// misspellings.
//
// @param aWords normalized words corresponding to aNodeOffsetRangesForWords.
// @param aOldRangesForSomeWords ranges from previous spellcheckings which
// might need to be removed. Its length might
// differ from `aWords.Length()`.
// @param aNodeOffsetRangesForWords One range for each word in aWords. So
// `aNodeOffsetRangesForWords.Length() ==
// aWords.Length()`.
void CheckWordsAndUpdateRangesForMisspellings(
const nsTArray<nsString>& aWords,
nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords);
void RemoveRanges(const nsTArray<RefPtr<nsRange>>& aRanges);
bool ShouldSpellCheckRange(const nsRange& aRange) const;
bool IsInNoCheckRange(const nsINode& aNode, int32_t aOffset) const;
mozInlineSpellChecker& mInlineSpellChecker;
mozInlineSpellWordUtil& mWordUtil;
mozilla::dom::Selection& mSpellCheckSelection;
const mozilla::UniquePtr<mozInlineSpellStatus>& mStatus;
bool& mDoneChecking;
};
bool mozInlineSpellChecker::SpellCheckerSlice::ShouldSpellCheckRange(
const nsRange& aRange) const {
if (aRange.Collapsed()) {
return false;
}
nsINode* beginNode = aRange.GetStartContainer();
nsINode* endNode = aRange.GetEndContainer();
const nsINode* rootNode = mWordUtil.GetRootNode();
return beginNode->IsInComposedDoc() && endNode->IsInComposedDoc() &&
beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) &&
endNode->IsShadowIncludingInclusiveDescendantOf(rootNode);
}
bool mozInlineSpellChecker::SpellCheckerSlice::IsInNoCheckRange(
const nsINode& aNode, int32_t aOffset) const {
ErrorResult erv;
return mStatus->GetNoCheckRange() &&
mStatus->GetNoCheckRange()->IsPointInRange(aNode, aOffset, erv);
}
void mozInlineSpellChecker::SpellCheckerSlice::RemoveRanges(
const nsTArray<RefPtr<nsRange>>& aRanges) {
for (uint32_t i = 0; i < aRanges.Length(); i++) {
mInlineSpellChecker.RemoveRange(&mSpellCheckSelection, aRanges[i]);
}
}
// mozInlineSpellChecker::SpellCheckerSlice::Execute
//
// This function checks words intersecting the given range, excluding those
// inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
// will have any spell selection removed (this is used to hide the
// underlining for the word that the caret is in). aNoCheckRange should be
// on word boundaries.
//
// mResume->mCreatedRange is a possibly nullptr range of new text that was
// inserted. Inside this range, we don't bother to check whether things are
// inside the spellcheck selection, which speeds up large paste operations
// considerably.
//
// Normal case when editing text by typing
// h e l l o w o r k d h o w a r e y o u
// ^ caret
// [-------] mRange
// [-------] mNoCheckRange
// -> does nothing (range is the same as the no check range)
//
// Case when pasting:
// [---------- pasted text ----------]
// h e l l o w o r k d h o w a r e y o u
// ^ caret
// [---] aNoCheckRange
// -> recheck all words in range except those in aNoCheckRange
//
// If checking is complete, *aDoneChecking will be set. If there is more
// but we ran out of time, this will be false and the range will be
// updated with the stuff that still needs checking.
nsresult mozInlineSpellChecker::SpellCheckerSlice::Execute() {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
mDoneChecking = true;
if (NS_WARN_IF(!mInlineSpellChecker.mSpellCheck)) {
return NS_ERROR_NOT_INITIALIZED;
}
if (mInlineSpellChecker.IsSpellCheckSelectionFull()) {
return NS_OK;
}
// get the editor for ShouldSpellCheckNode, this may fail in reasonable
// circumstances since the editor could have gone away
RefPtr<EditorBase> editorBase = mInlineSpellChecker.mEditorBase;
if (!editorBase || editorBase->Destroyed()) {
return NS_ERROR_FAILURE;
}
if (!ShouldSpellCheckRange(*mStatus->mRange)) {
// Just bail out and don't try to spell-check this
return NS_OK;
}
// see if the selection has any ranges, if not, then we can optimize checking
// range inclusion later (we have no ranges when we are initially checking or
// when there are no misspelled words yet).
const int32_t originalRangeCount = mSpellCheckSelection.RangeCount();
// set the starting DOM position to be the beginning of our range
if (nsresult rv = mWordUtil.SetPositionAndEnd(
mStatus->mRange->GetStartContainer(), mStatus->mRange->StartOffset(),
mStatus->mRange->GetEndContainer(), mStatus->mRange->EndOffset());
NS_FAILED(rv)) {
// Just bail out and don't try to spell-check this
return NS_OK;
}
// aWordUtil.SetPosition flushes pending notifications, check editor again.
if (!mInlineSpellChecker.mEditorBase) {
return NS_ERROR_FAILURE;
}
int32_t wordsChecked = 0;
PRTime beginTime = PR_Now();
nsTArray<nsString> normalizedWords;
nsTArray<RefPtr<nsRange>> oldRangesToRemove;
nsTArray<NodeOffsetRange> checkRanges;
mozInlineSpellWordUtil::Word word;
static const size_t requestChunkSize =
INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK;
while (mWordUtil.GetNextWord(word)) {
// get the range for the current word.
nsINode* const beginNode = word.mNodeOffsetRange.Begin().Node();
nsINode* const endNode = word.mNodeOffsetRange.End().Node();
// TODO: Make them `uint32_t`
const int32_t beginOffset = word.mNodeOffsetRange.Begin().Offset();
const int32_t endOffset = word.mNodeOffsetRange.End().Offset();
// see if we've done enough words in this round and run out of time.
if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
// stop checking, our time limit has been exceeded.
MOZ_LOG(
sInlineSpellCheckerLog, LogLevel::Verbose,
("%s: we have run out of time, schedule next round.", __FUNCTION__));
CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
std::move(oldRangesToRemove),
std::move(checkRanges));
// move the range to encompass the stuff that needs checking.
nsresult rv = mStatus->mRange->SetStart(
beginNode, AssertedCast<uint32_t>(beginOffset));
if (NS_FAILED(rv)) {
// The range might be unhappy because the beginning is after the
// end. This is possible when the requested end was in the middle
// of a word, just ignore this situation and assume we're done.
return NS_OK;
}
mDoneChecking = false;
return NS_OK;
}
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
("%s: got word \"%s\"%s", __FUNCTION__,
NS_ConvertUTF16toUTF8(word.mText).get(),
word.mSkipChecking ? " (not checking)" : ""));
// see if there is a spellcheck range that already intersects the word
// and remove it. We only need to remove old ranges, so don't bother if
// there were no ranges when we started out.
if (originalRangeCount > 0) {
ErrorResult erv;
// likewise, if this word is inside new text, we won't bother testing
if (!mStatus->GetCreatedRange() ||
!mStatus->GetCreatedRange()->IsPointInRange(
*beginNode, AssertedCast<uint32_t>(beginOffset), erv)) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
("%s: removing ranges for some interval.", __FUNCTION__));
nsTArray<RefPtr<nsRange>> ranges;
mSpellCheckSelection.GetRangesForInterval(
*beginNode, AssertedCast<uint32_t>(beginOffset), *endNode,
AssertedCast<uint32_t>(endOffset), true, ranges, erv);
ENSURE_SUCCESS(erv, erv.StealNSResult());
oldRangesToRemove.AppendElements(std::move(ranges));
}
}
// some words are special and don't need checking
if (word.mSkipChecking) {
continue;
}
// some nodes we don't spellcheck
if (!mozInlineSpellChecker::ShouldSpellCheckNode(editorBase, beginNode)) {
continue;
}
// Don't check spelling if we're inside the noCheckRange. This needs to
// be done after we clear any old selection because the excluded word
// might have been previously marked.
//
// We do a simple check to see if the beginning of our word is in the
// exclusion range. Because the exclusion range is a multiple of a word,
// this is sufficient.
if (IsInNoCheckRange(*beginNode, beginOffset)) {
continue;
}
// check spelling and add to selection if misspelled
mozInlineSpellWordUtil::NormalizeWord(word.mText);
normalizedWords.AppendElement(word.mText);
checkRanges.AppendElement(word.mNodeOffsetRange);
wordsChecked++;
if (normalizedWords.Length() >= requestChunkSize) {
CheckWordsAndUpdateRangesForMisspellings(normalizedWords,
std::move(oldRangesToRemove),
std::move(checkRanges));
normalizedWords.Clear();
oldRangesToRemove = {};
// Set new empty data for spellcheck range in DOM to avoid
// clang-tidy detection.
checkRanges = nsTArray<NodeOffsetRange>();
}
}
CheckWordsAndUpdateRangesForMisspellings(
normalizedWords, std::move(oldRangesToRemove), std::move(checkRanges));
return NS_OK;
}
nsresult mozInlineSpellChecker::DoSpellCheck(
mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection,
const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) {
MOZ_ASSERT(aDoneChecking);
SpellCheckerSlice spellCheckerSlice{*this, aWordUtil, *aSpellCheckSelection,
aStatus, *aDoneChecking};
return spellCheckerSlice.Execute();
}
// An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
class MOZ_RAII AutoChangeNumPendingSpellChecks final {
public:
explicit AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
int32_t aDelta)
: mSpellChecker(aSpellChecker), mDelta(aDelta) {}
~AutoChangeNumPendingSpellChecks() {
mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
}
private:
RefPtr<mozInlineSpellChecker> mSpellChecker;
int32_t mDelta;
};
void mozInlineSpellChecker::SpellCheckerSlice::
CheckWordsAndUpdateRangesForMisspellings(
const nsTArray<nsString>& aWords,
nsTArray<RefPtr<nsRange>>&& aOldRangesForSomeWords,
nsTArray<NodeOffsetRange>&& aNodeOffsetRangesForWords) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
("%s: aWords.Length()=%i", __FUNCTION__,
static_cast<int>(aWords.Length())));
MOZ_ASSERT(aWords.Length() == aNodeOffsetRangesForWords.Length());
// TODO:
// aOldRangesForSomeWords is sorted in the same order as aWords. Could be used
// to remove ranges more efficiently.
if (aWords.IsEmpty()) {
RemoveRanges(aOldRangesForSomeWords);
return;
}
mInlineSpellChecker.ChangeNumPendingSpellChecks(1);
RefPtr<mozInlineSpellChecker> inlineSpellChecker = &mInlineSpellChecker;
RefPtr<Selection> spellCheckerSelection = &mSpellCheckSelection;
uint32_t token = mInlineSpellChecker.mDisabledAsyncToken;
mInlineSpellChecker.mSpellCheck->CheckCurrentWordsNoSuggest(aWords)->Then(
GetMainThreadSerialEventTarget(), __func__,
[inlineSpellChecker, spellCheckerSelection,
nodeOffsetRangesForWords = std::move(aNodeOffsetRangesForWords),
oldRangesForSomeWords = std::move(aOldRangesForSomeWords),
token](const nsTArray<bool>& aIsMisspelled) {
if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
// This result is never used
return;
}
if (!inlineSpellChecker->mEditorBase ||
inlineSpellChecker->mEditorBase->Destroyed()) {
return;
}
AutoChangeNumPendingSpellChecks pendingChecks(inlineSpellChecker, -1);
if (inlineSpellChecker->IsSpellCheckSelectionFull()) {
return;
}
inlineSpellChecker->UpdateRangesForMisspelledWords(
nodeOffsetRangesForWords, oldRangesForSomeWords, aIsMisspelled,
*spellCheckerSelection);
},
[inlineSpellChecker, token](nsresult aRv) {
if (!inlineSpellChecker->mEditorBase ||
inlineSpellChecker->mEditorBase->Destroyed()) {
return;
}
if (token != inlineSpellChecker->GetDisabledAsyncToken()) {
// This result is never used
return;
}
inlineSpellChecker->ChangeNumPendingSpellChecks(-1);
});
}
// mozInlineSpellChecker::ResumeCheck
//
// Called by the resume event when it fires. We will try to pick up where
// the last resume left off.
nsresult mozInlineSpellChecker::ResumeCheck(
UniquePtr<mozInlineSpellStatus>&& aStatus) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
// Observers should be notified that spell check has ended only after spell
// check is done below, but since there are many early returns in this method
// and the number of pending spell checks must be decremented regardless of
// whether the spell check actually happens, use this RAII object.
AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
if (aStatus->IsFullSpellCheck()) {
// Allow posting new spellcheck resume events from inside
// ResumeCheck, now that we're actually firing.
MOZ_ASSERT(mFullSpellCheckScheduled,
"How could this be false? The full spell check is "
"calling us!!");
mFullSpellCheckScheduled = false;
}
if (!mSpellCheck) return NS_OK; // spell checking has been turned off
if (!mEditorBase) {
return NS_OK;
}
Maybe<mozInlineSpellWordUtil> wordUtil{
mozInlineSpellWordUtil::Create(*mEditorBase)};
if (!wordUtil) {
return NS_OK; // editor doesn't like us, don't assert
}
RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection();
if (NS_WARN_IF(!spellCheckSelection)) {
return NS_ERROR_FAILURE;
}
nsTArray<nsCString> currentDictionaries;
nsresult rv = mSpellCheck->GetCurrentDictionaries(currentDictionaries);
if (NS_FAILED(rv)) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug,
("%s: no active dictionary.", __FUNCTION__));
// no active dictionary
for (const uint32_t index :
Reversed(IntegerRange(spellCheckSelection->RangeCount()))) {
RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index);
if (MOZ_LIKELY(checkRange)) {
RemoveRange(spellCheckSelection, checkRange);
}
}
return NS_OK;
}
CleanupRangesInSelection(spellCheckSelection);
rv = aStatus->FinishInitOnEvent(*wordUtil);
NS_ENSURE_SUCCESS(rv, rv);
if (!aStatus->mRange) return NS_OK; // empty range, nothing to do
bool doneChecking = true;
if (aStatus->GetOperation() == mozInlineSpellStatus::eOpSelection)
rv = DoSpellCheckSelection(*wordUtil, spellCheckSelection);
else
rv = DoSpellCheck(*wordUtil, spellCheckSelection, aStatus, &doneChecking);
NS_ENSURE_SUCCESS(rv, rv);
if (!doneChecking) rv = ScheduleSpellCheck(std::move(aStatus));
return rv;
}
// mozInlineSpellChecker::IsPointInSelection
//
// Determines if a given (node,offset) point is inside the given
// selection. If so, the specific range of the selection that
// intersects is places in *aRange. (There may be multiple disjoint
// ranges in a selection.)
//
// If there is no intersection, *aRange will be nullptr.
// static
nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection,
nsINode* aNode,
uint32_t aOffset,
nsRange** aRange) {
*aRange = nullptr;
nsTArray<nsRange*> ranges;
nsresult rv = aSelection.GetDynamicRangesForIntervalArray(
aNode, aOffset, aNode, aOffset, true, &ranges);
NS_ENSURE_SUCCESS(rv, rv);
if (ranges.Length() == 0) return NS_OK; // no matches
// there may be more than one range returned, and we don't know what do
// do with that, so just get the first one
NS_ADDREF(*aRange = ranges[0]);
return NS_OK;
}
nsresult mozInlineSpellChecker::CleanupRangesInSelection(
Selection* aSelection) {
// integrity check - remove ranges that have collapsed to nothing. This
// can happen if the node containing a highlighted word was removed.
if (!aSelection) return NS_ERROR_FAILURE;
// TODO: Rewrite this with reversed ranged-loop, it might make this simpler.
int64_t count = aSelection->RangeCount();
for (int64_t index = 0; index < count; index++) {
nsRange* checkRange = aSelection->GetRangeAt(static_cast<uint32_t>(index));
if (MOZ_LIKELY(checkRange)) {
if (checkRange->Collapsed()) {
RemoveRange(aSelection, checkRange);
index--;
count--;
}
}
}
return NS_OK;
}
// mozInlineSpellChecker::RemoveRange
//
// For performance reasons, we have an upper bound on the number of word
// ranges in the spell check selection. When removing a range from the
// selection, we need to decrement mNumWordsInSpellSelection
nsresult mozInlineSpellChecker::RemoveRange(Selection* aSpellCheckSelection,
nsRange* aRange) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Debug, ("%s", __FUNCTION__));
NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
NS_ENSURE_ARG_POINTER(aRange);
ErrorResult rv;
RefPtr<nsRange> range{aRange};
RefPtr<Selection> selection{aSpellCheckSelection};
selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, rv);
if (!rv.Failed()) {
if (mNumWordsInSpellSelection) {
mNumWordsInSpellSelection--;
}
#ifdef ACCESSIBILITY
if (nsAccessibilityService* accService = GetAccService()) {
accService->SpellCheckRangeChanged(*aRange);
}
#endif
}
return rv.StealNSResult();
}
struct mozInlineSpellChecker::CompareRangeAndNodeOffsetRange {
static bool Equals(const RefPtr<nsRange>& aRange,
const NodeOffsetRange& aNodeOffsetRange) {
return aNodeOffsetRange == *aRange;
}
};
void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
const nsTArray<NodeOffsetRange>& aNodeOffsetRangesForWords,
const nsTArray<RefPtr<nsRange>>& aOldRangesForSomeWords,
const nsTArray<bool>& aIsMisspelled, Selection& aSpellCheckerSelection) {
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, ("%s", __FUNCTION__));
MOZ_ASSERT(aNodeOffsetRangesForWords.Length() == aIsMisspelled.Length());
// When the spellchecker checks text containing words separated by "/", it may
// happen that some words checked in one timeslice, are checked again in a
// following timeslice. E.g. for "foo/baz/qwertz", it may happen that "foo"
// and "baz" are checked in one timeslice and two ranges are added for them.
// In the following timeslice "foo" and "baz" are checked again but since
// their corresponding ranges are already in the spellcheck-Selection
// they don't have to be added again and since "foo" and "baz" still contain
// spelling mistakes, they don't have to be removed.
//
// In this case, it's more efficient to keep the existing ranges.
AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
oldRangesMarkedForRemoval;
for (size_t i = 0; i < aOldRangesForSomeWords.Length(); ++i) {
oldRangesMarkedForRemoval.AppendElement(true);
}
AutoTArray<bool, INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK>
nodeOffsetRangesMarkedForAdding;
for (size_t i = 0; i < aNodeOffsetRangesForWords.Length(); ++i) {
nodeOffsetRangesMarkedForAdding.AppendElement(false);
}
for (size_t i = 0; i < aIsMisspelled.Length(); i++) {
if (!aIsMisspelled[i]) {
continue;
}
const NodeOffsetRange& nodeOffsetRange = aNodeOffsetRangesForWords[i];
const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
aSpellCheckerSelection)) {
/** TODO: warn in case the old range doesn't
belong to the selection. This is not critical,
because other code can always remove them
before the actual spellchecking happens. */
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
("%s: reusing old range.", __FUNCTION__));
oldRangesMarkedForRemoval[indexOfOldRangeToKeep] = false;
} else {
nodeOffsetRangesMarkedForAdding[i] = true;
}
}
for (size_t i = 0; i < oldRangesMarkedForRemoval.Length(); ++i) {
if (oldRangesMarkedForRemoval[i]) {
RemoveRange(&aSpellCheckerSelection, aOldRangesForSomeWords[i]);
}
}
// Add ranges after removing the marked old ones, so that the Selection can
// become full again.
for (size_t i = 0; i < nodeOffsetRangesMarkedForAdding.Length(); ++i) {
if (nodeOffsetRangesMarkedForAdding[i]) {
RefPtr<nsRange> wordRange =
mozInlineSpellWordUtil::MakeRange(aNodeOffsetRangesForWords[i]);
// If we somehow can't make a range for this word, just ignore
// it.
if (wordRange) {
AddRange(&aSpellCheckerSelection, wordRange);
}
}
}
}
// mozInlineSpellChecker::AddRange
//
// For performance reasons, we have an upper bound on the number of word
// ranges we'll add to the spell check selection. Once we reach that upper
// bound, stop adding the ranges
nsresult mozInlineSpellChecker::AddRange(Selection* aSpellCheckSelection,
nsRange* aRange) {
NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
NS_ENSURE_ARG_POINTER(aRange);
nsresult rv = NS_OK;
if (!IsSpellCheckSelectionFull()) {
IgnoredErrorResult err;
aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange,
err);
if (err.Failed()) {
rv = err.StealNSResult();
} else {
mNumWordsInSpellSelection++;
#ifdef ACCESSIBILITY
if (nsAccessibilityService* accService = GetAccService()) {
accService->SpellCheckRangeChanged(*aRange);
}
#endif
}
}
return rv;
}
already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() {
if (NS_WARN_IF(!mEditorBase)) {
return nullptr;
}
RefPtr<Selection> selection =
mEditorBase->GetSelection(SelectionType::eSpellCheck);
if (!selection) {
return nullptr;
}
return selection.forget();
}
nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() {
if (NS_WARN_IF(!mEditorBase)) {
return NS_OK; // XXX Why NS_OK?
}
// figure out the old caret position based on the current selection
RefPtr<Selection> selection = mEditorBase->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
mCurrentSelectionAnchorNode = selection->GetFocusNode();
mCurrentSelectionOffset = selection->FocusOffset();
return NS_OK;
}
// mozInlineSpellChecker::HandleNavigationEvent
//
// Acts upon mouse clicks and keyboard navigation changes, spell checking
// the previous word if the new navigation location moves us to another
// word.
//
// This is complicated by the fact that our mouse events are happening after
// selection has been changed to account for the mouse click. But keyboard
// events are happening before the caret selection has changed. Working
// around this by letting keyboard events setting forceWordSpellCheck to
// true. aNewPositionOffset also tries to work around this for the
// DOM_VK_RIGHT and DOM_VK_LEFT cases.
nsresult mozInlineSpellChecker::HandleNavigationEvent(
bool aForceWordSpellCheck, int32_t aNewPositionOffset) {
nsresult rv;
// If we already handled the navigation event and there is no possibility
// anything has changed since then, we don't have to do anything. This
// optimization makes a noticeable difference when you hold down a navigation
// key like Page Down.
if (!mNeedsCheckAfterNavigation) return NS_OK;
nsCOMPtr<nsINode> currentAnchorNode = mCurrentSelectionAnchorNode;
uint32_t currentAnchorOffset = mCurrentSelectionOffset;
// now remember the new focus position resulting from the event
rv = SaveCurrentSelectionPosition();
NS_ENSURE_SUCCESS(rv, rv);
bool shouldPost;
Result<UniquePtr<mozInlineSpellStatus>, nsresult> res =
mozInlineSpellStatus::CreateForNavigation(
*this, aForceWordSpellCheck, aNewPositionOffset, currentAnchorNode,
currentAnchorOffset, mCurrentSelectionAnchorNode,
mCurrentSelectionOffset, &shouldPost);
if (NS_WARN_IF(res.isErr())) {
return res.unwrapErr();
}
if (shouldPost) {
rv = ScheduleSpellCheck(res.unwrap());
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(Event* aEvent) {
WidgetEvent* widgetEvent = aEvent->WidgetEventPtr();
if (MOZ_UNLIKELY(!widgetEvent)) {
return NS_OK;
}
switch (widgetEvent->mMessage) {
case eBlur:
OnBlur(*aEvent);
return NS_OK;
case eMouseClick:
OnMouseClick(*aEvent);
return NS_OK;
case eKeyDown:
OnKeyDown(*aEvent);
return NS_OK;
default:
MOZ_ASSERT_UNREACHABLE("You must forgot to handle new event type");
return NS_OK;
}
}
void mozInlineSpellChecker::OnBlur(Event& aEvent) {
// force spellcheck on blur, for instance when tabbing out of a textbox
HandleNavigationEvent(true);
}
void mozInlineSpellChecker::OnMouseClick(Event& aMouseEvent) {
MouseEvent* mouseEvent = aMouseEvent.AsMouseEvent();
if (MOZ_UNLIKELY(!mouseEvent)) {
return;
}
// ignore any errors from HandleNavigationEvent as we don't want to prevent
// anyone else from seeing this event.
HandleNavigationEvent(mouseEvent->Button() != 0);
}
void mozInlineSpellChecker::OnKeyDown(Event& aKeyEvent) {
WidgetKeyboardEvent* widgetKeyboardEvent =
aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
if (MOZ_UNLIKELY(!widgetKeyboardEvent)) {
return;
}
// we only care about navigation keys that moved selection
switch (widgetKeyboardEvent->mKeyNameIndex) {
case KEY_NAME_INDEX_ArrowRight:
// XXX Does this work with RTL text?
HandleNavigationEvent(false, 1);
return;
case KEY_NAME_INDEX_ArrowLeft:
// XXX Does this work with RTL text?
HandleNavigationEvent(false, -1);
return;
case KEY_NAME_INDEX_ArrowUp:
case KEY_NAME_INDEX_ArrowDown:
case KEY_NAME_INDEX_Home:
case KEY_NAME_INDEX_End:
case KEY_NAME_INDEX_PageDown:
case KEY_NAME_INDEX_PageUp:
HandleNavigationEvent(true /* force a spelling correction */);
return;
default:
return;
}
}
// Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
class UpdateCurrentDictionaryCallback final
: public nsIEditorSpellCheckCallback {
public:
NS_DECL_ISUPPORTS
explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
uint32_t aDisabledAsyncToken)
: mSpellChecker(aSpellChecker),
mDisabledAsyncToken(aDisabledAsyncToken) {}
NS_IMETHOD EditorSpellCheckDone() override {
// Ignore this callback if SetEnableRealTimeSpell(false) was called after
// the UpdateCurrentDictionary call that triggered it.
return mSpellChecker->GetDisabledAsyncToken() > mDisabledAsyncToken
? NS_OK
: mSpellChecker->CurrentDictionaryUpdated();
}
private:
~UpdateCurrentDictionaryCallback() {}
RefPtr<mozInlineSpellChecker> mSpellChecker;
uint32_t mDisabledAsyncToken;
};
NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary() {
// mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
// checker is being initialized. Calling UpdateCurrentDictionary on
// mPendingSpellCheck simply queues the dictionary update after the init.
RefPtr<EditorSpellCheck> spellCheck =
mSpellCheck ? mSpellCheck : mPendingSpellCheck;
if (!spellCheck) {
return NS_OK;
}
RefPtr<UpdateCurrentDictionaryCallback> cb =
new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
NS_ENSURE_STATE(cb);
nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
if (NS_FAILED(rv)) {
cb = nullptr;
return rv;
}
mNumPendingUpdateCurrentDictionary++;
ChangeNumPendingSpellChecks(1);
return NS_OK;
}
// Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
nsresult mozInlineSpellChecker::CurrentDictionaryUpdated() {
mNumPendingUpdateCurrentDictionary--;
MOZ_ASSERT(mNumPendingUpdateCurrentDictionary >= 0,
"CurrentDictionaryUpdated called without corresponding "
"UpdateCurrentDictionary call!");
ChangeNumPendingSpellChecks(-1);
nsresult rv = SpellCheckRange(nullptr);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMETHODIMP
mozInlineSpellChecker::GetSpellCheckPending(bool* aPending) {
*aPending = mNumPendingSpellChecks > 0;
return NS_OK;
}