diff options
Diffstat (limited to '')
-rw-r--r-- | extensions/spellcheck/src/components.conf | 20 | ||||
-rw-r--r-- | extensions/spellcheck/src/moz.build | 33 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozEnglishWordUtils.cpp | 106 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozEnglishWordUtils.h | 40 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellChecker.cpp | 1852 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellChecker.h | 284 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellWordUtil.cpp | 1098 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellWordUtil.h | 211 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozPersonalDictionary.cpp | 444 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozPersonalDictionary.h | 80 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellChecker.cpp | 557 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellChecker.h | 180 |
12 files changed, 4905 insertions, 0 deletions
diff --git a/extensions/spellcheck/src/components.conf b/extensions/spellcheck/src/components.conf new file mode 100644 index 0000000000..7fc73c1a17 --- /dev/null +++ b/extensions/spellcheck/src/components.conf @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{56c778e4-1bee-45f3-a689-886692a97fe7}', + 'contract_ids': ['@mozilla.org/spellchecker/engine;1'], + 'type': 'mozHunspell', + }, + { + 'cid': '{7ef52eaf-b7e1-462b-87e2-5d1dbaca9048}', + 'contract_ids': ['@mozilla.org/spellchecker/personaldictionary;1'], + 'type': 'mozPersonalDictionary', + 'headers': ['/extensions/spellcheck/src/mozPersonalDictionary.h'], + 'init_method': 'Init', + }, +] diff --git a/extensions/spellcheck/src/moz.build b/extensions/spellcheck/src/moz.build new file mode 100644 index 0000000000..dcc1d54f74 --- /dev/null +++ b/extensions/spellcheck/src/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +include("/ipc/chromium/chromium-config.mozbuild") +UNIFIED_SOURCES += [ + "mozEnglishWordUtils.cpp", + "mozInlineSpellChecker.cpp", + "mozInlineSpellWordUtil.cpp", + "mozPersonalDictionary.cpp", + "mozSpellChecker.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "../hunspell/glue", + "../hunspell/src", + "/dom/base", +] +EXPORTS.mozilla += [ + "mozInlineSpellChecker.h", + "mozSpellChecker.h", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.cpp b/extensions/spellcheck/src/mozEnglishWordUtils.cpp new file mode 100644 index 0000000000..a1b249b634 --- /dev/null +++ b/extensions/spellcheck/src/mozEnglishWordUtils.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozEnglishWordUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsUnicodeProperties.h" +#include "nsCRT.h" +#include "mozilla/Likely.h" +#include "nsMemory.h" + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(mozEnglishWordUtils, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(mozEnglishWordUtils, Release) + +NS_IMPL_CYCLE_COLLECTION(mozEnglishWordUtils, mURLDetector) + +mozEnglishWordUtils::mozEnglishWordUtils() { + mURLDetector = do_CreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID); +} + +mozEnglishWordUtils::~mozEnglishWordUtils() {} + +// This needs vast improvement + +// static +bool mozEnglishWordUtils::ucIsAlpha(char16_t aChar) { + // XXX we have to fix callers to handle the full Unicode range + return nsUGenCategory::kLetter == mozilla::unicode::GetGenCategory(aChar); +} + +bool mozEnglishWordUtils::FindNextWord(const nsAString& aWord, uint32_t offset, + int32_t* begin, int32_t* end) { + if (offset >= aWord.Length()) { + *begin = -1; + *end = -1; + return false; + } + + const char16_t* word = aWord.BeginReading(); + uint32_t length = aWord.Length(); + const char16_t* p = word + offset; + const char16_t* endbuf = word + length; + const char16_t* startWord = p; + + // XXX These loops should be modified to handle non-BMP characters. + // if previous character is a word character, need to advance out of the + // word + if (offset > 0 && ucIsAlpha(*(p - 1))) { + while (p < endbuf && ucIsAlpha(*p)) { + p++; + } + } + while ((p < endbuf) && (!ucIsAlpha(*p))) { + p++; + } + startWord = p; + while ((p < endbuf) && ((ucIsAlpha(*p)) || (*p == '\''))) { + p++; + } + + // we could be trying to break down a url, we don't want to break a url into + // parts, instead we want to find out if it really is a url and if so, skip + // it, advancing startWord to a point after the url. + + // before we spend more time looking to see if the word is a url, look for a + // url identifer and make sure that identifer isn't the last character in + // the word fragment. + if ((p < endbuf - 1) && (*p == ':' || *p == '@' || *p == '.')) { + // ok, we have a possible url...do more research to find out if we really + // have one and determine the length of the url so we can skip over it. + + if (mURLDetector) { + int32_t startPos = -1; + int32_t endPos = -1; + + mURLDetector->FindURLInPlaintext(startWord, endbuf - startWord, + p - startWord, &startPos, &endPos); + + // ok, if we got a url, adjust the array bounds, skip the current url + // text and find the next word again + if (startPos != -1 && endPos != -1) { + startWord = p + endPos + 1; // skip over the url + + // now recursively call FindNextWord to search for the next word now + // that we have skipped the url + return FindNextWord(aWord, startWord - word, begin, end); + } + } + } + + while ((p > startWord) && (*(p - 1) == '\'')) { // trim trailing apostrophes + p--; + } + + if (startWord == endbuf) { + *begin = -1; + *end = -1; + return false; + } + *begin = startWord - word; + *end = p - word; + return true; +} diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.h b/extensions/spellcheck/src/mozEnglishWordUtils.h new file mode 100644 index 0000000000..64338fe2c6 --- /dev/null +++ b/extensions/spellcheck/src/mozEnglishWordUtils.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozEnglishWordUtils_h__ +#define mozEnglishWordUtils_h__ + +#include "nsCOMPtr.h" +#include "nsString.h" + +#include "mozITXTToHTMLConv.h" +#include "nsCycleCollectionParticipant.h" + +class mozEnglishWordUtils final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(mozEnglishWordUtils) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(mozEnglishWordUtils) + + mozEnglishWordUtils(); + + /** + * Given a unicode string and an offset, find the beginning and end of the + * next word. Return false, begin and end are -1 if there are no words + * remaining in the string. This should really be folded into the + * Line/WordBreaker. + */ + bool FindNextWord(const nsAString& aWord, uint32_t offset, int32_t* begin, + int32_t* end); + + protected: + virtual ~mozEnglishWordUtils(); + + static bool ucIsAlpha(char16_t aChar); + + nsCOMPtr<mozITXTToHTMLConv> + mURLDetector; // used to detect urls so the spell checker can skip them. +}; + +#endif diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp new file mode 100644 index 0000000000..2a02f35847 --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -0,0 +1,1852 @@ +/* -*- 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. + * + * Using the WordUtil class to find words causes DOM notifications to be + * flushed because it asks for style information. As a result, we post an event + * and do all of the spellchecking in that event handler, which occurs later. + * 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/EditAction.h" +#include "mozilla/EditorSpellCheck.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/RangeUtils.h" +#include "mozilla/Services.h" +#include "mozilla/TextEditor.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" +#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 namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +// Set to spew messages to the console about what is happening. +//#define DEBUG_INLINESPELL + +// 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 const char kMaxSpellCheckSelectionSize[] = + "extensions.spellcheck.inline.max-misspellings"; +static const PRTime kMaxSpellCheckTimeInUsec = + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC; + +mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker) + : mSpellChecker(aSpellChecker) {} + +// mozInlineSpellStatus::InitForEditorChange +// +// 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. + +nsresult mozInlineSpellStatus::InitForEditorChange( + EditSubAction aEditSubAction, nsINode* aAnchorNode, uint32_t aAnchorOffset, + nsINode* aPreviousNode, uint32_t aPreviousOffset, nsINode* aStartNode, + uint32_t aStartOffset, nsINode* aEndNode, uint32_t aEndOffset) { + if (NS_WARN_IF(!aAnchorNode) || NS_WARN_IF(!aPreviousNode)) { + return NS_ERROR_FAILURE; + } + + // save the anchor point as a range so we can find the current word later + mAnchorRange = PositionToCollapsedRange(aAnchorNode, aAnchorOffset); + if (NS_WARN_IF(!mAnchorRange)) { + return 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(); + } + + if (deleted) { + // 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. + mOp = eOpChangeDelete; + mRange = nullptr; + return NS_OK; + } + + mOp = eOpChange; + + // range to check + mRange = nsRange::Create(aPreviousNode); + + // ...we need to put the start and end in the correct order + ErrorResult errorResult; + int16_t cmpResult = + mAnchorRange->ComparePoint(*aPreviousNode, aPreviousOffset, errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + nsresult rv; + if (cmpResult < 0) { + // previous anchor node is before the current anchor + rv = mRange->SetStartAndEnd(aPreviousNode, aPreviousOffset, aAnchorNode, + aAnchorOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // previous anchor node is after (or the same as) the current anchor + rv = mRange->SetStartAndEnd(aAnchorNode, aAnchorOffset, aPreviousNode, + aPreviousOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // On insert save this range: DoSpellCheck optimizes things in this range. + // Otherwise, just leave this nullptr. + if (aEditSubAction == EditSubAction::eInsertText) { + mCreatedRange = mRange; + } + + // if we were given a range, we need to expand our range to encompass it + if (aStartNode && aEndNode) { + cmpResult = mRange->ComparePoint(*aStartNode, aStartOffset, errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + if (cmpResult < 0) { // given range starts before + rv = mRange->SetStart(aStartNode, aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + cmpResult = mRange->ComparePoint(*aEndNode, aEndOffset, errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + if (cmpResult > 0) { // given range ends after + rv = mRange->SetEnd(aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +// mozInlineSpellStatis::InitForNavigation +// +// 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. + +nsresult mozInlineSpellStatus::InitForNavigation( + bool aForceCheck, int32_t aNewPositionOffset, nsINode* aOldAnchorNode, + uint32_t aOldAnchorOffset, nsINode* aNewAnchorNode, + uint32_t aNewAnchorOffset, bool* aContinue) { + mOp = eOpNavigation; + + mForceNavigationWordCheck = aForceCheck; + mNewNavigationPositionOffset = aNewPositionOffset; + + // get the root node for checking + TextEditor* textEditor = mSpellChecker->mTextEditor; + if (NS_WARN_IF(!textEditor)) { + return NS_ERROR_FAILURE; + } + Element* root = textEditor->GetRoot(); + if (NS_WARN_IF(!root)) { + return NS_ERROR_FAILURE; + } + // the anchor node might not be in the DOM anymore, check + if (root && aOldAnchorNode && + !aOldAnchorNode->IsShadowIncludingInclusiveDescendantOf(root)) { + *aContinue = false; + return NS_OK; + } + + mOldNavigationAnchorRange = + PositionToCollapsedRange(aOldAnchorNode, aOldAnchorOffset); + if (NS_WARN_IF(!mOldNavigationAnchorRange)) { + return NS_ERROR_FAILURE; + } + mAnchorRange = PositionToCollapsedRange(aNewAnchorNode, aNewAnchorOffset); + if (NS_WARN_IF(!mAnchorRange)) { + return NS_ERROR_FAILURE; + } + + *aContinue = true; + return NS_OK; +} + +// mozInlineSpellStatus::InitForSelection +// +// It is easy for selections since we always re-check the spellcheck +// selection. + +nsresult mozInlineSpellStatus::InitForSelection() { + mOp = eOpSelection; + return NS_OK; +} + +// mozInlineSpellStatus::InitForRange +// +// Called to cause the spellcheck of the given range. This will look like +// a change operation over the given range. + +nsresult mozInlineSpellStatus::InitForRange(nsRange* aRange) { + mOp = eOpChange; + mRange = aRange; + return NS_OK; +} + +// 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) { + 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 InitForEditorChange 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) { + RefPtr<TextEditor> textEditor = mSpellChecker->mTextEditor; + if (!textEditor) { + return NS_ERROR_FAILURE; // editor is gone + } + + NS_ASSERTION(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->mTextEditor) { + 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) { + 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->mTextEditor) { + return nullptr; + } + + return mSpellChecker->mTextEditor->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. + +already_AddRefed<nsRange> mozInlineSpellStatus::PositionToCollapsedRange( + nsINode* aNode, uint32_t aOffset) { + if (NS_WARN_IF(!aNode) || NS_WARN_IF(!GetDocument())) { + 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->mDisabledAsyncToken) { + 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, mTextEditor, mSpellCheck, + mCurrentSelectionAnchorNode) + +mozInlineSpellChecker::SpellCheckingState + mozInlineSpellChecker::gCanEnableSpellChecking = + mozInlineSpellChecker::SpellCheck_Uninitialized; + +mozInlineSpellChecker::mozInlineSpellChecker() + : mNumWordsInSpellSelection(0), + mMaxNumWordsInSpellSelection(250), + mNumPendingSpellChecks(0), + mNumPendingUpdateCurrentDictionary(0), + mDisabledAsyncToken(0), + mNeedsCheckAfterNavigation(false), + mFullSpellCheckScheduled(false), + mIsListeningToEditSubActions(false) { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) + prefs->GetIntPref(kMaxSpellCheckSelectionSize, + &mMaxNumWordsInSpellSelection); +} + +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) { + mTextEditor = aEditor ? aEditor->AsTextEditor() : 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. + +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<TextEditor> textEditor = std::move(mTextEditor); + if (mPendingSpellCheck) { + // Cancel the pending editor spell checker initialization. + mPendingSpellCheck = nullptr; + mPendingInitEditorSpellCheckCallback->Cancel(); + mPendingInitEditorSpellCheckCallback = nullptr; + ChangeNumPendingSpellChecks(-1, textEditor); + } + + // 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, + textEditor); + mNumPendingUpdateCurrentDictionary = 0; + } + if (mNumPendingSpellChecks > 0) { + // If mNumPendingSpellChecks is still > 0 at this point, the remainder is + // pending scheduled spell checks. + ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, textEditor); + } + + 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 (NS_WARN_IF(!mTextEditor)) { + return NS_ERROR_FAILURE; + } + + StartToListenToEditSubActions(); + + RefPtr<Document> doc = mTextEditor->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + doc->AddEventListener(u"blur"_ns, this, true, false); + doc->AddEventListener(u"click"_ns, this, false, false); + doc->AddEventListener(u"keypress"_ns, this, false, false); + return NS_OK; +} + +// mozInlineSpellChecker::UnregisterEventListeners + +nsresult mozInlineSpellChecker::UnregisterEventListeners() { + if (NS_WARN_IF(!mTextEditor)) { + return NS_ERROR_FAILURE; + } + + EndListeningToEditSubActions(); + + RefPtr<Document> doc = mTextEditor->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + doc->RemoveEventListener(u"blur"_ns, this, true); + doc->RemoveEventListener(u"click"_ns, this, false); + doc->RemoveEventListener(u"keypress"_ns, this, false); + 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( + mTextEditor, 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() { + NS_ASSERTION(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, TextEditor* aTextEditor) { + int8_t oldNumPending = mNumPendingSpellChecks; + mNumPendingSpellChecks += aDelta; + NS_ASSERTION(mNumPendingSpellChecks >= 0, + "Unbalanced ChangeNumPendingSpellChecks calls!"); + if (oldNumPending == 0 && mNumPendingSpellChecks > 0) { + NotifyObservers(INLINESPELL_STARTED_TOPIC, aTextEditor); + } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) { + NotifyObservers(INLINESPELL_ENDED_TOPIC, aTextEditor); + } +} + +// Broadcasts the given topic to observers. aEditor is passed to observers if +// nonnull; otherwise mTextEditor is passed. +void mozInlineSpellChecker::NotifyObservers(const char* aTopic, + TextEditor* aTextEditor) { + 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<TextEditor> textEditor = aTextEditor ? aTextEditor : mTextEditor.get(); + os->NotifyObservers(static_cast<nsIEditor*>(textEditor.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 + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForEditorChange( + aEditSubAction, aSelection.GetAnchorNode(), aSelection.AnchorOffset(), + aPreviousSelectedNode, aPreviousSelectedOffset, aStartNode, aStartOffset, + aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = ScheduleSpellCheck(std::move(status)); + 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; + } + + auto status = MakeUnique<mozInlineSpellStatus>(this); + nsresult rv = status->InitForRange(aRange); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(std::move(status)); +} + +// mozInlineSpellChecker::GetMisspelledWord + +NS_IMETHODIMP +mozInlineSpellChecker::GetMisspelledWord(nsINode* aNode, int32_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, int32_t aOffset, + const nsAString& aNewWord) { + if (NS_WARN_IF(!mTextEditor) || 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 (!mTextEditor->AsHTMLEditor()) { + nsContentUtils::PlatformToDOMLineBreaks(newWord); + } + + // Blink dispatches cancelable `beforeinput` event at collecting misspelled + // word so that we should allow to dispatch cancelable event. + RefPtr<TextEditor> textEditor(mTextEditor); + DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction( + newWord, range, TextEditor::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); + + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + 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); + + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForRange(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + 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); + + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + 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); + } + + auto status = MakeUnique<mozInlineSpellStatus>(this); + nsresult rv = status->InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(std::move(status)); +} + +void mozInlineSpellChecker::DidSplitNode(nsINode* aExistingRightNode, + nsINode* aNewLeftNode) { + if (!mIsListeningToEditSubActions) { + return; + } + SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0); +} + +void mozInlineSpellChecker::DidJoinNodes(nsINode& aLeftNode, + nsINode& aRightNode) { + if (!mIsListeningToEditSubActions) { + return; + } + SpellCheckBetweenNodes(&aRightNode, 0, &aRightNode, 0); +} + +// 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) { + nsresult rv; + *aRange = nullptr; + + if (NS_WARN_IF(!mTextEditor)) { + return NS_ERROR_FAILURE; + } + + RefPtr<Document> doc = mTextEditor->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 = mTextEditor->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 + + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForRange(range); + NS_ENSURE_SUCCESS(rv, rv); + 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. + +bool mozInlineSpellChecker::ShouldSpellCheckNode(TextEditor* aTextEditor, + nsINode* aNode) { + MOZ_ASSERT(aNode); + if (!aNode->IsContent()) return false; + + nsIContent* content = aNode->AsContent(); + + if (aTextEditor->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) { + 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; + + int32_t count = aSpellCheckSelection->RangeCount(); + + for (int32_t idx = 0; idx < count; idx++) { + nsRange* range = aSpellCheckSelection->GetRangeAt(idx); + if (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. + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForRange(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + bool doneChecking; + for (int32_t idx = 0; idx < count; idx++) { + // 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; +} + +// mozInlineSpellChecker::DoSpellCheck +// +// 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::DoSpellCheck( + mozInlineSpellWordUtil& aWordUtil, Selection* aSpellCheckSelection, + const UniquePtr<mozInlineSpellStatus>& aStatus, bool* aDoneChecking) { + *aDoneChecking = true; + + if (NS_WARN_IF(!mSpellCheck)) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (SpellCheckSelectionIsFull()) { + return NS_OK; + } + + // get the editor for ShouldSpellCheckNode, this may fail in reasonable + // circumstances since the editor could have gone away + RefPtr<TextEditor> textEditor = mTextEditor; + if (!textEditor || textEditor->Destroyed()) { + return NS_ERROR_FAILURE; + } + + if (aStatus->mRange->Collapsed()) 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). + int32_t originalRangeCount = aSpellCheckSelection->RangeCount(); + + // set the starting DOM position to be the beginning of our range + { + // Scope for the node/offset pairs here so they don't get + // accidentally used later + nsINode* beginNode = aStatus->mRange->GetStartContainer(); + int32_t beginOffset = aStatus->mRange->StartOffset(); + nsINode* endNode = aStatus->mRange->GetEndContainer(); + int32_t endOffset = aStatus->mRange->EndOffset(); + + // Now check that we're still looking at a range that's under + // aWordUtil.GetRootNode() + nsINode* rootNode = aWordUtil.GetRootNode(); + if (!beginNode->IsInComposedDoc() || !endNode->IsInComposedDoc() || + !beginNode->IsShadowIncludingInclusiveDescendantOf(rootNode) || + !endNode->IsShadowIncludingInclusiveDescendantOf(rootNode)) { + // Just bail out and don't try to spell-check this + return NS_OK; + } + + nsresult rv = + aWordUtil.SetPositionAndEnd(beginNode, beginOffset, endNode, endOffset); + if (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 (!mTextEditor) { + return NS_ERROR_FAILURE; + } + + int32_t wordsChecked = 0; + PRTime beginTime = PR_Now(); + + nsTArray<nsString> words; + nsTArray<NodeOffsetRange> checkRanges; + nsAutoString wordText; + NodeOffsetRange wordNodeOffsetRange; + bool dontCheckWord; + // XXX Spellchecker API isn't async on chrome process. + static const size_t requestChunkSize = + XRE_IsContentProcess() ? INLINESPELL_MAXIMUM_CHUNKED_WORDS_PER_TASK : 1; + while ( + aWordUtil.GetNextWord(wordText, &wordNodeOffsetRange, &dontCheckWord)) { + // get the range for the current word. + nsINode* beginNode = wordNodeOffsetRange.Begin().Node(); + nsINode* endNode = wordNodeOffsetRange.End().Node(); + int32_t beginOffset = wordNodeOffsetRange.Begin().Offset(); + int32_t endOffset = wordNodeOffsetRange.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. +#ifdef DEBUG_INLINESPELL + printf("We have run out of the time, schedule next round.\n"); +#endif + CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), + std::move(checkRanges)); + + // move the range to encompass the stuff that needs checking. + nsresult rv = aStatus->mRange->SetStart(beginNode, 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; + } + *aDoneChecking = false; + return NS_OK; + } + +#ifdef DEBUG_INLINESPELL + printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get()); + if (dontCheckWord) printf(" (not checking)"); + printf("\n"); +#endif + + ErrorResult erv; + // 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) { + // likewise, if this word is inside new text, we won't bother testing + if (!aStatus->mCreatedRange || !aStatus->mCreatedRange->IsPointInRange( + *beginNode, beginOffset, erv)) { + nsTArray<RefPtr<nsRange>> ranges; + aSpellCheckSelection->GetRangesForInterval( + *beginNode, beginOffset, *endNode, endOffset, true, ranges, erv); + ENSURE_SUCCESS(erv, erv.StealNSResult()); + for (uint32_t i = 0; i < ranges.Length(); i++) + RemoveRange(aSpellCheckSelection, ranges[i]); + } + } + + // some words are special and don't need checking + if (dontCheckWord) continue; + + // some nodes we don't spellcheck + if (!ShouldSpellCheckNode(textEditor, 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 (aStatus->mNoCheckRange && + aStatus->mNoCheckRange->IsPointInRange(*beginNode, beginOffset, erv)) { + continue; + } + + // check spelling and add to selection if misspelled + mozInlineSpellWordUtil::NormalizeWord(wordText); + words.AppendElement(wordText); + checkRanges.AppendElement(wordNodeOffsetRange); + wordsChecked++; + if (words.Length() >= requestChunkSize) { + CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), + std::move(checkRanges)); + // Set new empty data for spellcheck words and range in DOM to avoid + // clang-tidy detection. + words = nsTArray<nsString>(); + checkRanges = nsTArray<NodeOffsetRange>(); + } + } + + CheckCurrentWordsNoSuggest(aSpellCheckSelection, std::move(words), + std::move(checkRanges)); + + return NS_OK; +} + +// 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::CheckCurrentWordsNoSuggest( + Selection* aSpellCheckSelection, nsTArray<nsString>&& aWords, + nsTArray<NodeOffsetRange>&& aRanges) { + MOZ_ASSERT(aWords.Length() == aRanges.Length()); + + if (aWords.IsEmpty()) { + return; + } + + ChangeNumPendingSpellChecks(1); + + RefPtr<mozInlineSpellChecker> self = this; + RefPtr<Selection> spellCheckerSelection = aSpellCheckSelection; + uint32_t token = mDisabledAsyncToken; + nsTArray<nsString> words = std::move(aWords); + mSpellCheck->CheckCurrentWordsNoSuggest(words)->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, spellCheckerSelection, ranges = std::move(aRanges), + token](const nsTArray<bool>& aIsMisspelled) { + if (token != self->mDisabledAsyncToken) { + // This result is never used + return; + } + + if (!self->mTextEditor || self->mTextEditor->Destroyed()) { + return; + } + + AutoChangeNumPendingSpellChecks pendingChecks(self, -1); + + if (self->SpellCheckSelectionIsFull()) { + return; + } + + for (size_t i = 0; i < aIsMisspelled.Length(); i++) { + if (!aIsMisspelled[i]) { + continue; + } + + RefPtr<nsRange> wordRange = + mozInlineSpellWordUtil::MakeRange(ranges[i]); + // If we somehow can't make a range for this word, just ignore + // it. + if (wordRange) { + self->AddRange(spellCheckerSelection, wordRange); + } + } + }, + [self, token](nsresult aRv) { + if (!self->mTextEditor || self->mTextEditor->Destroyed()) { + return; + } + + if (token != self->mDisabledAsyncToken) { + // This result is never used + return; + } + + self->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) { + // 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. + NS_ASSERTION(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 (!mTextEditor) { + return NS_OK; + } + + mozInlineSpellWordUtil wordUtil; + nsresult rv = wordUtil.Init(mTextEditor); + if (NS_FAILED(rv)) return NS_OK; // editor doesn't like us, don't assert + + RefPtr<Selection> spellCheckSelection = GetSpellCheckSelection(); + if (NS_WARN_IF(!spellCheckSelection)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString currentDictionary; + rv = mSpellCheck->GetCurrentDictionary(currentDictionary); + if (NS_FAILED(rv)) { + // no active dictionary + int32_t count = spellCheckSelection->RangeCount(); + for (int32_t index = count - 1; index >= 0; index--) { + RefPtr<nsRange> checkRange = spellCheckSelection->GetRangeAt(index); + if (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->mOp == 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. + +nsresult mozInlineSpellChecker::IsPointInSelection(Selection& aSelection, + nsINode* aNode, + int32_t aOffset, + nsRange** aRange) { + *aRange = nullptr; + + nsTArray<nsRange*> ranges; + nsresult rv = aSelection.GetRangesForIntervalArray(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; + + int32_t count = aSelection->RangeCount(); + + for (int32_t index = 0; index < count; index++) { + nsRange* checkRange = aSelection->GetRangeAt(index); + if (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) { + 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() && mNumWordsInSpellSelection) mNumWordsInSpellSelection--; + + return rv.StealNSResult(); +} + +// 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 (!SpellCheckSelectionIsFull()) { + IgnoredErrorResult err; + aSpellCheckSelection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, + err); + if (err.Failed()) { + rv = err.StealNSResult(); + } else { + mNumWordsInSpellSelection++; + } + } + + return rv; +} + +already_AddRefed<Selection> mozInlineSpellChecker::GetSpellCheckSelection() { + if (NS_WARN_IF(!mTextEditor)) { + return nullptr; + } + RefPtr<Selection> selection = + mTextEditor->GetSelection(SelectionType::eSpellCheck); + if (!selection) { + return nullptr; + } + return selection.forget(); +} + +nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() { + if (NS_WARN_IF(!mTextEditor)) { + return NS_OK; // XXX Why NS_OK? + } + + // figure out the old caret position based on the current selection + RefPtr<Selection> selection = mTextEditor->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; + auto status = MakeUnique<mozInlineSpellStatus>(this); + rv = status->InitForNavigation(aForceWordSpellCheck, aNewPositionOffset, + currentAnchorNode, currentAnchorOffset, + mCurrentSelectionAnchorNode, + mCurrentSelectionOffset, &shouldPost); + NS_ENSURE_SUCCESS(rv, rv); + if (shouldPost) { + rv = ScheduleSpellCheck(std::move(status)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +mozInlineSpellChecker::HandleEvent(Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType.EqualsLiteral("blur")) { + return OnBlur(aEvent); + } + if (eventType.EqualsLiteral("click")) { + return OnMouseClick(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return OnKeyPress(aEvent); + } + + return NS_OK; +} + +nsresult mozInlineSpellChecker::OnBlur(Event* aEvent) { + // force spellcheck on blur, for instance when tabbing out of a textbox + HandleNavigationEvent(true); + return NS_OK; +} + +nsresult mozInlineSpellChecker::OnMouseClick(Event* aMouseEvent) { + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_OK); + + // ignore any errors from HandleNavigationEvent as we don't want to prevent + // anyone else from seeing this event. + HandleNavigationEvent(mouseEvent->Button() != 0); + return NS_OK; +} + +nsresult mozInlineSpellChecker::OnKeyPress(Event* aKeyEvent) { + RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent(); + NS_ENSURE_TRUE(keyEvent, NS_OK); + + uint32_t keyCode = keyEvent->KeyCode(); + + // we only care about navigation keys that moved selection + switch (keyCode) { + case KeyboardEvent_Binding::DOM_VK_RIGHT: + case KeyboardEvent_Binding::DOM_VK_LEFT: + HandleNavigationEvent( + false, keyCode == KeyboardEvent_Binding::DOM_VK_RIGHT ? 1 : -1); + break; + case KeyboardEvent_Binding::DOM_VK_UP: + case KeyboardEvent_Binding::DOM_VK_DOWN: + case KeyboardEvent_Binding::DOM_VK_HOME: + case KeyboardEvent_Binding::DOM_VK_END: + case KeyboardEvent_Binding::DOM_VK_PAGE_UP: + case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN: + HandleNavigationEvent(true /* force a spelling correction */); + break; + } + + return NS_OK; +} + +// 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->mDisabledAsyncToken > 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--; + NS_ASSERTION(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; +} diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.h b/extensions/spellcheck/src/mozInlineSpellChecker.h new file mode 100644 index 0000000000..9877affcaa --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellChecker.h @@ -0,0 +1,284 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_mozInlineSpellChecker_h +#define mozilla_mozInlineSpellChecker_h + +#include "nsCycleCollectionParticipant.h" +#include "nsIDOMEventListener.h" +#include "nsIEditorSpellCheck.h" +#include "nsIInlineSpellChecker.h" +#include "mozInlineSpellWordUtil.h" +#include "nsRange.h" +#include "nsWeakReference.h" + +class InitEditorSpellCheckCallback; +class mozInlineSpellChecker; +class mozInlineSpellResume; +class UpdateCurrentDictionaryCallback; + +namespace mozilla { +class EditorSpellCheck; +class TextEditor; +enum class EditSubAction : int32_t; + +namespace dom { +class Event; +} // namespace dom +} // namespace mozilla + +class mozInlineSpellStatus { + public: + explicit mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker); + + nsresult InitForEditorChange(mozilla::EditSubAction aEditSubAction, + nsINode* aAnchorNode, uint32_t aAnchorOffset, + nsINode* aPreviousNode, uint32_t aPreviousOffset, + nsINode* aStartNode, uint32_t aStartOffset, + nsINode* aEndNode, uint32_t aEndOffset); + nsresult InitForNavigation(bool aForceCheck, int32_t aNewPositionOffset, + nsINode* aOldAnchorNode, uint32_t aOldAnchorOffset, + nsINode* aNewAnchorNode, uint32_t aNewAnchorOffset, + bool* aContinue); + nsresult InitForSelection(); + nsresult InitForRange(nsRange* aRange); + + nsresult FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil); + + // Return true if we plan to spell-check everything + bool IsFullSpellCheck() const { return mOp == eOpChange && !mRange; } + + RefPtr<mozInlineSpellChecker> mSpellChecker; + + // what happened? + enum Operation { + eOpChange, // for SpellCheckAfterEditorChange except + // deleteSelection + eOpChangeDelete, // for SpellCheckAfterEditorChange with + // deleteSelection + eOpNavigation, // for HandleNavigationEvent + eOpSelection, // re-check all misspelled words + eOpResume + }; // for resuming a previously started check + Operation mOp; + + // Used for events where we have already computed the range to use. It can + // also be nullptr in these cases where we need to check the entire range. + RefPtr<nsRange> mRange; + + // If we happen to know something was inserted, this is that range. + // Can be nullptr (this only allows an optimization, so not setting doesn't + // hurt) + RefPtr<nsRange> mCreatedRange; + + // Contains the range computed for the current word. Can be nullptr. + RefPtr<nsRange> mNoCheckRange; + + // Indicates the position of the cursor for the event (so we can compute + // mNoCheckRange). It can be nullptr if we don't care about the cursor + // position (such as for the intial check of everything). + // + // For mOp == eOpNavigation, this is the NEW position of the cursor + RefPtr<nsRange> mAnchorRange; + + // ----- + // The following members are only for navigation events and are only + // stored for FinishNavigationEvent to initialize the other members. + // ----- + + // this is the OLD position of the cursor + RefPtr<nsRange> mOldNavigationAnchorRange; + + // Set when we should force checking the current word. See + // mozInlineSpellChecker::HandleNavigationEvent for a description of why we + // have this. + bool mForceNavigationWordCheck; + + // Contains the offset passed in to HandleNavigationEvent + int32_t mNewNavigationPositionOffset; + + protected: + nsresult FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil); + + nsresult FillNoCheckRangeFromAnchor(mozInlineSpellWordUtil& aWordUtil); + + mozilla::dom::Document* GetDocument() const; + already_AddRefed<nsRange> PositionToCollapsedRange(nsINode* aNode, + uint32_t aOffset); +}; + +class mozInlineSpellChecker final : public nsIInlineSpellChecker, + public nsIDOMEventListener, + public nsSupportsWeakReference { + private: + friend class mozInlineSpellStatus; + friend class InitEditorSpellCheckCallback; + friend class UpdateCurrentDictionaryCallback; + friend class AutoChangeNumPendingSpellChecks; + friend class mozInlineSpellResume; + + // Access with CanEnableInlineSpellChecking + enum SpellCheckingState { + SpellCheck_Uninitialized = -1, + SpellCheck_NotAvailable = 0, + SpellCheck_Available = 1 + }; + static SpellCheckingState gCanEnableSpellChecking; + + RefPtr<mozilla::TextEditor> mTextEditor; + RefPtr<mozilla::EditorSpellCheck> mSpellCheck; + RefPtr<mozilla::EditorSpellCheck> mPendingSpellCheck; + + int32_t mNumWordsInSpellSelection; + int32_t mMaxNumWordsInSpellSelection; + + // we need to keep track of the current text position in the document + // so we can spell check the old word when the user clicks around the + // document. + nsCOMPtr<nsINode> mCurrentSelectionAnchorNode; + uint32_t mCurrentSelectionOffset; + + // Tracks the number of pending spell checks *and* async operations that may + // lead to spell checks, like updating the current dictionary. This is + // necessary so that observers can know when to wait for spell check to + // complete. + int32_t mNumPendingSpellChecks; + + // The number of calls to UpdateCurrentDictionary that haven't finished yet. + int32_t mNumPendingUpdateCurrentDictionary; + + // This number is incremented each time the spell checker is disabled so that + // pending scheduled spell checks and UpdateCurrentDictionary calls can be + // ignored when they finish. + uint32_t mDisabledAsyncToken; + + // When mPendingSpellCheck is non-null, this is the callback passed when + // it was initialized. + RefPtr<InitEditorSpellCheckCallback> mPendingInitEditorSpellCheckCallback; + + // Set when we have spellchecked after the last edit operation. See the + // commment at the top of the .cpp file for more info. + bool mNeedsCheckAfterNavigation; + + // Set when we have a pending mozInlineSpellResume which will check + // the whole document. + bool mFullSpellCheckScheduled; + + // Set to true when this instance needs to listen to edit actions of + // the editor. + bool mIsListeningToEditSubActions; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIINLINESPELLCHECKER + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozInlineSpellChecker, + nsIDOMEventListener) + + mozilla::EditorSpellCheck* GetEditorSpellCheck(); + + // returns true if there are any spell checking dictionaries available + static bool CanEnableInlineSpellChecking(); + // update the cached value whenever the list of available dictionaries changes + static void UpdateCanEnableInlineSpellChecking(); + + nsresult OnBlur(mozilla::dom::Event* aEvent); + nsresult OnMouseClick(mozilla::dom::Event* aMouseEvent); + nsresult OnKeyPress(mozilla::dom::Event* aKeyEvent); + + mozInlineSpellChecker(); + + // spell checks all of the words between two nodes + nsresult SpellCheckBetweenNodes(nsINode* aStartNode, int32_t aStartOffset, + nsINode* aEndNode, int32_t aEndOffset); + + // examines the dom node in question and returns true if the inline spell + // checker should skip the node (i.e. the text is inside of a block quote + // or an e-mail signature...) + bool ShouldSpellCheckNode(mozilla::TextEditor* aTextEditor, nsINode* aNode); + + // spell check the text contained within aRange, potentially scheduling + // another check in the future if the time threshold is reached + nsresult ScheduleSpellCheck( + mozilla::UniquePtr<mozInlineSpellStatus>&& aStatus); + + nsresult DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil, + mozilla::dom::Selection* aSpellCheckSelection); + nsresult DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, + mozilla::dom::Selection* aSpellCheckSelection, + const mozilla::UniquePtr<mozInlineSpellStatus>& aStatus, + bool* aDoneChecking); + + // helper routine to determine if a point is inside of the passed in + // selection. + nsresult IsPointInSelection(mozilla::dom::Selection& aSelection, + nsINode* aNode, int32_t aOffset, + nsRange** aRange); + + nsresult CleanupRangesInSelection(mozilla::dom::Selection* aSelection); + + /** + * @param aRange needs to be kept alive by the caller. + */ + // TODO: annotate with `MOZ_CAN_RUN_SCRIPT` instead + // (https://bugzilla.mozilla.org/show_bug.cgi?id=1620540). + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + RemoveRange(mozilla::dom::Selection* aSpellCheckSelection, nsRange* aRange); + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + AddRange(mozilla::dom::Selection* aSpellCheckSelection, nsRange* aRange); + bool SpellCheckSelectionIsFull() { + return mNumWordsInSpellSelection >= mMaxNumWordsInSpellSelection; + } + + nsresult MakeSpellCheckRange(nsINode* aStartNode, int32_t aStartOffset, + nsINode* aEndNode, int32_t aEndOffset, + nsRange** aRange); + + // DOM and editor event registration helper routines + nsresult RegisterEventListeners(); + nsresult UnregisterEventListeners(); + nsresult HandleNavigationEvent(bool aForceWordSpellCheck, + int32_t aNewPositionOffset = 0); + + already_AddRefed<mozilla::dom::Selection> GetSpellCheckSelection(); + nsresult SaveCurrentSelectionPosition(); + + nsresult ResumeCheck(mozilla::UniquePtr<mozInlineSpellStatus>&& aStatus); + + // Those methods are called when mTextEditor splits a node or joins the + // given nodes. + void DidSplitNode(nsINode* aExistingRightNode, nsINode* aNewLeftNode); + void DidJoinNodes(nsINode& aRightNode, nsINode& aLeftNode); + + nsresult SpellCheckAfterEditorChange(mozilla::EditSubAction aEditSubAction, + mozilla::dom::Selection& aSelection, + nsINode* aPreviousSelectedNode, + uint32_t aPreviousSelectedOffset, + nsINode* aStartNode, + uint32_t aStartOffset, nsINode* aEndNode, + uint32_t aEndOffset); + + protected: + virtual ~mozInlineSpellChecker(); + + // called when async nsIEditorSpellCheck methods complete + nsresult EditorSpellCheckInited(); + nsresult CurrentDictionaryUpdated(); + + // track the number of pending spell checks and async operations that may lead + // to spell checks, notifying observers accordingly + void ChangeNumPendingSpellChecks(int32_t aDelta, + mozilla::TextEditor* aTextEditor = nullptr); + void NotifyObservers(const char* aTopic, mozilla::TextEditor* aTextEditor); + + void StartToListenToEditSubActions() { mIsListeningToEditSubActions = true; } + void EndListeningToEditSubActions() { mIsListeningToEditSubActions = false; } + + void CheckCurrentWordsNoSuggest(mozilla::dom::Selection* aSpellCheckSelection, + nsTArray<nsString>&& aWords, + nsTArray<NodeOffsetRange>&& aRanges); +}; + +#endif // #ifndef mozilla_mozInlineSpellChecker_h diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp new file mode 100644 index 0000000000..65cabcf0c7 --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp @@ -0,0 +1,1098 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozInlineSpellWordUtil.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/Element.h" + +#include "nsDebug.h" +#include "nsAtom.h" +#include "nsComponentManagerUtils.h" +#include "nsUnicodeProperties.h" +#include "nsServiceManagerUtils.h" +#include "nsIContent.h" +#include "nsTextFragment.h" +#include "nsRange.h" +#include "nsContentUtils.h" +#include "nsIFrame.h" +#include <algorithm> + +using namespace mozilla; + +// IsIgnorableCharacter +// +// These characters are ones that we should ignore in input. + +inline bool IsIgnorableCharacter(char ch) { + return (ch == static_cast<char>(0xAD)); // SOFT HYPHEN +} + +inline bool IsIgnorableCharacter(char16_t ch) { + return (ch == 0xAD || // SOFT HYPHEN + ch == 0x1806); // MONGOLIAN TODO SOFT HYPHEN +} + +// IsConditionalPunctuation +// +// Some characters (like apostrophes) require characters on each side to be +// part of a word, and are otherwise punctuation. + +inline bool IsConditionalPunctuation(char ch) { + return (ch == '\'' || // RIGHT SINGLE QUOTATION MARK + ch == static_cast<char>(0xB7)); // MIDDLE DOT +} + +inline bool IsConditionalPunctuation(char16_t ch) { + return (ch == '\'' || ch == 0x2019 || // RIGHT SINGLE QUOTATION MARK + ch == 0x00B7); // MIDDLE DOT +} + +static bool IsAmbiguousDOMWordSeprator(char16_t ch) { + // This class may be CHAR_CLASS_SEPARATOR, but it depends on context. + return (ch == '@' || ch == ':' || ch == '.' || ch == '/' || ch == '-' || + IsConditionalPunctuation(ch)); +} + +static bool IsAmbiguousDOMWordSeprator(char ch) { + // This class may be CHAR_CLASS_SEPARATOR, but it depends on context. + return IsAmbiguousDOMWordSeprator(static_cast<char16_t>(ch)); +} + +// IsDOMWordSeparator +// +// Determines if the given character should be considered as a DOM Word +// separator. Basically, this is whitespace, although it could also have +// certain punctuation that we know ALWAYS breaks words. This is important. +// For example, we can't have any punctuation that could appear in a URL +// or email address in this, because those need to always fit into a single +// DOM word. + +static bool IsDOMWordSeparator(char ch) { + // simple spaces or no-break space + return (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || + ch == static_cast<char>(0xA0)); +} + +static bool IsDOMWordSeparator(char16_t ch) { + // simple spaces + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') return true; + + // complex spaces - check only if char isn't ASCII (uncommon) + if (ch >= 0xA0 && (ch == 0x00A0 || // NO-BREAK SPACE + ch == 0x2002 || // EN SPACE + ch == 0x2003 || // EM SPACE + ch == 0x2009 || // THIN SPACE + ch == 0x3000)) // IDEOGRAPHIC SPACE + return true; + + // otherwise not a space + return false; +} + +// mozInlineSpellWordUtil::Init + +nsresult mozInlineSpellWordUtil::Init(TextEditor* aTextEditor) { + if (NS_WARN_IF(!aTextEditor)) { + return NS_ERROR_FAILURE; + } + + mDocument = aTextEditor->GetDocument(); + if (NS_WARN_IF(!mDocument)) { + return NS_ERROR_FAILURE; + } + + mIsContentEditableOrDesignMode = !!aTextEditor->AsHTMLEditor(); + + // Find the root node for the editor. For contenteditable the mRootNode could + // change to shadow root if the begin and end are inside the shadowDOM. + mRootNode = aTextEditor->GetRoot(); + if (NS_WARN_IF(!mRootNode)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +static inline bool IsSpellCheckingTextNode(nsINode* aNode) { + nsIContent* parent = aNode->GetParent(); + if (parent && + parent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style)) + return false; + return aNode->IsText(); +} + +typedef void (*OnLeaveNodeFunPtr)(nsINode* aNode, void* aClosure); + +// Find the next node in the DOM tree in preorder. +// Calls OnLeaveNodeFunPtr when the traversal leaves a node, which is +// why we can't just use GetNextNode here, sadly. +static nsINode* FindNextNode(nsINode* aNode, nsINode* aRoot, + OnLeaveNodeFunPtr aOnLeaveNode, void* aClosure) { + MOZ_ASSERT(aNode, "Null starting node?"); + + nsINode* next = aNode->GetFirstChild(); + if (next) return next; + + // Don't look at siblings or otherwise outside of aRoot + if (aNode == aRoot) return nullptr; + + next = aNode->GetNextSibling(); + if (next) return next; + + // Go up + for (;;) { + if (aOnLeaveNode) { + aOnLeaveNode(aNode, aClosure); + } + + next = aNode->GetParent(); + if (next == aRoot || !next) return nullptr; + aNode = next; + + next = aNode->GetNextSibling(); + if (next) return next; + } +} + +// aNode is not a text node. Find the first text node starting at aNode/aOffset +// in a preorder DOM traversal. +static nsINode* FindNextTextNode(nsINode* aNode, int32_t aOffset, + nsINode* aRoot) { + MOZ_ASSERT(aNode, "Null starting node?"); + NS_ASSERTION(!IsSpellCheckingTextNode(aNode), + "FindNextTextNode should start with a non-text node"); + + nsINode* checkNode; + // Need to start at the aOffset'th child + nsIContent* child = aNode->GetChildAt_Deprecated(aOffset); + + if (child) { + checkNode = child; + } else { + // aOffset was beyond the end of the child list. + // goto next node after the last descendant of aNode in + // a preorder DOM traversal. + checkNode = aNode->GetNextNonChildNode(aRoot); + } + + while (checkNode && !IsSpellCheckingTextNode(checkNode)) { + checkNode = checkNode->GetNextNode(aRoot); + } + return checkNode; +} + +// mozInlineSpellWordUtil::SetPositionAndEnd +// +// We have two ranges "hard" and "soft". The hard boundary is simply +// the scope of the root node. The soft boundary is that which is set +// by the caller of this class by calling this function. If this function is +// not called, the soft boundary is the same as the hard boundary. +// +// When we reach the soft boundary (mSoftEnd), we keep +// going until we reach the end of a word. This allows the caller to set the +// end of the range to anything, and we will always check whole multiples of +// words. When we reach the hard boundary we stop no matter what. +// +// There is no beginning soft boundary. This is because we only go to the +// previous node once, when finding the previous word boundary in +// SetPosition(). You might think of the soft boundary as being this initial +// position. + +nsresult mozInlineSpellWordUtil::SetPositionAndEnd(nsINode* aPositionNode, + int32_t aPositionOffset, + nsINode* aEndNode, + int32_t aEndOffset) { + MOZ_ASSERT(aPositionNode, "Null begin node?"); + MOZ_ASSERT(aEndNode, "Null end node?"); + + NS_ASSERTION(mRootNode, "Not initialized"); + + // Find a appropriate root if we are dealing with contenteditable nodes which + // are in the shadow DOM. + if (mIsContentEditableOrDesignMode) { + nsINode* rootNode = aPositionNode->SubtreeRoot(); + if (rootNode != aEndNode->SubtreeRoot()) { + return NS_ERROR_FAILURE; + } + + if (mozilla::dom::ShadowRoot::FromNode(rootNode)) { + mRootNode = rootNode; + } + } + + InvalidateWords(); + + if (!IsSpellCheckingTextNode(aPositionNode)) { + // Start at the start of the first text node after aNode/aOffset. + aPositionNode = FindNextTextNode(aPositionNode, aPositionOffset, mRootNode); + aPositionOffset = 0; + } + mSoftBegin = NodeOffset(aPositionNode, aPositionOffset); + + if (!IsSpellCheckingTextNode(aEndNode)) { + // End at the start of the first text node after aEndNode/aEndOffset. + aEndNode = FindNextTextNode(aEndNode, aEndOffset, mRootNode); + aEndOffset = 0; + } + mSoftEnd = NodeOffset(aEndNode, aEndOffset); + + nsresult rv = EnsureWords(); + if (NS_FAILED(rv)) { + return rv; + } + + int32_t textOffset = MapDOMPositionToSoftTextOffset(mSoftBegin); + if (textOffset < 0) { + return NS_OK; + } + + mNextWordIndex = FindRealWordContaining(textOffset, HINT_END, true); + return NS_OK; +} + +nsresult mozInlineSpellWordUtil::EnsureWords() { + if (mSoftTextValid) return NS_OK; + BuildSoftText(); + nsresult rv = BuildRealWords(); + if (NS_FAILED(rv)) { + mRealWords.Clear(); + return rv; + } + mSoftTextValid = true; + return NS_OK; +} + +nsresult mozInlineSpellWordUtil::MakeRangeForWord(const RealWord& aWord, + nsRange** aRange) { + NodeOffset begin = + MapSoftTextOffsetToDOMPosition(aWord.mSoftTextOffset, HINT_BEGIN); + NodeOffset end = MapSoftTextOffsetToDOMPosition(aWord.EndOffset(), HINT_END); + return MakeRange(begin, end, aRange); +} +void mozInlineSpellWordUtil::MakeNodeOffsetRangeForWord( + const RealWord& aWord, NodeOffsetRange* aNodeOffsetRange) { + NodeOffset begin = + MapSoftTextOffsetToDOMPosition(aWord.mSoftTextOffset, HINT_BEGIN); + NodeOffset end = MapSoftTextOffsetToDOMPosition(aWord.EndOffset(), HINT_END); + *aNodeOffsetRange = NodeOffsetRange(begin, end); +} + +// mozInlineSpellWordUtil::GetRangeForWord + +nsresult mozInlineSpellWordUtil::GetRangeForWord(nsINode* aWordNode, + int32_t aWordOffset, + nsRange** aRange) { + // Set our soft end and start + NodeOffset pt(aWordNode, aWordOffset); + + if (!mSoftTextValid || pt != mSoftBegin || pt != mSoftEnd) { + InvalidateWords(); + mSoftBegin = mSoftEnd = pt; + nsresult rv = EnsureWords(); + if (NS_FAILED(rv)) { + return rv; + } + } + + int32_t offset = MapDOMPositionToSoftTextOffset(pt); + if (offset < 0) return MakeRange(pt, pt, aRange); + int32_t wordIndex = FindRealWordContaining(offset, HINT_BEGIN, false); + if (wordIndex < 0) return MakeRange(pt, pt, aRange); + return MakeRangeForWord(mRealWords[wordIndex], aRange); +} + +// This is to fix characters that the spellchecker may not like +static void NormalizeWord(const nsAString& aInput, int32_t aPos, int32_t aLen, + nsAString& aOutput) { + aOutput.Truncate(); + for (int32_t i = 0; i < aLen; i++) { + char16_t ch = aInput.CharAt(i + aPos); + + // remove ignorable characters from the word + if (IsIgnorableCharacter(ch)) continue; + + // the spellchecker doesn't handle curly apostrophes in all languages + if (ch == 0x2019) { // RIGHT SINGLE QUOTATION MARK + ch = '\''; + } + + aOutput.Append(ch); + } +} + +// mozInlineSpellWordUtil::GetNextWord +// +// FIXME-optimization: we shouldn't have to generate a range every single +// time. It would be better if the inline spellchecker didn't require a +// range unless the word was misspelled. This may or may not be possible. + +bool mozInlineSpellWordUtil::GetNextWord(nsAString& aText, + NodeOffsetRange* aNodeOffsetRange, + bool* aSkipChecking) { +#ifdef DEBUG_SPELLCHECK + printf("GetNextWord called; mNextWordIndex=%d\n", mNextWordIndex); +#endif + + if (mNextWordIndex < 0 || mNextWordIndex >= int32_t(mRealWords.Length())) { + mNextWordIndex = -1; + *aSkipChecking = true; + return false; + } + + const RealWord& word = mRealWords[mNextWordIndex]; + MakeNodeOffsetRangeForWord(word, aNodeOffsetRange); + ++mNextWordIndex; + *aSkipChecking = !word.mCheckableWord; + ::NormalizeWord(mSoftText, word.mSoftTextOffset, word.mLength, aText); + +#ifdef DEBUG_SPELLCHECK + printf("GetNextWord returning: %s (skip=%d)\n", + NS_ConvertUTF16toUTF8(aText).get(), *aSkipChecking); +#endif + + return true; +} + +// mozInlineSpellWordUtil::MakeRange +// +// Convenience function for creating a range over the current document. + +nsresult mozInlineSpellWordUtil::MakeRange(NodeOffset aBegin, NodeOffset aEnd, + nsRange** aRange) { + NS_ENSURE_ARG_POINTER(aBegin.mNode); + if (!mDocument) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + RefPtr<nsRange> range = nsRange::Create(aBegin.mNode, aBegin.mOffset, + aEnd.mNode, aEnd.mOffset, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + MOZ_ASSERT(range); + range.forget(aRange); + return NS_OK; +} + +// static +already_AddRefed<nsRange> mozInlineSpellWordUtil::MakeRange( + const NodeOffsetRange& aRange) { + IgnoredErrorResult ignoredError; + RefPtr<nsRange> range = + nsRange::Create(aRange.Begin().Node(), aRange.Begin().Offset(), + aRange.End().Node(), aRange.End().Offset(), ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), "Creating a range failed"); + return range.forget(); +} + +/*********** Word Splitting ************/ + +// classifies a given character in the DOM word +enum CharClass { + CHAR_CLASS_WORD, + CHAR_CLASS_SEPARATOR, + CHAR_CLASS_END_OF_INPUT +}; + +// Encapsulates DOM-word to real-word splitting +template <class T> +struct MOZ_STACK_CLASS WordSplitState { + const T& mDOMWordText; + int32_t mDOMWordOffset; + CharClass mCurCharClass; + + explicit WordSplitState(const T& aString) + : mDOMWordText(aString), + mDOMWordOffset(0), + mCurCharClass(CHAR_CLASS_END_OF_INPUT) {} + + CharClass ClassifyCharacter(int32_t aIndex, bool aRecurse) const; + void Advance(); + void AdvanceThroughSeparators(); + void AdvanceThroughWord(); + + // Finds special words like email addresses and URLs that may start at the + // current position, and returns their length, or 0 if not found. This allows + // arbitrary word breaking rules to be used for these special entities, as + // long as they can not contain whitespace. + bool IsSpecialWord() const; + + // Similar to IsSpecialWord except that this takes a split word as + // input. This checks for things that do not require special word-breaking + // rules. + bool ShouldSkipWord(int32_t aStart, int32_t aLength) const; + + // Checks to see if there's a DOM word separator before aBeforeOffset within + // it. This function does not modify aSeparatorOffset when it returns false. + bool GetDOMWordSeparatorOffset(int32_t aOffset, + int32_t* aSeparatorOffset) const; + + char16_t GetUnicharAt(int32_t aIndex) const; +}; + +// WordSplitState::ClassifyCharacter +template <class T> +CharClass WordSplitState<T>::ClassifyCharacter(int32_t aIndex, + bool aRecurse) const { + NS_ASSERTION(aIndex >= 0 && aIndex <= int32_t(mDOMWordText.Length()), + "Index out of range"); + if (aIndex == int32_t(mDOMWordText.Length())) return CHAR_CLASS_SEPARATOR; + + // this will classify the character, we want to treat "ignorable" characters + // such as soft hyphens, and also ZWJ and ZWNJ as word characters. + nsUGenCategory charCategory = + mozilla::unicode::GetGenCategory(GetUnicharAt(aIndex)); + if (charCategory == nsUGenCategory::kLetter || + IsIgnorableCharacter(mDOMWordText[aIndex]) || + mDOMWordText[aIndex] == 0x200C /* ZWNJ */ || + mDOMWordText[aIndex] == 0x200D /* ZWJ */) + return CHAR_CLASS_WORD; + + // If conditional punctuation is surrounded immediately on both sides by word + // characters it also counts as a word character. + if (IsConditionalPunctuation(mDOMWordText[aIndex])) { + if (!aRecurse) { + // not allowed to look around, this punctuation counts like a separator + return CHAR_CLASS_SEPARATOR; + } + + // check the left-hand character + if (aIndex == 0) return CHAR_CLASS_SEPARATOR; + if (ClassifyCharacter(aIndex - 1, false) != CHAR_CLASS_WORD) + return CHAR_CLASS_SEPARATOR; + // If the previous charatcer is a word-char, make sure that it's not a + // special dot character. + if (mDOMWordText[aIndex - 1] == '.') return CHAR_CLASS_SEPARATOR; + + // now we know left char is a word-char, check the right-hand character + if (aIndex == int32_t(mDOMWordText.Length() - 1)) { + return CHAR_CLASS_SEPARATOR; + } + + if (ClassifyCharacter(aIndex + 1, false) != CHAR_CLASS_WORD) + return CHAR_CLASS_SEPARATOR; + // If the next charatcer is a word-char, make sure that it's not a + // special dot character. + if (mDOMWordText[aIndex + 1] == '.') return CHAR_CLASS_SEPARATOR; + + // char on either side is a word, this counts as a word + return CHAR_CLASS_WORD; + } + + // The dot character, if appearing at the end of a word, should + // be considered part of that word. Example: "etc.", or + // abbreviations + if (aIndex > 0 && mDOMWordText[aIndex] == '.' && + mDOMWordText[aIndex - 1] != '.' && + ClassifyCharacter(aIndex - 1, false) != CHAR_CLASS_WORD) { + return CHAR_CLASS_WORD; + } + + // all other punctuation + if (charCategory == nsUGenCategory::kSeparator || + charCategory == nsUGenCategory::kOther || + charCategory == nsUGenCategory::kPunctuation || + charCategory == nsUGenCategory::kSymbol) { + // Don't break on hyphens, as hunspell handles them on its own. + if (aIndex > 0 && mDOMWordText[aIndex] == '-' && + mDOMWordText[aIndex - 1] != '-' && + ClassifyCharacter(aIndex - 1, false) == CHAR_CLASS_WORD) { + // A hyphen is only meaningful as a separator inside a word + // if the previous and next characters are a word character. + if (aIndex == int32_t(mDOMWordText.Length()) - 1) + return CHAR_CLASS_SEPARATOR; + if (mDOMWordText[aIndex + 1] != '.' && + ClassifyCharacter(aIndex + 1, false) == CHAR_CLASS_WORD) + return CHAR_CLASS_WORD; + } + return CHAR_CLASS_SEPARATOR; + } + + // any other character counts as a word + return CHAR_CLASS_WORD; +} + +// WordSplitState::Advance +template <class T> +void WordSplitState<T>::Advance() { + NS_ASSERTION(mDOMWordOffset >= 0, "Negative word index"); + NS_ASSERTION(mDOMWordOffset < (int32_t)mDOMWordText.Length(), + "Length beyond end"); + + mDOMWordOffset++; + if (mDOMWordOffset >= (int32_t)mDOMWordText.Length()) + mCurCharClass = CHAR_CLASS_END_OF_INPUT; + else + mCurCharClass = ClassifyCharacter(mDOMWordOffset, true); +} + +// WordSplitState::AdvanceThroughSeparators +template <class T> +void WordSplitState<T>::AdvanceThroughSeparators() { + while (mCurCharClass == CHAR_CLASS_SEPARATOR) Advance(); +} + +// WordSplitState::AdvanceThroughWord +template <class T> +void WordSplitState<T>::AdvanceThroughWord() { + while (mCurCharClass == CHAR_CLASS_WORD) Advance(); +} + +// WordSplitState::IsSpecialWord +template <class T> +bool WordSplitState<T>::IsSpecialWord() const { + // Search for email addresses. We simply define these as any sequence of + // characters with an '@' character in the middle. The DOM word is already + // split on whitepace, so we know that everything to the end is the address + int32_t firstColon = -1; + for (int32_t i = mDOMWordOffset; i < int32_t(mDOMWordText.Length()); i++) { + if (mDOMWordText[i] == '@') { + // only accept this if there are unambiguous word characters (don't bother + // recursing to disambiguate apostrophes) on each side. This prevents + // classifying, e.g. "@home" as an email address + + // Use this condition to only accept words with '@' in the middle of + // them. It works, but the inlinespellcker doesn't like this. The problem + // is that you type "fhsgfh@" that's a misspelled word followed by a + // symbol, but when you type another letter "fhsgfh@g" that first word + // need to be unmarked misspelled. It doesn't do this. it only checks the + // current position for potentially removing a spelling range. + if (i > 0 && ClassifyCharacter(i - 1, false) == CHAR_CLASS_WORD && + i < (int32_t)mDOMWordText.Length() - 1 && + ClassifyCharacter(i + 1, false) == CHAR_CLASS_WORD) { + return true; + } + } else if (mDOMWordText[i] == ':' && firstColon < 0) { + firstColon = i; + + // If the first colon is followed by a slash, consider it a URL + // This will catch things like asdf://foo.com + if (firstColon < (int32_t)mDOMWordText.Length() - 1 && + mDOMWordText[firstColon + 1] == '/') { + return true; + } + } + } + + // Check the text before the first colon against some known protocols. It + // is impossible to check against all protocols, especially since you can + // plug in new protocols. We also don't want to waste time here checking + // against a lot of obscure protocols. + if (firstColon > mDOMWordOffset) { + nsString protocol( + Substring(mDOMWordText, mDOMWordOffset, firstColon - mDOMWordOffset)); + if (protocol.EqualsIgnoreCase("http") || + protocol.EqualsIgnoreCase("https") || + protocol.EqualsIgnoreCase("news") || + protocol.EqualsIgnoreCase("file") || + protocol.EqualsIgnoreCase("javascript") || + protocol.EqualsIgnoreCase("data") || protocol.EqualsIgnoreCase("ftp")) { + return true; + } + } + + // not anything special + return false; +} + +// WordSplitState::ShouldSkipWord +template <class T> +bool WordSplitState<T>::ShouldSkipWord(int32_t aStart, int32_t aLength) const { + int32_t last = aStart + aLength; + + // check to see if the word contains a digit + for (int32_t i = aStart; i < last; i++) { + if (mozilla::unicode::GetGenCategory(GetUnicharAt(i)) == + nsUGenCategory::kNumber) { + return true; + } + } + + // not special + return false; +} + +template <class T> +bool WordSplitState<T>::GetDOMWordSeparatorOffset( + int32_t aOffset, int32_t* aSeparatorOffset) const { + for (int32_t i = aOffset - 1; i >= 0; --i) { + if (IsDOMWordSeparator(mDOMWordText[i]) || + (!IsAmbiguousDOMWordSeprator(mDOMWordText[i]) && + ClassifyCharacter(i, true) == CHAR_CLASS_SEPARATOR)) { + // Be greedy, find as many separators as we can + for (int32_t j = i - 1; j >= 0; --j) { + if (IsDOMWordSeparator(mDOMWordText[j]) || + (!IsAmbiguousDOMWordSeprator(mDOMWordText[j]) && + ClassifyCharacter(j, true) == CHAR_CLASS_SEPARATOR)) { + i = j; + } else { + break; + } + } + *aSeparatorOffset = i; + return true; + } + } + return false; +} + +template <> +char16_t WordSplitState<nsDependentSubstring>::GetUnicharAt( + int32_t aIndex) const { + return mDOMWordText[aIndex]; +} + +template <> +char16_t WordSplitState<nsDependentCSubstring>::GetUnicharAt( + int32_t aIndex) const { + return static_cast<char16_t>(static_cast<uint8_t>(mDOMWordText[aIndex])); +} + +static inline bool IsBRElement(nsINode* aNode) { + return aNode->IsHTMLElement(nsGkAtoms::br); +} + +/** + * Given a TextNode, checks to see if there's a DOM word separator before + * aBeforeOffset within it. This function does not modify aSeparatorOffset when + * it returns false. + * + * @param aContent the TextNode to check. + * @param aBeforeOffset the offset in the TextNode before which we will search + * for the DOM separator. You can pass INT32_MAX to search the entire + * length of the string. + * @param aSeparatorOffset will be set to the offset of the first separator it + * encounters. Will not be written to if no separator is found. + * @returns True if it found a separator. + */ +static bool TextNodeContainsDOMWordSeparator(nsIContent* aContent, + int32_t aBeforeOffset, + int32_t* aSeparatorOffset) { + const nsTextFragment* textFragment = aContent->GetText(); + NS_ASSERTION(textFragment, "Where is our text?"); + int32_t end = std::min(aBeforeOffset, int32_t(textFragment->GetLength())); + + if (textFragment->Is2b()) { + nsDependentSubstring targetText(textFragment->Get2b(), end); + WordSplitState<nsDependentSubstring> state(targetText); + return state.GetDOMWordSeparatorOffset(end, aSeparatorOffset); + } + + nsDependentCSubstring targetText(textFragment->Get1b(), end); + WordSplitState<nsDependentCSubstring> state(targetText); + return state.GetDOMWordSeparatorOffset(end, aSeparatorOffset); +} + +/** + * Check if there's a DOM word separator before aBeforeOffset in this node. + * Always returns true if it's a BR element. + * aSeparatorOffset is set to the index of the first character in the last + * separator if any is found (0 for BR elements). + * + * This function does not modify aSeparatorOffset when it returns false. + */ +static bool ContainsDOMWordSeparator(nsINode* aNode, int32_t aBeforeOffset, + int32_t* aSeparatorOffset) { + if (IsBRElement(aNode)) { + *aSeparatorOffset = 0; + return true; + } + + if (!IsSpellCheckingTextNode(aNode)) return false; + + return TextNodeContainsDOMWordSeparator(aNode->AsContent(), aBeforeOffset, + aSeparatorOffset); +} + +static bool IsBreakElement(nsINode* aNode) { + if (!aNode->IsElement()) { + return false; + } + + dom::Element* element = aNode->AsElement(); + if (element->IsHTMLElement(nsGkAtoms::br)) { + return true; + } + + // If we don't have a frame, we don't consider ourselves a break + // element. In particular, words can span us. + nsIFrame* frame = element->GetPrimaryFrame(); + if (!frame) { + return false; + } + + auto* disp = frame->StyleDisplay(); + // Anything that's not an inline element is a break element. + // XXXbz should replaced inlines be break elements, though? + // Also should inline-block and such be break elements? + // + // FIXME(emilio): We should teach the spell checker to deal with generated + // content (it doesn't at all), then remove the IsListItem() check, as there + // could be no marker, etc... + return !disp->IsInlineFlow() || disp->IsListItem(); +} + +struct CheckLeavingBreakElementClosure { + bool mLeftBreakElement; +}; + +static void CheckLeavingBreakElement(nsINode* aNode, void* aClosure) { + CheckLeavingBreakElementClosure* cl = + static_cast<CheckLeavingBreakElementClosure*>(aClosure); + if (!cl->mLeftBreakElement && IsBreakElement(aNode)) { + cl->mLeftBreakElement = true; + } +} + +void mozInlineSpellWordUtil::NormalizeWord(nsAString& aWord) { + nsAutoString result; + ::NormalizeWord(aWord, 0, aWord.Length(), result); + aWord = result; +} + +void mozInlineSpellWordUtil::BuildSoftText() { + // First we have to work backwards from mSoftStart to find a text node + // containing a DOM word separator, a non-inline-element + // boundary, or the hard start node. That's where we'll start building the + // soft string from. + nsINode* node = mSoftBegin.mNode; + int32_t firstOffsetInNode = 0; + int32_t checkBeforeOffset = mSoftBegin.mOffset; + while (node) { + if (ContainsDOMWordSeparator(node, checkBeforeOffset, &firstOffsetInNode)) { + if (node == mSoftBegin.mNode) { + // If we find a word separator on the first node, look at the preceding + // word on the text node as well. + int32_t newOffset = 0; + if (firstOffsetInNode > 0) { + // Try to find the previous word boundary in the current node. If + // we can't find one, start checking previous sibling nodes (if any + // adjacent ones exist) to see if we can find any text nodes with + // DOM word separators. We bail out as soon as we see a node that is + // not a text node, or we run out of previous sibling nodes. In the + // event that we simply cannot find any preceding word separator, the + // offset is set to 0, and the soft text beginning node is set to the + // "most previous" text node before the original starting node, or + // kept at the original starting node if no previous text nodes exist. + if (!ContainsDOMWordSeparator(node, firstOffsetInNode - 1, + &newOffset)) { + nsIContent* prevNode = node->GetPreviousSibling(); + while (prevNode && IsSpellCheckingTextNode(prevNode)) { + mSoftBegin.mNode = prevNode; + if (TextNodeContainsDOMWordSeparator(prevNode, INT32_MAX, + &newOffset)) { + break; + } + prevNode = prevNode->GetPreviousSibling(); + } + } + } + firstOffsetInNode = newOffset; + mSoftBegin.mOffset = newOffset; + } + break; + } + checkBeforeOffset = INT32_MAX; + if (IsBreakElement(node)) { + // Since GetPreviousContent follows tree *preorder*, we're about to + // traverse up out of 'node'. Since node induces breaks (e.g., it's a + // block), don't bother trying to look outside it, just stop now. + break; + } + // GetPreviousContent below expects mRootNode to be an ancestor of node. + if (!node->IsInclusiveDescendantOf(mRootNode)) { + break; + } + node = node->GetPreviousContent(mRootNode); + } + + // Now build up the string moving forward through the DOM until we reach + // the soft end and *then* see a DOM word separator, a non-inline-element + // boundary, or the hard end node. + mSoftText.Truncate(); + mSoftTextDOMMapping.Clear(); + bool seenSoftEnd = false; + // Leave this outside the loop so large heap string allocations can be reused + // across iterations + while (node) { + if (node == mSoftEnd.mNode) { + seenSoftEnd = true; + } + + bool exit = false; + if (IsSpellCheckingTextNode(node)) { + nsIContent* content = static_cast<nsIContent*>(node); + NS_ASSERTION(content, "Where is our content?"); + const nsTextFragment* textFragment = content->GetText(); + NS_ASSERTION(textFragment, "Where is our text?"); + int32_t lastOffsetInNode = textFragment->GetLength(); + + if (seenSoftEnd) { + // check whether we can stop after this + for (int32_t i = node == mSoftEnd.mNode ? mSoftEnd.mOffset : 0; + i < int32_t(textFragment->GetLength()); ++i) { + if (IsDOMWordSeparator(textFragment->CharAt(i))) { + exit = true; + // stop at the first separator after the soft end point + lastOffsetInNode = i; + break; + } + } + } + + if (firstOffsetInNode < lastOffsetInNode) { + int32_t len = lastOffsetInNode - firstOffsetInNode; + mSoftTextDOMMapping.AppendElement(DOMTextMapping( + NodeOffset(node, firstOffsetInNode), mSoftText.Length(), len)); + + bool ok = textFragment->AppendTo(mSoftText, firstOffsetInNode, len, + mozilla::fallible); + if (!ok) { + // probably out of memory, remove from mSoftTextDOMMapping + mSoftTextDOMMapping.RemoveLastElement(); + exit = true; + } + } + + firstOffsetInNode = 0; + } + + if (exit) break; + + CheckLeavingBreakElementClosure closure = {false}; + node = FindNextNode(node, mRootNode, CheckLeavingBreakElement, &closure); + if (closure.mLeftBreakElement || (node && IsBreakElement(node))) { + // We left, or are entering, a break element (e.g., block). Maybe we can + // stop now. + if (seenSoftEnd) break; + // Record the break + mSoftText.Append(' '); + } + } + +#ifdef DEBUG_SPELLCHECK + printf("Got DOM string: %s\n", NS_ConvertUTF16toUTF8(mSoftText).get()); +#endif +} + +nsresult mozInlineSpellWordUtil::BuildRealWords() { + // This is pretty simple. We just have to walk mSoftText, tokenizing it + // into "real words". + // We do an outer traversal of words delimited by IsDOMWordSeparator, calling + // SplitDOMWord on each of those DOM words + int32_t wordStart = -1; + mRealWords.Clear(); + for (int32_t i = 0; i < int32_t(mSoftText.Length()); ++i) { + if (IsDOMWordSeparator(mSoftText.CharAt(i))) { + if (wordStart >= 0) { + nsresult rv = SplitDOMWord(wordStart, i); + if (NS_FAILED(rv)) { + return rv; + } + wordStart = -1; + } + } else { + if (wordStart < 0) { + wordStart = i; + } + } + } + if (wordStart >= 0) { + nsresult rv = SplitDOMWord(wordStart, mSoftText.Length()); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +/*********** DOM/realwords<->mSoftText mapping functions ************/ + +int32_t mozInlineSpellWordUtil::MapDOMPositionToSoftTextOffset( + NodeOffset aNodeOffset) { + if (!mSoftTextValid) { + NS_ERROR("Soft text must be valid if we're to map into it"); + return -1; + } + + for (int32_t i = 0; i < int32_t(mSoftTextDOMMapping.Length()); ++i) { + const DOMTextMapping& map = mSoftTextDOMMapping[i]; + if (map.mNodeOffset.mNode == aNodeOffset.mNode) { + // Allow offsets at either end of the string, in particular, allow the + // offset that's at the end of the contributed string + int32_t offsetInContributedString = + aNodeOffset.mOffset - map.mNodeOffset.mOffset; + if (offsetInContributedString >= 0 && + offsetInContributedString <= map.mLength) + return map.mSoftTextOffset + offsetInContributedString; + return -1; + } + } + return -1; +} + +namespace { + +template <class T> +class FirstLargerOffset { + int32_t mSoftTextOffset; + + public: + explicit FirstLargerOffset(int32_t aSoftTextOffset) + : mSoftTextOffset(aSoftTextOffset) {} + int operator()(const T& t) const { + // We want the first larger offset, so never return 0 (which would + // short-circuit evaluation before finding the last such offset). + return mSoftTextOffset < t.mSoftTextOffset ? -1 : 1; + } +}; + +template <class T> +bool FindLastNongreaterOffset(const nsTArray<T>& aContainer, + int32_t aSoftTextOffset, size_t* aIndex) { + if (aContainer.Length() == 0) { + return false; + } + + BinarySearchIf(aContainer, 0, aContainer.Length(), + FirstLargerOffset<T>(aSoftTextOffset), aIndex); + if (*aIndex > 0) { + // There was at least one mapping with offset <= aSoftTextOffset. Step back + // to find the last element with |mSoftTextOffset <= aSoftTextOffset|. + *aIndex -= 1; + } else { + // Every mapping had offset greater than aSoftTextOffset. + MOZ_ASSERT(aContainer[*aIndex].mSoftTextOffset > aSoftTextOffset); + } + return true; +} + +} // namespace + +NodeOffset mozInlineSpellWordUtil::MapSoftTextOffsetToDOMPosition( + int32_t aSoftTextOffset, DOMMapHint aHint) { + NS_ASSERTION(mSoftTextValid, + "Soft text must be valid if we're to map out of it"); + if (!mSoftTextValid) return NodeOffset(nullptr, -1); + + // Find the last mapping, if any, such that mSoftTextOffset <= aSoftTextOffset + size_t index; + bool found = + FindLastNongreaterOffset(mSoftTextDOMMapping, aSoftTextOffset, &index); + if (!found) { + return NodeOffset(nullptr, -1); + } + + // 'index' is now the last mapping, if any, such that + // mSoftTextOffset <= aSoftTextOffset. + // If we're doing HINT_END, then we may want to return the end of the + // the previous mapping instead of the start of this mapping + if (aHint == HINT_END && index > 0) { + const DOMTextMapping& map = mSoftTextDOMMapping[index - 1]; + if (map.mSoftTextOffset + map.mLength == aSoftTextOffset) + return NodeOffset(map.mNodeOffset.mNode, + map.mNodeOffset.mOffset + map.mLength); + } + + // We allow ourselves to return the end of this mapping even if we're + // doing HINT_START. This will only happen if there is no mapping which this + // point is the start of. I'm not 100% sure this is OK... + const DOMTextMapping& map = mSoftTextDOMMapping[index]; + int32_t offset = aSoftTextOffset - map.mSoftTextOffset; + if (offset >= 0 && offset <= map.mLength) + return NodeOffset(map.mNodeOffset.mNode, map.mNodeOffset.mOffset + offset); + + return NodeOffset(nullptr, -1); +} + +int32_t mozInlineSpellWordUtil::FindRealWordContaining(int32_t aSoftTextOffset, + DOMMapHint aHint, + bool aSearchForward) { + NS_ASSERTION(mSoftTextValid, + "Soft text must be valid if we're to map out of it"); + if (!mSoftTextValid) return -1; + + // Find the last word, if any, such that mSoftTextOffset <= aSoftTextOffset + size_t index; + bool found = FindLastNongreaterOffset(mRealWords, aSoftTextOffset, &index); + if (!found) { + return -1; + } + + // 'index' is now the last word, if any, such that + // mSoftTextOffset <= aSoftTextOffset. + // If we're doing HINT_END, then we may want to return the end of the + // the previous word instead of the start of this word + if (aHint == HINT_END && index > 0) { + const RealWord& word = mRealWords[index - 1]; + if (word.mSoftTextOffset + word.mLength == aSoftTextOffset) + return index - 1; + } + + // We allow ourselves to return the end of this word even if we're + // doing HINT_START. This will only happen if there is no word which this + // point is the start of. I'm not 100% sure this is OK... + const RealWord& word = mRealWords[index]; + int32_t offset = aSoftTextOffset - word.mSoftTextOffset; + if (offset >= 0 && offset <= static_cast<int32_t>(word.mLength)) return index; + + if (aSearchForward) { + if (mRealWords[0].mSoftTextOffset > aSoftTextOffset) { + // All words have mSoftTextOffset > aSoftTextOffset + return 0; + } + // 'index' is the last word such that mSoftTextOffset <= aSoftTextOffset. + // Word index+1, if it exists, will be the first with + // mSoftTextOffset > aSoftTextOffset. + if (index + 1 < mRealWords.Length()) return index + 1; + } + + return -1; +} + +// mozInlineSpellWordUtil::SplitDOMWord + +nsresult mozInlineSpellWordUtil::SplitDOMWord(int32_t aStart, int32_t aEnd) { + nsDependentSubstring targetText(mSoftText, aStart, aEnd - aStart); + WordSplitState<nsDependentSubstring> state(targetText); + state.mCurCharClass = state.ClassifyCharacter(0, true); + + state.AdvanceThroughSeparators(); + if (state.mCurCharClass != CHAR_CLASS_END_OF_INPUT && state.IsSpecialWord()) { + int32_t specialWordLength = + state.mDOMWordText.Length() - state.mDOMWordOffset; + if (!mRealWords.AppendElement( + RealWord(aStart + state.mDOMWordOffset, specialWordLength, false), + fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; + } + + while (state.mCurCharClass != CHAR_CLASS_END_OF_INPUT) { + state.AdvanceThroughSeparators(); + if (state.mCurCharClass == CHAR_CLASS_END_OF_INPUT) break; + + // save the beginning of the word + int32_t wordOffset = state.mDOMWordOffset; + + // find the end of the word + state.AdvanceThroughWord(); + int32_t wordLen = state.mDOMWordOffset - wordOffset; + if (!mRealWords.AppendElement( + RealWord(aStart + wordOffset, wordLen, + !state.ShouldSkipWord(wordOffset, wordLen)), + fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return NS_OK; +} diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.h b/extensions/spellcheck/src/mozInlineSpellWordUtil.h new file mode 100644 index 0000000000..78ab9f0b7d --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.h @@ -0,0 +1,211 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozInlineSpellWordUtil_h +#define mozInlineSpellWordUtil_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Document.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +//#define DEBUG_SPELLCHECK + +class nsRange; +class nsINode; + +namespace mozilla { +class TextEditor; + +namespace dom { +class Document; +} +} // namespace mozilla + +struct NodeOffset { + nsCOMPtr<nsINode> mNode; + int32_t mOffset; + + NodeOffset() : mOffset(0) {} + NodeOffset(nsINode* aNode, int32_t aOffset) + : mNode(aNode), mOffset(aOffset) {} + + bool operator==(const NodeOffset& aOther) const { + return mNode == aOther.mNode && mOffset == aOther.mOffset; + } + + bool operator!=(const NodeOffset& aOther) const { return !(*this == aOther); } + + nsINode* Node() const { return mNode.get(); } + int32_t Offset() const { return mOffset; } +}; + +class NodeOffsetRange { + private: + NodeOffset mBegin; + NodeOffset mEnd; + + public: + NodeOffsetRange() {} + NodeOffsetRange(NodeOffset b, NodeOffset e) : mBegin(b), mEnd(e) {} + + NodeOffset Begin() const { return mBegin; } + + NodeOffset End() const { return mEnd; } +}; + +/** + * This class extracts text from the DOM and builds it into a single string. + * The string includes whitespace breaks whereever non-inline elements begin + * and end. This string is broken into "real words", following somewhat + * complex rules; for example substrings that look like URLs or + * email addresses are treated as single words, but otherwise many kinds of + * punctuation are treated as word separators. GetNextWord provides a way + * to iterate over these "real words". + * + * The basic operation is: + * + * 1. Call Init with the weak pointer to the editor that you're using. + * 2. Call SetPositionAndEnd to to initialize the current position inside the + * previously given range and set where you want to stop spellchecking. + * We'll stop at the word boundary after that. If SetEnd is not called, + * we'll stop at the end of the root element. + * 3. Call GetNextWord over and over until it returns false. + */ + +class MOZ_STACK_CLASS mozInlineSpellWordUtil { + public: + mozInlineSpellWordUtil() + : mIsContentEditableOrDesignMode(false), + mRootNode(nullptr), + mSoftBegin(nullptr, 0), + mSoftEnd(nullptr, 0), + mNextWordIndex(-1), + mSoftTextValid(false) {} + + nsresult Init(mozilla::TextEditor* aTextEditor); + + // sets the current position, this should be inside the range. If we are in + // the middle of a word, we'll move to its start. + nsresult SetPositionAndEnd(nsINode* aPositionNode, int32_t aPositionOffset, + nsINode* aEndNode, int32_t aEndOffset); + + // Given a point inside or immediately following a word, this returns the + // DOM range that exactly encloses that word's characters. The current + // position will be at the end of the word. This will find the previous + // word if the current position is space, so if you care that the point is + // inside the word, you should check the range. + // + // THIS CHANGES THE CURRENT POSITION AND RANGE. It is designed to be called + // before you actually generate the range you are interested in and iterate + // the words in it. + nsresult GetRangeForWord(nsINode* aWordNode, int32_t aWordOffset, + nsRange** aRange); + + // Convenience functions, object must be initialized + nsresult MakeRange(NodeOffset aBegin, NodeOffset aEnd, nsRange** aRange); + static already_AddRefed<nsRange> MakeRange(const NodeOffsetRange& aRange); + + // Moves to the the next word in the range, and retrieves it's text and range. + // false is returned when we are done checking. + // aSkipChecking will be set if the word is "special" and shouldn't be + // checked (e.g., an email address). + bool GetNextWord(nsAString& aText, NodeOffsetRange* aNodeOffsetRange, + bool* aSkipChecking); + + // Call to normalize some punctuation. This function takes an autostring + // so we can access characters directly. + static void NormalizeWord(nsAString& aWord); + + mozilla::dom::Document* GetDocument() const { return mDocument; } + nsINode* GetRootNode() { return mRootNode; } + + private: + // cached stuff for the editor, set by Init + RefPtr<mozilla::dom::Document> mDocument; + bool mIsContentEditableOrDesignMode; + + // range to check, see SetPosition and SetEnd + nsINode* mRootNode; + NodeOffset mSoftBegin; + NodeOffset mSoftEnd; + + // DOM text covering the soft range, with newlines added at block boundaries + nsString mSoftText; + // A list of where we extracted text from, ordered by mSoftTextOffset. A given + // DOM node appears at most once in this list. + struct DOMTextMapping { + NodeOffset mNodeOffset; + int32_t mSoftTextOffset; + int32_t mLength; + + DOMTextMapping(NodeOffset aNodeOffset, int32_t aSoftTextOffset, + int32_t aLength) + : mNodeOffset(aNodeOffset), + mSoftTextOffset(aSoftTextOffset), + mLength(aLength) {} + }; + nsTArray<DOMTextMapping> mSoftTextDOMMapping; + + // A list of the "real words" in mSoftText, ordered by mSoftTextOffset + struct RealWord { + int32_t mSoftTextOffset; + uint32_t mLength : 31; + uint32_t mCheckableWord : 1; + + RealWord(int32_t aOffset, uint32_t aLength, bool aCheckable) + : mSoftTextOffset(aOffset), + mLength(aLength), + mCheckableWord(aCheckable) { + static_assert(sizeof(RealWord) == 8, + "RealWord should be limited to 8 bytes"); + MOZ_ASSERT(aLength < INT32_MAX, + "Word length is too large to fit in the bitfield"); + } + + int32_t EndOffset() const { return mSoftTextOffset + mLength; } + }; + nsTArray<RealWord> mRealWords; + int32_t mNextWordIndex; + + bool mSoftTextValid; + + void InvalidateWords() { mSoftTextValid = false; } + nsresult EnsureWords(); + + int32_t MapDOMPositionToSoftTextOffset(NodeOffset aNodeOffset); + // Map an offset into mSoftText to a DOM position. Note that two DOM positions + // can map to the same mSoftText offset, e.g. given nodes A=aaaa and B=bbbb + // forming aaaabbbb, (A,4) and (B,0) give the same string offset. So, + // aHintBefore controls which position we return ... if aHint is eEnd + // then the position indicates the END of a range so we return (A,4). + // Otherwise the position indicates the START of a range so we return (B,0). + enum DOMMapHint { HINT_BEGIN, HINT_END }; + NodeOffset MapSoftTextOffsetToDOMPosition(int32_t aSoftTextOffset, + DOMMapHint aHint); + // Finds the index of the real word containing aSoftTextOffset, or -1 if none + // If it's exactly between two words, then if aHint is HINT_BEGIN, return the + // later word (favouring the assumption that it's the BEGINning of a word), + // otherwise return the earlier word (assuming it's the END of a word). + // If aSearchForward is true, then if we don't find a word at the given + // position, search forward until we do find a word and return that (if + // found). + int32_t FindRealWordContaining(int32_t aSoftTextOffset, DOMMapHint aHint, + bool aSearchForward); + + // build mSoftText and mSoftTextDOMMapping + void BuildSoftText(); + // Build mRealWords array + nsresult BuildRealWords(); + + nsresult SplitDOMWord(int32_t aStart, int32_t aEnd); + + nsresult MakeRangeForWord(const RealWord& aWord, nsRange** aRange); + void MakeNodeOffsetRangeForWord(const RealWord& aWord, + NodeOffsetRange* aNodeOffsetRange); +}; + +#endif diff --git a/extensions/spellcheck/src/mozPersonalDictionary.cpp b/extensions/spellcheck/src/mozPersonalDictionary.cpp new file mode 100644 index 0000000000..77605a2e2c --- /dev/null +++ b/extensions/spellcheck/src/mozPersonalDictionary.cpp @@ -0,0 +1,444 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozPersonalDictionary.h" + +#include <utility> + +#include "nsAppDirectoryServiceDefs.h" +#include "nsCRT.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIRunnable.h" +#include "nsISafeOutputStream.h" +#include "nsIUnicharInputStream.h" +#include "nsIWeakReference.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsReadableUtils.h" +#include "nsStringEnumerator.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsUnicharInputStream.h" +#include "prio.h" + +#define MOZ_PERSONAL_DICT_NAME u"persdict.dat" + +/** + * This is the most braindead implementation of a personal dictionary possible. + * There is not much complexity needed, though. It could be made much faster, + * and probably should, but I don't see much need for more in terms of + * interface. + * + * Allowing personal words to be associated with only certain dictionaries + * maybe. + * + * TODO: + * Implement the suggestion record. + */ + +NS_IMPL_ADDREF(mozPersonalDictionary) +NS_IMPL_RELEASE(mozPersonalDictionary) + +NS_INTERFACE_MAP_BEGIN(mozPersonalDictionary) + NS_INTERFACE_MAP_ENTRY(mozIPersonalDictionary) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIPersonalDictionary) +NS_INTERFACE_MAP_END + +class mozPersonalDictionaryLoader final : public mozilla::Runnable { + public: + explicit mozPersonalDictionaryLoader(mozPersonalDictionary* dict) + : mozilla::Runnable("mozPersonalDictionaryLoader"), mDict(dict) {} + + NS_IMETHOD Run() override { + mDict->SyncLoad(); + + // Release the dictionary on the main thread + NS_ReleaseOnMainThread("mozPersonalDictionaryLoader::mDict", + mDict.forget().downcast<mozIPersonalDictionary>()); + + return NS_OK; + } + + private: + RefPtr<mozPersonalDictionary> mDict; +}; + +class mozPersonalDictionarySave final : public mozilla::Runnable { + public: + explicit mozPersonalDictionarySave(mozPersonalDictionary* aDict, + nsCOMPtr<nsIFile> aFile, + nsTArray<nsString>&& aDictWords) + : mozilla::Runnable("mozPersonalDictionarySave"), + mDictWords(std::move(aDictWords)), + mFile(aFile), + mDict(aDict) {} + + NS_IMETHOD Run() override { + nsresult res; + + MOZ_ASSERT(!NS_IsMainThread()); + + { + mozilla::MonitorAutoLock mon(mDict->mMonitorSave); + + nsCOMPtr<nsIOutputStream> outStream; + NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStream), mFile, + PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE, + 0664); + + // Get a buffered output stream 4096 bytes big, to optimize writes. + nsCOMPtr<nsIOutputStream> bufferedOutputStream; + res = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream), + outStream.forget(), 4096); + if (NS_FAILED(res)) { + return res; + } + + uint32_t bytesWritten; + nsAutoCString utf8Key; + for (uint32_t i = 0; i < mDictWords.Length(); ++i) { + CopyUTF16toUTF8(mDictWords[i], utf8Key); + + bufferedOutputStream->Write(utf8Key.get(), utf8Key.Length(), + &bytesWritten); + bufferedOutputStream->Write("\n", 1, &bytesWritten); + } + nsCOMPtr<nsISafeOutputStream> safeStream = + do_QueryInterface(bufferedOutputStream); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (safeStream) { + res = safeStream->Finish(); + if (NS_FAILED(res)) { + NS_WARNING( + "failed to save personal dictionary file! possible data loss"); + } + } + + // Save is done, reset the state variable and notify those who are + // waiting. + mDict->mSavePending = false; + mon.Notify(); + + // Leaving the block where 'mon' was declared will call the destructor + // and unlock. + } + + // Release the dictionary on the main thread. + NS_ReleaseOnMainThread("mozPersonalDictionarySave::mDict", + mDict.forget().downcast<mozIPersonalDictionary>()); + + return NS_OK; + } + + private: + nsTArray<nsString> mDictWords; + nsCOMPtr<nsIFile> mFile; + RefPtr<mozPersonalDictionary> mDict; +}; + +mozPersonalDictionary::mozPersonalDictionary() + : mIsLoaded(false), + mSavePending(false), + mMonitor("mozPersonalDictionary::mMonitor"), + mMonitorSave("mozPersonalDictionary::mMonitorSave") {} + +mozPersonalDictionary::~mozPersonalDictionary() {} + +nsresult mozPersonalDictionary::Init() { + nsCOMPtr<nsIObserverService> svc = + do_GetService("@mozilla.org/observer-service;1"); + + NS_ENSURE_STATE(svc); + // we want to reload the dictionary if the profile switches + nsresult rv = svc->AddObserver(this, "profile-do-change", true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = svc->AddObserver(this, "profile-before-change", true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Load(); + + return NS_OK; +} + +void mozPersonalDictionary::WaitForLoad() { + // If the dictionary is already loaded, we return straight away. + if (mIsLoaded) { + return; + } + + // If the dictionary hasn't been loaded, we try to lock the same monitor + // that the thread uses that does the load. This way the main thread will + // be suspended until the monitor becomes available. + mozilla::MonitorAutoLock mon(mMonitor); + + // The monitor has become available. This can have two reasons: + // 1: The thread that does the load has finished. + // 2: The thread that does the load hasn't even started. + // In this case we need to wait. + if (!mIsLoaded) { + mon.Wait(); + } +} + +nsresult mozPersonalDictionary::LoadInternal() { + nsresult rv; + mozilla::MonitorAutoLock mon(mMonitor); + + if (mIsLoaded) { + return NS_OK; + } + + rv = + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mFile) { + return NS_ERROR_FAILURE; + } + + rv = mFile->Append(nsLiteralString(MOZ_PERSONAL_DICT_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIRunnable> runnable = new mozPersonalDictionaryLoader(this); + rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::Load() { + nsresult rv = LoadInternal(); + + if (NS_FAILED(rv)) { + mIsLoaded = true; + } + + return rv; +} + +void mozPersonalDictionary::SyncLoad() { + MOZ_ASSERT(!NS_IsMainThread()); + + mozilla::MonitorAutoLock mon(mMonitor); + + if (mIsLoaded) { + return; + } + + SyncLoadInternal(); + mIsLoaded = true; + mon.Notify(); +} + +void mozPersonalDictionary::SyncLoadInternal() { + MOZ_ASSERT(!NS_IsMainThread()); + + // FIXME Deinst -- get dictionary name from prefs; + nsresult rv; + bool dictExists; + + rv = mFile->Exists(&dictExists); + if (NS_FAILED(rv)) { + return; + } + + if (!dictExists) { + // Nothing is really wrong... + return; + } + + nsCOMPtr<nsIInputStream> inStream; + NS_NewLocalFileInputStream(getter_AddRefs(inStream), mFile); + + nsCOMPtr<nsIUnicharInputStream> convStream; + rv = NS_NewUnicharInputStream(inStream, getter_AddRefs(convStream)); + if (NS_FAILED(rv)) { + return; + } + + // we're rereading to get rid of the old data -- we shouldn't have any, + // but... + mDictionaryTable.Clear(); + + char16_t c; + uint32_t nRead; + bool done = false; + do { // read each line of text into the string array. + if ((NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) break; + while (!done && ((c == '\n') || (c == '\r'))) { + if ((NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) + done = true; + } + if (!done) { + nsAutoString word; + while ((c != '\n') && (c != '\r') && !done) { + word.Append(c); + if ((NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) + done = true; + } + mDictionaryTable.PutEntry(word); + } + } while (!done); +} + +void mozPersonalDictionary::WaitForSave() { + // If no save is pending, we return straight away. + if (!mSavePending) { + return; + } + + // If a save is pending, we try to lock the same monitor that the thread uses + // that does the save. This way the main thread will be suspended until the + // monitor becomes available. + mozilla::MonitorAutoLock mon(mMonitorSave); + + // The monitor has become available. This can have two reasons: + // 1: The thread that does the save has finished. + // 2: The thread that does the save hasn't even started. + // In this case we need to wait. + if (mSavePending) { + mon.Wait(); + } +} + +NS_IMETHODIMP mozPersonalDictionary::Save() { + nsCOMPtr<nsIFile> theFile; + nsresult res; + + WaitForSave(); + + mSavePending = true; + + // FIXME Deinst -- get dictionary name from prefs; + res = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(theFile)); + if (NS_FAILED(res)) return res; + if (!theFile) return NS_ERROR_FAILURE; + res = theFile->Append(nsLiteralString(MOZ_PERSONAL_DICT_NAME)); + if (NS_FAILED(res)) return res; + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &res); + if (NS_WARN_IF(NS_FAILED(res))) { + return res; + } + + nsTArray<nsString> array; + nsString* elems = array.AppendElements(mDictionaryTable.Count()); + for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) { + elems->Assign(iter.Get()->GetKey()); + elems++; + } + + nsCOMPtr<nsIRunnable> runnable = + new mozPersonalDictionarySave(this, theFile, std::move(array)); + res = target->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(res))) { + return res; + } + return res; +} + +NS_IMETHODIMP mozPersonalDictionary::GetWordList(nsIStringEnumerator** aWords) { + NS_ENSURE_ARG_POINTER(aWords); + *aWords = nullptr; + + WaitForLoad(); + + nsTArray<nsString>* array = new nsTArray<nsString>(); + nsString* elems = array->AppendElements(mDictionaryTable.Count()); + for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) { + elems->Assign(iter.Get()->GetKey()); + elems++; + } + + array->Sort(); + + return NS_NewAdoptingStringEnumerator(aWords, array); +} + +NS_IMETHODIMP +mozPersonalDictionary::Check(const nsAString& aWord, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + WaitForLoad(); + + *aResult = (mDictionaryTable.GetEntry(aWord) || mIgnoreTable.GetEntry(aWord)); + return NS_OK; +} + +NS_IMETHODIMP +mozPersonalDictionary::AddWord(const nsAString& aWord) { + nsresult res; + WaitForLoad(); + + mDictionaryTable.PutEntry(aWord); + res = Save(); + return res; +} + +NS_IMETHODIMP +mozPersonalDictionary::RemoveWord(const nsAString& aWord) { + nsresult res; + WaitForLoad(); + + mDictionaryTable.RemoveEntry(aWord); + res = Save(); + return res; +} + +NS_IMETHODIMP +mozPersonalDictionary::IgnoreWord(const nsAString& aWord) { + // avoid adding duplicate words to the ignore list + if (!mIgnoreTable.GetEntry(aWord)) mIgnoreTable.PutEntry(aWord); + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::EndSession() { + WaitForLoad(); + + WaitForSave(); + mIgnoreTable.Clear(); + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (!nsCRT::strcmp(aTopic, "profile-do-change")) { + // The observer is registered in Init() which calls Load and in turn + // LoadInternal(); i.e. Observe() can't be called before Load(). + WaitForLoad(); + mIsLoaded = false; + Load(); // load automatically clears out the existing dictionary table + } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) { + WaitForSave(); + } + + return NS_OK; +} diff --git a/extensions/spellcheck/src/mozPersonalDictionary.h b/extensions/spellcheck/src/mozPersonalDictionary.h new file mode 100644 index 0000000000..4e356d91d9 --- /dev/null +++ b/extensions/spellcheck/src/mozPersonalDictionary.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozPersonalDictionary_h__ +#define mozPersonalDictionary_h__ + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozIPersonalDictionary.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsTHashtable.h" +#include "nsCRT.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include <mozilla/Monitor.h> + +#define MOZ_PERSONALDICTIONARY_CONTRACTID \ + "@mozilla.org/spellchecker/personaldictionary;1" +#define MOZ_PERSONALDICTIONARY_CID \ + { /* 7EF52EAF-B7E1-462B-87E2-5D1DBACA9048 */ \ + 0X7EF52EAF, 0XB7E1, 0X462B, { \ + 0X87, 0XE2, 0X5D, 0X1D, 0XBA, 0XCA, 0X90, 0X48 \ + } \ + } + +class mozPersonalDictionaryLoader; +class mozPersonalDictionarySave; + +class mozPersonalDictionary final : public mozIPersonalDictionary, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZIPERSONALDICTIONARY + NS_DECL_NSIOBSERVER + + mozPersonalDictionary(); + + nsresult Init(); + + protected: + virtual ~mozPersonalDictionary(); + + /* true if the dictionary has been loaded from disk */ + bool mIsLoaded; + + /* true if a dictionary save is pending */ + bool mSavePending; + + nsCOMPtr<nsIFile> mFile; + mozilla::Monitor mMonitor; + mozilla::Monitor mMonitorSave; + nsTHashtable<nsStringHashKey> mDictionaryTable; + nsTHashtable<nsStringHashKey> mIgnoreTable; + + private: + /* wait for the asynchronous load of the dictionary to be completed */ + void WaitForLoad(); + + /* enter the monitor before starting a synchronous load off the main-thread */ + void SyncLoad(); + + /* launch an asynchrounous load of the dictionary from the main-thread + * after successfully initializing mFile with the path of the dictionary */ + nsresult LoadInternal(); + + /* perform a synchronous load of the dictionary from disk */ + void SyncLoadInternal(); + + /* wait for the asynchronous save of the dictionary to be completed */ + void WaitForSave(); + + friend class mozPersonalDictionaryLoader; + friend class mozPersonalDictionarySave; +}; + +#endif diff --git a/extensions/spellcheck/src/mozSpellChecker.cpp b/extensions/spellcheck/src/mozSpellChecker.cpp new file mode 100644 index 0000000000..b1c242da80 --- /dev/null +++ b/extensions/spellcheck/src/mozSpellChecker.cpp @@ -0,0 +1,557 @@ +/* vim: set ts=2 sts=2 sw=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/. */ + +#include "mozSpellChecker.h" +#include "nsIStringEnumerator.h" +#include "nsICategoryManager.h" +#include "nsISupportsPrimitives.h" +#include "nsISimpleEnumerator.h" +#include "mozEnglishWordUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/PRemoteSpellcheckEngineChild.h" +#include "mozilla/TextServicesDocument.h" +#include "nsXULAppAPI.h" +#include "RemoteSpellCheckEngineChild.h" + +using mozilla::GenericPromise; +using mozilla::PRemoteSpellcheckEngineChild; +using mozilla::RemoteSpellcheckEngineChild; +using mozilla::TextServicesDocument; +using mozilla::dom::ContentChild; + +#define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" + +NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, mTextServicesDocument, + mPersonalDictionary) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(mozSpellChecker, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(mozSpellChecker, Release) + +mozSpellChecker::mozSpellChecker() : mEngine(nullptr) {} + +mozSpellChecker::~mozSpellChecker() { + if (mPersonalDictionary) { + // mPersonalDictionary->Save(); + mPersonalDictionary->EndSession(); + } + mSpellCheckingEngine = nullptr; + mPersonalDictionary = nullptr; + + if (mEngine) { + MOZ_ASSERT(XRE_IsContentProcess()); + RemoteSpellcheckEngineChild::Send__delete__(mEngine); + MOZ_ASSERT(!mEngine); + } +} + +nsresult mozSpellChecker::Init() { + mSpellCheckingEngine = nullptr; + if (XRE_IsContentProcess()) { + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + mEngine = new RemoteSpellcheckEngineChild(this); + contentChild->SendPRemoteSpellcheckEngineConstructor(mEngine); + } else { + mPersonalDictionary = + do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); + } + + return NS_OK; +} + +TextServicesDocument* mozSpellChecker::GetTextServicesDocument() { + return mTextServicesDocument; +} + +nsresult mozSpellChecker::SetDocument( + TextServicesDocument* aTextServicesDocument, bool aFromStartofDoc) { + mTextServicesDocument = aTextServicesDocument; + mFromStart = aFromStartofDoc; + return NS_OK; +} + +nsresult mozSpellChecker::NextMisspelledWord(nsAString& aWord, + nsTArray<nsString>& aSuggestions) { + if (NS_WARN_IF(!mConverter)) { + return NS_ERROR_NOT_INITIALIZED; + } + + int32_t selOffset; + nsresult result; + result = SetupDoc(&selOffset); + if (NS_FAILED(result)) return result; + + bool done; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + int32_t begin, end; + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + while (mConverter->FindNextWord(str, selOffset, &begin, &end)) { + const nsDependentSubstring currWord(str, begin, end - begin); + bool isMisspelled; + result = CheckWord(currWord, &isMisspelled, &aSuggestions); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + if (isMisspelled) { + aWord = currWord; + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, end - begin); + // After ScrollSelectionIntoView(), the pending notifications might + // be flushed and PresShell/PresContext/Frames may be dead. + // See bug 418470. + mTextServicesDocument->ScrollSelectionIntoView(); + return NS_OK; + } + selOffset = end; + } + mTextServicesDocument->NextBlock(); + selOffset = 0; + } + return NS_OK; +} + +RefPtr<mozilla::CheckWordPromise> mozSpellChecker::CheckWords( + const nsTArray<nsString>& aWords) { + if (XRE_IsContentProcess()) { + return mEngine->CheckWords(aWords); + } + + nsTArray<bool> misspells; + misspells.SetCapacity(aWords.Length()); + for (auto& word : aWords) { + bool misspelled; + nsresult rv = CheckWord(word, &misspelled, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return mozilla::CheckWordPromise::CreateAndReject(rv, __func__); + } + misspells.AppendElement(misspelled); + } + return mozilla::CheckWordPromise::CreateAndResolve(std::move(misspells), + __func__); +} + +nsresult mozSpellChecker::CheckWord(const nsAString& aWord, bool* aIsMisspelled, + nsTArray<nsString>* aSuggestions) { + nsresult result; + bool correct; + + if (XRE_IsContentProcess()) { + MOZ_ASSERT(aSuggestions, "Use CheckWords if content process"); + if (!mEngine->SendCheckAndSuggest(nsString(aWord), aIsMisspelled, + aSuggestions)) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; + } + + if (!mSpellCheckingEngine) { + return NS_ERROR_NULL_POINTER; + } + *aIsMisspelled = false; + result = mSpellCheckingEngine->Check(aWord, &correct); + NS_ENSURE_SUCCESS(result, result); + if (!correct) { + if (aSuggestions) { + result = mSpellCheckingEngine->Suggest(aWord, *aSuggestions); + NS_ENSURE_SUCCESS(result, result); + } + *aIsMisspelled = true; + } + return NS_OK; +} + +nsresult mozSpellChecker::Replace(const nsAString& aOldWord, + const nsAString& aNewWord, + bool aAllOccurrences) { + if (NS_WARN_IF(!mConverter)) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aAllOccurrences) { + MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord); + return NS_OK; + } + + int32_t selOffset; + int32_t startBlock; + int32_t begin, end; + bool done; + nsresult result; + + // find out where we are + result = SetupDoc(&selOffset); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + result = GetCurrentBlockIndex(mTextServicesDocument, &startBlock); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + + // start at the beginning + result = mTextServicesDocument->FirstBlock(); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + int32_t currOffset = 0; + int32_t currentBlock = 0; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + while (mConverter->FindNextWord(str, currOffset, &begin, &end)) { + if (aOldWord.Equals(Substring(str, begin, end - begin))) { + // if we are before the current selection point but in the same + // block move the selection point forwards + if (currentBlock == startBlock && begin < selOffset) { + selOffset += int32_t(aNewWord.Length()) - int32_t(aOldWord.Length()); + if (selOffset < begin) { + selOffset = begin; + } + } + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, end - begin); + MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord); + mTextServicesDocument->GetCurrentTextBlock(str); + end += (aNewWord.Length() - + aOldWord.Length()); // recursion was cute in GEB, not here. + } + currOffset = end; + } + mTextServicesDocument->NextBlock(); + currentBlock++; + currOffset = 0; + } + + // We are done replacing. Put the selection point back where we found it + // (or equivalent); + result = mTextServicesDocument->FirstBlock(); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + currentBlock = 0; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done && + currentBlock < startBlock) { + mTextServicesDocument->NextBlock(); + } + + // After we have moved to the block where the first occurrence of replace + // was done, put the selection to the next word following it. In case there + // is no word following it i.e if it happens to be the last word in that + // block, then move to the next block and put the selection to the first + // word in that block, otherwise when the Setupdoc() is called, it queries + // the LastSelectedBlock() and the selection offset of the last occurrence + // of the replaced word is taken instead of the first occurrence and things + // get messed up as reported in the bug 244969 + + if (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + if (mConverter->FindNextWord(str, selOffset, &begin, &end)) { + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, 0); + return NS_OK; + } + mTextServicesDocument->NextBlock(); + mTextServicesDocument->GetCurrentTextBlock(str); + if (mConverter->FindNextWord(str, 0, &begin, &end)) { + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, 0); + } + } + return NS_OK; +} + +nsresult mozSpellChecker::IgnoreAll(const nsAString& aWord) { + if (mPersonalDictionary) { + mPersonalDictionary->IgnoreWord(aWord); + } + return NS_OK; +} + +nsresult mozSpellChecker::AddWordToPersonalDictionary(const nsAString& aWord) { + nsresult res; + if (NS_WARN_IF(!mPersonalDictionary)) { + return NS_ERROR_NOT_INITIALIZED; + } + res = mPersonalDictionary->AddWord(aWord); + return res; +} + +nsresult mozSpellChecker::RemoveWordFromPersonalDictionary( + const nsAString& aWord) { + nsresult res; + if (NS_WARN_IF(!mPersonalDictionary)) { + return NS_ERROR_NOT_INITIALIZED; + } + res = mPersonalDictionary->RemoveWord(aWord); + return res; +} + +nsresult mozSpellChecker::GetPersonalDictionary(nsTArray<nsString>* aWordList) { + if (!aWordList || !mPersonalDictionary) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsIStringEnumerator> words; + mPersonalDictionary->GetWordList(getter_AddRefs(words)); + + bool hasMore; + nsAutoString word; + while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) { + words->GetNext(word); + aWordList->AppendElement(word); + } + return NS_OK; +} + +nsresult mozSpellChecker::GetDictionaryList( + nsTArray<nsCString>* aDictionaryList) { + MOZ_ASSERT(aDictionaryList->IsEmpty()); + if (XRE_IsContentProcess()) { + ContentChild* child = ContentChild::GetSingleton(); + child->GetAvailableDictionaries(*aDictionaryList); + return NS_OK; + } + + nsresult rv; + + // For catching duplicates + nsTHashtable<nsCStringHashKey> dictionaries; + + nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines; + rv = GetEngineList(&spellCheckingEngines); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { + nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i]; + + nsTArray<nsCString> dictNames; + engine->GetDictionaryList(dictNames); + for (auto& dictName : dictNames) { + // Skip duplicate dictionaries. Only take the first one + // for each name. + if (dictionaries.Contains(dictName)) continue; + + dictionaries.PutEntry(dictName); + aDictionaryList->AppendElement(dictName); + } + } + + return NS_OK; +} + +nsresult mozSpellChecker::GetCurrentDictionary(nsACString& aDictionary) { + if (XRE_IsContentProcess()) { + aDictionary = mCurrentDictionary; + return NS_OK; + } + + if (!mSpellCheckingEngine) { + aDictionary.Truncate(); + return NS_OK; + } + + return mSpellCheckingEngine->GetDictionary(aDictionary); +} + +nsresult mozSpellChecker::SetCurrentDictionary(const nsACString& aDictionary) { + if (XRE_IsContentProcess()) { + nsCString wrappedDict = nsCString(aDictionary); + bool isSuccess; + mEngine->SendSetDictionary(wrappedDict, &isSuccess); + if (!isSuccess) { + mCurrentDictionary.Truncate(); + return NS_ERROR_NOT_AVAILABLE; + } + + mCurrentDictionary = wrappedDict; + return NS_OK; + } + + // Calls to mozISpellCheckingEngine::SetDictionary might destroy us + RefPtr<mozSpellChecker> kungFuDeathGrip = this; + + mSpellCheckingEngine = nullptr; + + if (aDictionary.IsEmpty()) { + return NS_OK; + } + + nsresult rv; + nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines; + rv = GetEngineList(&spellCheckingEngines); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { + // We must set mSpellCheckingEngine before we call SetDictionary, since + // SetDictionary calls back to this spell checker to check if the + // dictionary was set + mSpellCheckingEngine = spellCheckingEngines[i]; + + rv = mSpellCheckingEngine->SetDictionary(aDictionary); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<mozIPersonalDictionary> personalDictionary = + do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); + mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get()); + + mConverter = new mozEnglishWordUtils; + return NS_OK; + } + } + + mSpellCheckingEngine = nullptr; + + // We could not find any engine with the requested dictionary + return NS_ERROR_NOT_AVAILABLE; +} + +RefPtr<GenericPromise> mozSpellChecker::SetCurrentDictionaryFromList( + const nsTArray<nsCString>& aList) { + if (aList.IsEmpty()) { + return GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__); + } + + if (XRE_IsContentProcess()) { + // mCurrentDictionary will be set by RemoteSpellCheckEngineChild + return mEngine->SetCurrentDictionaryFromList(aList); + } + + for (auto& dictionary : aList) { + nsresult rv = SetCurrentDictionary(dictionary); + if (NS_SUCCEEDED(rv)) { + return GenericPromise::CreateAndResolve(true, __func__); + } + } + // We could not find any engine with the requested dictionary + return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); +} + +nsresult mozSpellChecker::SetupDoc(int32_t* outBlockOffset) { + nsresult rv; + + TextServicesDocument::BlockSelectionStatus blockStatus; + int32_t selOffset; + int32_t selLength; + *outBlockOffset = 0; + + if (!mFromStart) { + rv = MOZ_KnownLive(mTextServicesDocument) + ->LastSelectedBlock(&blockStatus, &selOffset, &selLength); + if (NS_SUCCEEDED(rv) && + blockStatus != + TextServicesDocument::BlockSelectionStatus::eBlockNotFound) { + switch (blockStatus) { + // No TB in S, but found one before/after S. + case TextServicesDocument::BlockSelectionStatus::eBlockOutside: + // S begins or ends in TB but extends outside of TB. + case TextServicesDocument::BlockSelectionStatus::eBlockPartial: + // the TS doc points to the block we want. + *outBlockOffset = selOffset + selLength; + break; + + // S extends beyond the start and end of TB. + case TextServicesDocument::BlockSelectionStatus::eBlockInside: + // we want the block after this one. + rv = mTextServicesDocument->NextBlock(); + *outBlockOffset = 0; + break; + + // TB contains entire S. + case TextServicesDocument::BlockSelectionStatus::eBlockContains: + *outBlockOffset = selOffset + selLength; + break; + + // There is no text block (TB) in or before the selection (S). + case TextServicesDocument::BlockSelectionStatus::eBlockNotFound: + default: + MOZ_ASSERT_UNREACHABLE("Shouldn't ever get this status"); + } + } + // Failed to get last sel block. Just start at beginning + else { + rv = mTextServicesDocument->FirstBlock(); + *outBlockOffset = 0; + } + + } + // We want the first block + else { + rv = mTextServicesDocument->FirstBlock(); + mFromStart = false; + } + return rv; +} + +// utility method to discover which block we're in. The TSDoc interface doesn't +// give us this, because it can't assume a read-only document. shamelessly +// stolen from nsTextServicesDocument +nsresult mozSpellChecker::GetCurrentBlockIndex( + TextServicesDocument* aTextServicesDocument, int32_t* aOutBlockIndex) { + int32_t blockIndex = 0; + bool isDone = false; + nsresult result = NS_OK; + + do { + aTextServicesDocument->PrevBlock(); + result = aTextServicesDocument->IsDone(&isDone); + if (!isDone) { + blockIndex++; + } + } while (NS_SUCCEEDED(result) && !isDone); + + *aOutBlockIndex = blockIndex; + + return result; +} + +nsresult mozSpellChecker::GetEngineList( + nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines) { + MOZ_ASSERT(!XRE_IsContentProcess()); + + nsresult rv; + bool hasMoreEngines; + + nsCOMPtr<nsICategoryManager> catMgr = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + if (!catMgr) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsISimpleEnumerator> catEntries; + + // Get contract IDs of registrated external spell-check engines and + // append one of HunSpell at the end. + rv = catMgr->EnumerateCategory("spell-check-engine", + getter_AddRefs(catEntries)); + if (NS_FAILED(rv)) return rv; + + while (NS_SUCCEEDED(catEntries->HasMoreElements(&hasMoreEngines)) && + hasMoreEngines) { + nsCOMPtr<nsISupports> elem; + rv = catEntries->GetNext(getter_AddRefs(elem)); + + nsCOMPtr<nsISupportsCString> entry = do_QueryInterface(elem, &rv); + if (NS_FAILED(rv)) return rv; + + nsCString contractId; + rv = entry->GetData(contractId); + if (NS_FAILED(rv)) return rv; + + // Try to load spellchecker engine. Ignore errors silently + // except for the last one (HunSpell). + nsCOMPtr<mozISpellCheckingEngine> engine = + do_GetService(contractId.get(), &rv); + if (NS_SUCCEEDED(rv)) { + aSpellCheckingEngines->AppendObject(engine); + } + } + + // Try to load HunSpell spellchecker engine. + nsCOMPtr<mozISpellCheckingEngine> engine = + do_GetService(DEFAULT_SPELL_CHECKER, &rv); + if (NS_FAILED(rv)) { + // Fail if not succeeded to load HunSpell. Ignore errors + // for external spellcheck engines. + return rv; + } + aSpellCheckingEngines->AppendObject(engine); + + return NS_OK; +} diff --git a/extensions/spellcheck/src/mozSpellChecker.h b/extensions/spellcheck/src/mozSpellChecker.h new file mode 100644 index 0000000000..ca2ac2f51e --- /dev/null +++ b/extensions/spellcheck/src/mozSpellChecker.h @@ -0,0 +1,180 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozSpellChecker_h__ +#define mozSpellChecker_h__ + +#include "mozilla/MozPromise.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsString.h" +#include "mozIPersonalDictionary.h" +#include "mozISpellCheckingEngine.h" +#include "nsClassHashtable.h" +#include "nsTArray.h" +#include "nsCycleCollectionParticipant.h" + +class mozEnglishWordUtils; + +namespace mozilla { +class RemoteSpellcheckEngineChild; +class TextServicesDocument; +typedef MozPromise<CopyableTArray<bool>, nsresult, false> CheckWordPromise; +} // namespace mozilla + +class mozSpellChecker final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(mozSpellChecker) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(mozSpellChecker) + + static already_AddRefed<mozSpellChecker> Create() { + RefPtr<mozSpellChecker> spellChecker = new mozSpellChecker(); + nsresult rv = spellChecker->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + return spellChecker.forget(); + } + + /** + * Tells the spellchecker what document to check. + * @param aDoc is the document to check. + * @param aFromStartOfDoc If true, start check from beginning of document, + * if false, start check from current cursor position. + */ + nsresult SetDocument(mozilla::TextServicesDocument* aTextServicesDocument, + bool aFromStartofDoc); + + /** + * Selects (hilites) the next misspelled word in the document. + * @param aWord will contain the misspelled word. + * @param aSuggestions is an array of nsStrings, that represent the + * suggested replacements for the misspelled word. + */ + MOZ_CAN_RUN_SCRIPT + nsresult NextMisspelledWord(nsAString& aWord, + nsTArray<nsString>& aSuggestions); + + /** + * Checks if a word is misspelled. No document is required to use this method. + * @param aWord is the word to check. + * @param aIsMisspelled will be set to true if the word is misspelled. + * @param aSuggestions is an array of nsStrings which represent the + * suggested replacements for the misspelled word. The array will be empty + * in chrome process if there aren't any suggestions. If suggestions is + * unnecessary, use CheckWords of async version. + */ + nsresult CheckWord(const nsAString& aWord, bool* aIsMisspelled, + nsTArray<nsString>* aSuggestions); + + /** + * This is a flavor of CheckWord, is async version of CheckWord. + * @Param aWords is array of words to check + */ + RefPtr<mozilla::CheckWordPromise> CheckWords( + const nsTArray<nsString>& aWords); + + /** + * Replaces the old word with the specified new word. + * @param aOldWord is the word to be replaced. + * @param aNewWord is the word that is to replace old word. + * @param aAllOccurrences will replace all occurrences of old + * word, in the document, with new word when it is true. If + * false, it will replace the 1st occurrence only! + */ + MOZ_CAN_RUN_SCRIPT + nsresult Replace(const nsAString& aOldWord, const nsAString& aNewWord, + bool aAllOccurrences); + + /** + * Ignores all occurrences of the specified word in the document. + * @param aWord is the word to ignore. + */ + nsresult IgnoreAll(const nsAString& aWord); + + /** + * Add a word to the user's personal dictionary. + * @param aWord is the word to add. + */ + nsresult AddWordToPersonalDictionary(const nsAString& aWord); + + /** + * Remove a word from the user's personal dictionary. + * @param aWord is the word to remove. + */ + nsresult RemoveWordFromPersonalDictionary(const nsAString& aWord); + + /** + * Returns the list of words in the user's personal dictionary. + * @param aWordList is an array of nsStrings that represent the + * list of words in the user's personal dictionary. + */ + nsresult GetPersonalDictionary(nsTArray<nsString>* aWordList); + + /** + * Returns the list of strings representing the dictionaries + * the spellchecker supports. It was suggested that the strings + * returned be in the RFC 1766 format. This format looks something + * like <ISO 639 language code>-<ISO 3166 country code>. + * For example: en-US + * @param aDictionaryList is an array of nsStrings that represent the + * dictionaries supported by the spellchecker. + */ + nsresult GetDictionaryList(nsTArray<nsCString>* aDictionaryList); + + /** + * Returns a string representing the current dictionary. + * @param aDictionary will contain the name of the dictionary. + * This name is the same string that is in the list returned + * by GetDictionaryList(). + */ + nsresult GetCurrentDictionary(nsACString& aDictionary); + + /** + * Tells the spellchecker to use a specific dictionary. + * @param aDictionary a string that is in the list returned + * by GetDictionaryList() or an empty string. If aDictionary is + * empty string, spellchecker will be disabled. + */ + nsresult SetCurrentDictionary(const nsACString& aDictionary); + + /** + * Tells the spellchecker to use a specific dictionary from list. + * @param aList a preferred dictionary list + */ + RefPtr<mozilla::GenericPromise> SetCurrentDictionaryFromList( + const nsTArray<nsCString>& aList); + + void DeleteRemoteEngine() { mEngine = nullptr; } + + mozilla::TextServicesDocument* GetTextServicesDocument(); + + protected: + mozSpellChecker(); + virtual ~mozSpellChecker(); + + nsresult Init(); + + RefPtr<mozEnglishWordUtils> mConverter; + RefPtr<mozilla::TextServicesDocument> mTextServicesDocument; + nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary; + + nsCOMPtr<mozISpellCheckingEngine> mSpellCheckingEngine; + bool mFromStart; + + nsCString mCurrentDictionary; + + MOZ_CAN_RUN_SCRIPT + nsresult SetupDoc(int32_t* outBlockOffset); + + nsresult GetCurrentBlockIndex( + mozilla::TextServicesDocument* aTextServicesDocument, + int32_t* aOutBlockIndex); + + nsresult GetEngineList(nsCOMArray<mozISpellCheckingEngine>* aDictionaryList); + + mozilla::RemoteSpellcheckEngineChild* mEngine; + + friend class mozilla::RemoteSpellcheckEngineChild; +}; +#endif // mozSpellChecker_h__ |