/* -*- 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.
*/
using mozilla::LogLevel; usingnamespace mozilla; usingnamespace mozilla::dom; usingnamespace 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"
// 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.
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.
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.
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.
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.
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.
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
// 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.
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;
}
// Used as the nsIEditorSpellCheck::InitSpellChecker callback. class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback {
~InitEditorSpellCheckCallback() {}
// 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.
// 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.
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);
// 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();
// 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);
} elseif (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(constchar* 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();
}
// 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));
}
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;
}
// 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.
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;
}
}
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.
if (aEditorBase->IsMailEditor()) {
nsIContent* parent = content->GetParent(); while (parent) { if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::cite, eIgnoreCase)) { returnfalse;
} if (parent->IsAnyOfHTMLElements(nsGkAtoms::pre, nsGkAtoms::div) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
nsGkAtoms::mozsignature,
eIgnoreCase)) { returnfalse;
} if (parent->IsHTMLElement(nsGkAtoms::div) &&
parent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
nsGkAtoms::mozfwcontainer,
eIgnoreCase)) { returnfalse;
}
parent = parent->GetParent();
}
} else { // Check spelling only if the node is editable, and GetSpellcheck() is true // on the nearest HTMLElement ancestor. if (!content->IsEditable()) { returnfalse;
}
// 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()) { returntrue;
}
}
// 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) { returntrue;
}
}
// See if it's spellcheckable returnstatic_cast<nsGenericHTMLElement*>(parent)->Spellcheck();
}
returntrue;
}
// mozInlineSpellChecker::ScheduleSpellCheck // // This is called by code to do the actual spellchecking. We will set up // the proper structures for calls to DoSpellCheck.
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.
// 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?!?!");
}
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 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.
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;
}
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__));
// 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;
}
// 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__));
// 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>();
}
}
// 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) {}
// 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
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.
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
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.