diff options
Diffstat (limited to 'extensions/spellcheck/src')
-rw-r--r-- | extensions/spellcheck/src/moz.build | 34 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozEnglishWordUtils.cpp | 284 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozEnglishWordUtils.h | 42 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellChecker.cpp | 2043 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellChecker.h | 272 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellWordUtil.cpp | 1085 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozInlineSpellWordUtil.h | 179 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozPersonalDictionary.cpp | 471 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozPersonalDictionary.h | 83 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellChecker.cpp | 565 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellChecker.h | 76 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellCheckerFactory.cpp | 73 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellI18NManager.cpp | 32 | ||||
-rw-r--r-- | extensions/spellcheck/src/mozSpellI18NManager.h | 30 |
14 files changed, 5269 insertions, 0 deletions
diff --git a/extensions/spellcheck/src/moz.build b/extensions/spellcheck/src/moz.build new file mode 100644 index 000000000..8d6cef588 --- /dev/null +++ b/extensions/spellcheck/src/moz.build @@ -0,0 +1,34 @@ +# -*- 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') +SOURCES += [ + 'mozEnglishWordUtils.cpp', + 'mozInlineSpellChecker.cpp', + 'mozInlineSpellWordUtil.cpp', + 'mozPersonalDictionary.cpp', + 'mozSpellChecker.cpp', + 'mozSpellCheckerFactory.cpp', + 'mozSpellI18NManager.cpp', +] + +FINAL_LIBRARY = 'xul' + +if CONFIG['MOZ_SYSTEM_HUNSPELL']: + CXXFLAGS += CONFIG['MOZ_HUNSPELL_CFLAGS'] +else: + LOCAL_INCLUDES += ['../hunspell/src'] + +LOCAL_INCLUDES += [ + '../hunspell/glue', + '/dom/base', +] +EXPORTS.mozilla += [ + 'mozSpellChecker.h', +] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.cpp b/extensions/spellcheck/src/mozEnglishWordUtils.cpp new file mode 100644 index 000000000..5033b247b --- /dev/null +++ b/extensions/spellcheck/src/mozEnglishWordUtils.cpp @@ -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/. */ + +#include "mozEnglishWordUtils.h" +#include "nsReadableUtils.h" +#include "nsIServiceManager.h" +#include "nsUnicharUtils.h" +#include "nsUnicharUtilCIID.h" +#include "nsUnicodeProperties.h" +#include "nsCRT.h" +#include "mozilla/Likely.h" + +NS_IMPL_CYCLE_COLLECTING_ADDREF(mozEnglishWordUtils) +NS_IMPL_CYCLE_COLLECTING_RELEASE(mozEnglishWordUtils) + +NS_INTERFACE_MAP_BEGIN(mozEnglishWordUtils) + NS_INTERFACE_MAP_ENTRY(mozISpellI18NUtil) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozISpellI18NUtil) + NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozEnglishWordUtils) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(mozEnglishWordUtils, + mURLDetector) + +mozEnglishWordUtils::mozEnglishWordUtils() +{ + mLanguage.AssignLiteral("en"); + + nsresult rv; + mURLDetector = do_CreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID, &rv); +} + +mozEnglishWordUtils::~mozEnglishWordUtils() +{ +} + +NS_IMETHODIMP mozEnglishWordUtils::GetLanguage(char16_t * *aLanguage) +{ + NS_ENSURE_ARG_POINTER(aLanguage); + + *aLanguage = ToNewUnicode(mLanguage); + if (!*aLanguage) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +// return the possible root forms of aWord. +NS_IMETHODIMP mozEnglishWordUtils::GetRootForm(const char16_t *aWord, uint32_t type, char16_t ***words, uint32_t *count) +{ + nsAutoString word(aWord); + char16_t **tmpPtr; + int32_t length = word.Length(); + + *count = 0; + + mozEnglishWordUtils::myspCapitalization ct = captype(word); + switch (ct) + { + case HuhCap: + case NoCap: + tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *)); + if (!tmpPtr) + return NS_ERROR_OUT_OF_MEMORY; + tmpPtr[0] = ToNewUnicode(word); + if (!tmpPtr[0]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + *words = tmpPtr; + *count = 1; + break; + + + case AllCap: + tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * 3); + if (!tmpPtr) + return NS_ERROR_OUT_OF_MEMORY; + tmpPtr[0] = ToNewUnicode(word); + if (!tmpPtr[0]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + ToLowerCase(tmpPtr[0], tmpPtr[0], length); + + tmpPtr[1] = ToNewUnicode(word); + if (!tmpPtr[1]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(1, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + ToLowerCase(tmpPtr[1], tmpPtr[1], length); + ToUpperCase(tmpPtr[1], tmpPtr[1], 1); + + tmpPtr[2] = ToNewUnicode(word); + if (!tmpPtr[2]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(2, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + + *words = tmpPtr; + *count = 3; + break; + + case InitCap: + tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * 2); + if (!tmpPtr) + return NS_ERROR_OUT_OF_MEMORY; + + tmpPtr[0] = ToNewUnicode(word); + if (!tmpPtr[0]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + ToLowerCase(tmpPtr[0], tmpPtr[0], length); + + tmpPtr[1] = ToNewUnicode(word); + if (!tmpPtr[1]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(1, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + + *words = tmpPtr; + *count = 2; + break; + default: + return NS_ERROR_FAILURE; // should never get here; + } + return NS_OK; +} + +// This needs vast improvement +bool mozEnglishWordUtils::ucIsAlpha(char16_t aChar) +{ + // XXX we have to fix callers to handle the full Unicode range + return nsIUGenCategory::kLetter == mozilla::unicode::GetGenCategory(aChar); +} + +NS_IMETHODIMP mozEnglishWordUtils::FindNextWord(const char16_t *word, uint32_t length, uint32_t offset, int32_t *begin, int32_t *end) +{ + const char16_t *p = word + offset; + const char16_t *endbuf = word + length; + const char16_t *startWord=p; + if(p<endbuf){ + // 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 == ':' || *p == '@' || *p == '.') && p < endbuf - 1) { + + // 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 + p = startWord; // reset p + + // now recursively call FindNextWord to search for the next word now that we have skipped the url + return FindNextWord(word, length, startWord - word, begin, end); + } + } + } + + while((p > startWord)&&(*(p-1) == '\'')){ // trim trailing apostrophes + p--; + } + } + else{ + startWord = endbuf; + } + if(startWord == endbuf){ + *begin = -1; + *end = -1; + } + else{ + *begin = startWord-word; + *end = p-word; + } + return NS_OK; +} + +mozEnglishWordUtils::myspCapitalization +mozEnglishWordUtils::captype(const nsString &word) +{ + char16_t* lword=ToNewUnicode(word); + ToUpperCase(lword,lword,word.Length()); + if(word.Equals(lword)){ + free(lword); + return AllCap; + } + + ToLowerCase(lword,lword,word.Length()); + if(word.Equals(lword)){ + free(lword); + return NoCap; + } + int32_t length=word.Length(); + if(Substring(word,1,length-1).Equals(lword+1)){ + free(lword); + return InitCap; + } + free(lword); + return HuhCap; +} + +// Convert the list of words in iwords to the same capitalization aWord and +// return them in owords. +NS_IMETHODIMP mozEnglishWordUtils::FromRootForm(const char16_t *aWord, const char16_t **iwords, uint32_t icount, char16_t ***owords, uint32_t *ocount) +{ + nsAutoString word(aWord); + nsresult rv = NS_OK; + + int32_t length; + char16_t **tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *)*icount); + if (!tmpPtr) + return NS_ERROR_OUT_OF_MEMORY; + + mozEnglishWordUtils::myspCapitalization ct = captype(word); + for(uint32_t i = 0; i < icount; ++i) { + length = NS_strlen(iwords[i]); + tmpPtr[i] = (char16_t *) moz_xmalloc(sizeof(char16_t) * (length + 1)); + if (MOZ_UNLIKELY(!tmpPtr[i])) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(i, tmpPtr); + return NS_ERROR_OUT_OF_MEMORY; + } + memcpy(tmpPtr[i], iwords[i], (length + 1) * sizeof(char16_t)); + + nsAutoString capTest(tmpPtr[i]); + mozEnglishWordUtils::myspCapitalization newCt=captype(capTest); + if(newCt == NoCap){ + switch(ct) + { + case HuhCap: + case NoCap: + break; + case AllCap: + ToUpperCase(tmpPtr[i],tmpPtr[i],length); + rv = NS_OK; + break; + case InitCap: + ToUpperCase(tmpPtr[i],tmpPtr[i],1); + rv = NS_OK; + break; + default: + rv = NS_ERROR_FAILURE; // should never get here; + break; + + } + } + } + if (NS_SUCCEEDED(rv)){ + *owords = tmpPtr; + *ocount = icount; + } + return rv; +} + diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.h b/extensions/spellcheck/src/mozEnglishWordUtils.h new file mode 100644 index 000000000..0bc3ddfba --- /dev/null +++ b/extensions/spellcheck/src/mozEnglishWordUtils.h @@ -0,0 +1,42 @@ +/* -*- 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 "mozISpellI18NUtil.h" +#include "nsIUnicodeEncoder.h" +#include "nsIUnicodeDecoder.h" +#include "nsString.h" + +#include "mozITXTToHTMLConv.h" +#include "nsCycleCollectionParticipant.h" + +class mozEnglishWordUtils : public mozISpellI18NUtil +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_MOZISPELLI18NUTIL + NS_DECL_CYCLE_COLLECTION_CLASS(mozEnglishWordUtils) + + mozEnglishWordUtils(); + /* additional members */ + enum myspCapitalization{ + NoCap,InitCap,AllCap,HuhCap + }; + +protected: + virtual ~mozEnglishWordUtils(); + + mozEnglishWordUtils::myspCapitalization captype(const nsString &word); + bool ucIsAlpha(char16_t aChar); + + nsString mLanguage; + nsString mCharset; + 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 000000000..96011a37e --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -0,0 +1,2043 @@ +/* -*- 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 "mozilla/EditorBase.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/Services.h" +#include "mozilla/dom/Selection.h" +#include "mozInlineSpellChecker.h" +#include "mozInlineSpellWordUtil.h" +#include "mozISpellI18NManager.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsIDOMNode.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMMouseEvent.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMNode.h" +#include "nsIDOMNodeList.h" +#include "nsIDOMEvent.h" +#include "nsGenericHTMLElement.h" +#include "nsRange.h" +#include "nsIPlaintextEditor.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIRunnable.h" +#include "nsISelection.h" +#include "nsISelectionPrivate.h" +#include "nsISelectionController.h" +#include "nsIServiceManager.h" +#include "nsITextServicesFilter.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsUnicharUtils.h" +#include "nsIContent.h" +#include "nsRange.h" +#include "nsContentUtils.h" +#include "nsIObserverService.h" +#include "nsITextControlElement.h" +#include "prtime.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// 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 50 + +// 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 spending +// too much time checking the clock. Note that misspelled words count for +// more than one word in this calculation. +#define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50 + +// This number is the number of checked words a misspelled word counts for +// when we're checking the time to see if the alloted time is up for +// spellchecking. Misspelled words take longer to process since we have to +// create a range, so they count more. The exact number isn't very important +// since this just controls how often we check the current time. +#define MISSPELLED_WORD_COUNT_PENALTY 4 + +// 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 bool ContentIsDescendantOf(nsINode* aPossibleDescendant, + nsINode* aPossibleAncestor); + +static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; + +mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker) + : mSpellChecker(aSpellChecker), mWordCount(0) +{ +} + +// 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( + EditAction aAction, + nsIDOMNode* aAnchorNode, int32_t aAnchorOffset, + nsIDOMNode* aPreviousNode, int32_t aPreviousOffset, + nsIDOMNode* aStartNode, int32_t aStartOffset, + nsIDOMNode* aEndNode, int32_t aEndOffset) +{ + nsresult rv; + + nsCOMPtr<nsIDOMDocument> doc; + rv = GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS(rv, rv); + + // save the anchor point as a range so we can find the current word later + rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset, + getter_AddRefs(mAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINode> prevNode = do_QueryInterface(aPreviousNode); + NS_ENSURE_STATE(prevNode); + + bool deleted = aAction == EditAction::deleteSelection; + if (aAction == EditAction::insertIMEText) { + // IME may remove the previous node if it cancels composition when + // there is no text around the composition. + deleted = !prevNode->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 = new nsRange(prevNode); + + // ...we need to put the start and end in the correct order + int16_t cmpResult; + rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + if (cmpResult < 0) { + // previous anchor node is before the current anchor + rv = mRange->SetStart(aPreviousNode, aPreviousOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mRange->SetEnd(aAnchorNode, aAnchorOffset); + } else { + // previous anchor node is after (or the same as) the current anchor + rv = mRange->SetStart(aAnchorNode, aAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mRange->SetEnd(aPreviousNode, aPreviousOffset); + } + NS_ENSURE_SUCCESS(rv, rv); + + // On insert save this range: DoSpellCheck optimizes things in this range. + // Otherwise, just leave this nullptr. + if (aAction == EditAction::insertText) + mCreatedRange = mRange; + + // if we were given a range, we need to expand our range to encompass it + if (aStartNode && aEndNode) { + rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + if (cmpResult < 0) { // given range starts before + rv = mRange->SetStart(aStartNode, aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + 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, + nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset, + nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset, + bool* aContinue) +{ + nsresult rv; + mOp = eOpNavigation; + + mForceNavigationWordCheck = aForceCheck; + mNewNavigationPositionOffset = aNewPositionOffset; + + // get the root node for checking + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDOMElement> rootElt; + rv = editor->GetRootElement(getter_AddRefs(rootElt)); + NS_ENSURE_SUCCESS(rv, rv); + + // the anchor node might not be in the DOM anymore, check + nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) { + *aContinue = false; + return NS_OK; + } + + nsCOMPtr<nsIDOMDocument> doc; + rv = GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset, + getter_AddRefs(mOldNavigationAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset, + getter_AddRefs(mAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + + *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: + NS_NOTREACHED("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) +{ + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor); + if (! editor) + return NS_ERROR_FAILURE; // editor is gone + + NS_ASSERTION(mAnchorRange, "No anchor for navigation!"); + nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode; + int32_t newAnchorOffset, oldAnchorOffset; + + // get the DOM position of the old caret, the range should be collapsed + nsresult rv = mOldNavigationAnchorRange->GetStartContainer( + getter_AddRefs(oldAnchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // find the word on the old caret position, this is the one that we MAY need + // to check + RefPtr<nsRange> oldWord; + rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset, + getter_AddRefs(oldWord)); + NS_ENSURE_SUCCESS(rv, rv); + + // aWordUtil.GetRangeForWord flushes pending notifications, check editor again. + editor = do_QueryReferent(mSpellChecker->mEditor); + if (! editor) + return NS_ERROR_FAILURE; // editor is gone + + // get the DOM position of the new caret, the range should be collapsed + rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mAnchorRange->GetStartOffset(&newAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // see if the new cursor position is in the word of the old cursor position + bool isInRange = false; + if (! mForceNavigationWordCheck) { + rv = oldWord->IsPointInRange(newAnchorNode, + newAnchorOffset + mNewNavigationPositionOffset, + &isInRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + 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) +{ + nsCOMPtr<nsIDOMNode> anchorNode; + nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t anchorOffset; + rv = mAnchorRange->GetStartOffset(&anchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + return aWordUtil.GetRangeForWord(anchorNode, anchorOffset, + getter_AddRefs(mNoCheckRange)); +} + +// mozInlineSpellStatus::GetDocument +// +// Returns the nsIDOMDocument object for the document for the +// current spellchecker. + +nsresult +mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument) +{ + nsresult rv; + *aDocument = nullptr; + if (! mSpellChecker->mEditor) + return NS_ERROR_UNEXPECTED; + + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMDocument> domDoc; + rv = editor->GetDocument(getter_AddRefs(domDoc)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER); + domDoc.forget(aDocument); + return NS_OK; +} + +// 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. + +nsresult +mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument, + nsIDOMNode* aNode, int32_t aOffset, nsIDOMRange** aRange) +{ + *aRange = nullptr; + nsCOMPtr<nsIDOMRange> range; + nsresult rv = aDocument->CreateRange(getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = range->SetStart(aNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->SetEnd(aNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + + range.swap(*aRange); + return NS_OK; +} + +// mozInlineSpellResume + +class mozInlineSpellResume : public Runnable +{ +public: + mozInlineSpellResume(const mozInlineSpellStatus& aStatus, + uint32_t aDisabledAsyncToken) + : mDisabledAsyncToken(aDisabledAsyncToken), mStatus(aStatus) {} + + nsresult Post() + { + return NS_DispatchToMainThread(this); + } + + 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(&mStatus); + } + return NS_OK; + } + +private: + uint32_t mDisabledAsyncToken; + 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(nsIEditActionListener) + 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(mozInlineSpellChecker, + mSpellCheck, + mTreeWalker, + mCurrentSelectionAnchorNode) + +mozInlineSpellChecker::SpellCheckingState + mozInlineSpellChecker::gCanEnableSpellChecking = + mozInlineSpellChecker::SpellCheck_Uninitialized; + +mozInlineSpellChecker::mozInlineSpellChecker() : + mNumWordsInSpellSelection(0), + mMaxNumWordsInSpellSelection(250), + mNumPendingSpellChecks(0), + mNumPendingUpdateCurrentDictionary(0), + mDisabledAsyncToken(0), + mNeedsCheckAfterNavigation(false), + mFullSpellCheckScheduled(false) +{ + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) + prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); + mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4; +} + +mozInlineSpellChecker::~mozInlineSpellChecker() +{ +} + +NS_IMETHODIMP +mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck) +{ + *aSpellCheck = mSpellCheck; + NS_IF_ADDREF(*aSpellCheck); + return NS_OK; +} + +NS_IMETHODIMP +mozInlineSpellChecker::Init(nsIEditor *aEditor) +{ + mEditor = do_GetWeakReference(aEditor); + 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; + nsCOMPtr<nsISelection> spellCheckSelection; + nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + if (NS_FAILED(rv)) { + // Ensure we still unregister event listeners (but return a failure code) + UnregisterEventListeners(); + } else { + if (!aDestroyingFrames) { + spellCheckSelection->RemoveAllRanges(); + } + + 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. + + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor); + if (mPendingSpellCheck) { + // Cancel the pending editor spell checker initialization. + mPendingSpellCheck = nullptr; + mPendingInitEditorSpellCheckCallback->Cancel(); + mPendingInitEditorSpellCheckCallback = nullptr; + ChangeNumPendingSpellChecks(-1, editor); + } + + // 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, editor); + mNumPendingUpdateCurrentDictionary = 0; + } + if (mNumPendingSpellChecks > 0) { + // If mNumPendingSpellChecks is still > 0 at this point, the remainder is + // pending scheduled spell checks. + ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor); + } + + mEditor = nullptr; + 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() +{ + nsresult rv; + if (gCanEnableSpellChecking == SpellCheck_Uninitialized) { + gCanEnableSpellChecking = SpellCheck_NotAvailable; + + nsCOMPtr<nsIEditorSpellCheck> spellchecker = + do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv); + NS_ENSURE_SUCCESS(rv, false); + + bool canSpellCheck = false; + 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() +{ + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + + editor->AddEditActionListener(this); + + nsCOMPtr<nsIDOMDocument> doc; + nsresult rv = editor->GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this, + true, false); + piTarget->AddEventListener(NS_LITERAL_STRING("click"), this, + false, false); + piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this, + false, false); + return NS_OK; +} + +// mozInlineSpellChecker::UnregisterEventListeners + +nsresult +mozInlineSpellChecker::UnregisterEventListeners() +{ + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + + editor->RemoveEditActionListener(this); + + nsCOMPtr<nsIDOMDocument> doc; + editor->GetDocument(getter_AddRefs(doc)); + NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER); + + nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc); + NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER); + + piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true); + piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false); + piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), 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 = + do_CreateInstance("@mozilla.org/editor/editorspellchecker;1"); + NS_ENSURE_STATE(mPendingSpellCheck); + + nsCOMPtr<nsITextServicesFilter> filter = + do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1"); + if (!filter) { + mPendingSpellCheck = nullptr; + NS_ENSURE_STATE(filter); + } + mPendingSpellCheck->SetFilter(filter); + + mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this); + if (!mPendingInitEditorSpellCheckCallback) { + mPendingSpellCheck = nullptr; + NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback); + } + + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor); + nsresult rv = mPendingSpellCheck->InitSpellChecker( + editor, 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, + nsIEditor* aEditor) +{ + int8_t oldNumPending = mNumPendingSpellChecks; + mNumPendingSpellChecks += aDelta; + NS_ASSERTION(mNumPendingSpellChecks >= 0, + "Unbalanced ChangeNumPendingSpellChecks calls!"); + if (oldNumPending == 0 && mNumPendingSpellChecks > 0) { + NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor); + } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) { + NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor); + } +} + +// Broadcasts the given topic to observers. aEditor is passed to observers if +// nonnull; otherwise mEditor is passed. +void +mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor) +{ + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (!os) + return; + nsCOMPtr<nsIEditor> editor = aEditor; + if (!editor) { + editor = do_QueryReferent(mEditor); + } + os->NotifyObservers(editor, 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. + +NS_IMETHODIMP +mozInlineSpellChecker::SpellCheckAfterEditorChange( + int32_t aAction, nsISelection *aSelection, + nsIDOMNode *aPreviousSelectedNode, int32_t aPreviousSelectedOffset, + nsIDOMNode *aStartNode, int32_t aStartOffset, + nsIDOMNode *aEndNode, int32_t aEndOffset) +{ + nsresult rv; + NS_ENSURE_ARG_POINTER(aSelection); + 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 + nsCOMPtr<nsIDOMNode> anchorNode; + rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + int32_t anchorOffset; + rv = aSelection->GetAnchorOffset(&anchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + mozInlineSpellStatus status(this); + rv = status.InitForEditorChange((EditAction)aAction, + anchorNode, anchorOffset, + aPreviousSelectedNode, aPreviousSelectedOffset, + aStartNode, aStartOffset, + aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = ScheduleSpellCheck(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(nsIDOMRange* aRange) +{ + if (!mSpellCheck) { + NS_WARNING_ASSERTION( + mPendingSpellCheck, + "Trying to spellcheck, but checking seems to be disabled"); + return NS_ERROR_NOT_INITIALIZED; + } + + mozInlineSpellStatus status(this); + nsRange* range = static_cast<nsRange*>(aRange); + nsresult rv = status.InitForRange(range); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} + +// mozInlineSpellChecker::GetMisspelledWord + +NS_IMETHODIMP +mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, int32_t aOffset, + nsIDOMRange **newword) +{ + NS_ENSURE_ARG_POINTER(aNode); + nsCOMPtr<nsISelection> spellCheckSelection; + nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(res, res); + + return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword); +} + +// mozInlineSpellChecker::ReplaceWord + +NS_IMETHODIMP +mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, int32_t aOffset, + const nsAString &newword) +{ + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMRange> range; + nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range)); + NS_ENSURE_SUCCESS(res, res); + + if (range) + { + // This range was retrieved from the spellchecker selection. As + // ranges cannot be shared between selections, we must clone it + // before adding it to the editor's selection. + nsCOMPtr<nsIDOMRange> editorRange; + res = range->CloneRange(getter_AddRefs(editorRange)); + NS_ENSURE_SUCCESS(res, res); + + AutoPlaceHolderBatch phb(editor, nullptr); + + nsCOMPtr<nsISelection> selection; + res = editor->GetSelection(getter_AddRefs(selection)); + NS_ENSURE_SUCCESS(res, res); + selection->RemoveAllRanges(); + selection->AddRange(editorRange); + editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + + nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor)); + if (textEditor) + textEditor->InsertText(newword); + } + + return NS_OK; +} + +// mozInlineSpellChecker::AddWordToDictionary + +NS_IMETHODIMP +mozInlineSpellChecker::AddWordToDictionary(const nsAString &word) +{ + NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); + + nsAutoString wordstr(word); + nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get()); + NS_ENSURE_SUCCESS(rv, rv); + + mozInlineSpellStatus status(this); + rv = status.InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} + +// mozInlineSpellChecker::RemoveWordFromDictionary + +NS_IMETHODIMP +mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word) +{ + NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); + + nsAutoString wordstr(word); + nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get()); + NS_ENSURE_SUCCESS(rv, rv); + + mozInlineSpellStatus status(this); + rv = status.InitForRange(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} + +// mozInlineSpellChecker::IgnoreWord + +NS_IMETHODIMP +mozInlineSpellChecker::IgnoreWord(const nsAString &word) +{ + NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); + + nsAutoString wordstr(word); + nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get()); + NS_ENSURE_SUCCESS(rv, rv); + + mozInlineSpellStatus status(this); + rv = status.InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} + +// mozInlineSpellChecker::IgnoreWords + +NS_IMETHODIMP +mozInlineSpellChecker::IgnoreWords(const char16_t **aWordsToIgnore, + uint32_t aCount) +{ + NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); + + // add each word to the ignore list and then recheck the document + for (uint32_t index = 0; index < aCount; index++) + mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]); + + mozInlineSpellStatus status(this); + nsresult rv = status.InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} + +NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, int32_t aPosition) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent, + int32_t aPosition, nsresult aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent, + int32_t aPosition) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent, + int32_t aPosition, nsresult aResult) +{ + + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, int32_t aOffset) +{ + return NS_OK; +} + +NS_IMETHODIMP +mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode, + int32_t aOffset, + nsIDOMNode *aNewLeftNode, nsresult aResult) +{ + return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0); +} + +NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, + nsIDOMNode *aParent, nsresult aResult) +{ + return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0); +} + +NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString & aString) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, + const nsAString & aString, nsresult aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection) +{ + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection) +{ + return NS_OK; +} + +// 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( + nsIDOMNode* aStartNode, int32_t aStartOffset, + nsIDOMNode* aEndNode, int32_t aEndOffset, + nsRange** aRange) +{ + nsresult rv; + *aRange = nullptr; + + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMDocument> doc; + rv = editor->GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMRange> range; + rv = doc->CreateRange(getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + + // possibly use full range of the editor + nsCOMPtr<nsIDOMElement> rootElem; + if (! aStartNode || ! aEndNode) { + rv = editor->GetRootElement(getter_AddRefs(rootElem)); + NS_ENSURE_SUCCESS(rv, rv); + + aStartNode = rootElem; + aStartOffset = 0; + + aEndNode = rootElem; + aEndOffset = -1; + } + + if (aEndOffset == -1) { + nsCOMPtr<nsIDOMNodeList> childNodes; + rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t childCount; + rv = childNodes->GetLength(&childCount); + NS_ENSURE_SUCCESS(rv, rv); + + aEndOffset = childCount; + } + + // 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; + + rv = range->SetStart(aStartNode, aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (aEndOffset) + rv = range->SetEnd(aEndNode, aEndOffset); + else + rv = range->SetEndAfter(aEndNode); + NS_ENSURE_SUCCESS(rv, rv); + + *aRange = static_cast<nsRange*>(range.forget().take()); + return NS_OK; +} + +nsresult +mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, + int32_t aStartOffset, + nsIDOMNode *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 + + mozInlineSpellStatus status(this); + rv = status.InitForRange(range); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(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(nsIEditor* aEditor, + nsINode *aNode) +{ + MOZ_ASSERT(aNode); + if (!aNode->IsContent()) + return true; + + nsIContent *content = aNode->AsContent(); + + uint32_t flags; + aEditor->GetFlags(&flags); + if (flags & nsIPlaintextEditor::eEditorMailMask) { + nsIContent *parent = content->GetParent(); + while (parent) { + if (parent->IsHTMLElement(nsGkAtoms::blockquote) && + parent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::type, + nsGkAtoms::cite, + eIgnoreCase)) { + return false; + } + if (parent->IsHTMLElement(nsGkAtoms::pre) && + parent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::_class, + nsGkAtoms::mozsignature, + 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->IsInAnonymousSubtree()) { + nsIContent *node = content->GetParent(); + while (node && node->IsInNativeAnonymousSubtree()) { + node = node->GetParent(); + } + nsCOMPtr<nsITextControlElement> textControl = do_QueryInterface(node); + if (textControl) { + 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(const mozInlineSpellStatus& aStatus) +{ + if (mFullSpellCheckScheduled) { + // Just ignore this; we're going to spell-check everything anyway + return NS_OK; + } + + RefPtr<mozInlineSpellResume> resume = + new mozInlineSpellResume(aStatus, mDisabledAsyncToken); + NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = resume->Post(); + if (NS_SUCCEEDED(rv)) { + if (aStatus.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, + mozInlineSpellStatus* aStatus) +{ + 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(); + + // 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. + mozInlineSpellStatus status(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?!?!"); + + status.mWordCount = 0; + } + + 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, + mozInlineSpellStatus* aStatus, + bool* aDoneChecking) +{ + *aDoneChecking = true; + + NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); + + // get the editor for ShouldSpellCheckNode, this may fail in reasonable + // circumstances since the editor could have gone away + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + if (! editor) + 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->GetStartParent(); + int32_t beginOffset = aStatus->mRange->StartOffset(); + nsINode* endNode = aStatus->mRange->GetEndParent(); + 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 (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) || + !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) { + // Just bail out and don't try to spell-check this + return NS_OK; + } + + aWordUtil.SetEnd(endNode, endOffset); + aWordUtil.SetPosition(beginNode, beginOffset); + } + + // aWordUtil.SetPosition flushes pending notifications, check editor again. + // XXX Uhhh, *we're* holding a strong ref to the editor. + editor = do_QueryReferent(mEditor); + if (! editor) + return NS_ERROR_FAILURE; + + int32_t wordsSinceTimeCheck = 0; + PRTime beginTime = PR_Now(); + + nsAutoString wordText; + RefPtr<nsRange> wordRange; + bool dontCheckWord; + while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText, + getter_AddRefs(wordRange), + &dontCheckWord)) && + wordRange) { + wordsSinceTimeCheck++; + + // get the range for the current word. + nsINode *beginNode; + nsINode *endNode; + int32_t beginOffset, endOffset; + + ErrorResult erv; + beginNode = wordRange->GetStartContainer(erv); + endNode = wordRange->GetEndContainer(erv); + beginOffset = wordRange->GetStartOffset(erv); + endOffset = wordRange->GetEndOffset(erv); + +#ifdef DEBUG_INLINESPELL + printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get()); + if (dontCheckWord) + printf(" (not checking)"); + printf("\n"); +#endif + + // 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(editor, 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 + bool isMisspelled; + aWordUtil.NormalizeWord(wordText); + nsresult rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled); + if (NS_FAILED(rv)) + continue; + + if (isMisspelled) { + // misspelled words count extra toward the max + wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY; + AddRange(aSpellCheckSelection, wordRange); + + aStatus->mWordCount ++; + if (aStatus->mWordCount >= mMaxMisspellingsPerCheck || + SpellCheckSelectionIsFull()) + break; + } + + // see if we've run out of time, only check every N words for perf + if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) { + wordsSinceTimeCheck = 0; + if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) { + // stop checking, our time limit has been exceeded + + // move the range to encompass the stuff that needs checking + rv = aStatus->mRange->SetStart(endNode, endOffset); + 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; + } + } + } + + return NS_OK; +} + +// An RAII helper that calls ChangeNumPendingSpellChecks on destruction. +class AutoChangeNumPendingSpellChecks +{ +public: + AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker, + int32_t aDelta) + : mSpellChecker(aSpellChecker), mDelta(aDelta) {} + + ~AutoChangeNumPendingSpellChecks() + { + mSpellChecker->ChangeNumPendingSpellChecks(mDelta); + } + +private: + RefPtr<mozInlineSpellChecker> mSpellChecker; + int32_t mDelta; +}; + +// 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(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 + + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor); + if (! editor) + return NS_OK; // editor is gone + + mozInlineSpellWordUtil wordUtil; + nsresult rv = wordUtil.Init(mEditor); + if (NS_FAILED(rv)) + return NS_OK; // editor doesn't like us, don't assert + + nsCOMPtr<nsISelection> spellCheckSelectionRef; + rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelectionRef)); + NS_ENSURE_SUCCESS(rv, rv); + + auto spellCheckSelection = + static_cast<Selection *>(spellCheckSelectionRef.get()); + + nsAutoString 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--) { + 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, aStatus); + else + rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking); + NS_ENSURE_SUCCESS(rv, rv); + + if (! doneChecking) + rv = ScheduleSpellCheck(*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(nsISelection *aSelection, + nsIDOMNode *aNode, + int32_t aOffset, + nsIDOMRange **aRange) +{ + *aRange = nullptr; + + nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection)); + + nsTArray<nsRange*> ranges; + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, 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; + aSpellCheckSelection->RemoveRange(*aRange, 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(nsISelection* aSpellCheckSelection, + nsIDOMRange* aRange) +{ + NS_ENSURE_ARG_POINTER(aSpellCheckSelection); + NS_ENSURE_ARG_POINTER(aRange); + + nsresult rv = NS_OK; + + if (!SpellCheckSelectionIsFull()) + { + rv = aSpellCheckSelection->AddRange(aRange); + if (NS_SUCCEEDED(rv)) + mNumWordsInSpellSelection++; + } + + return rv; +} + +nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection) +{ + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsISelectionController> selcon; + nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon)); + NS_ENSURE_SUCCESS(rv, rv); + + return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection); +} + +nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() +{ + nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor)); + NS_ENSURE_TRUE(editor, NS_OK); + + // figure out the old caret position based on the current selection + nsCOMPtr<nsISelection> selection; + nsresult rv = editor->GetSelection(getter_AddRefs(selection)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + + selection->GetFocusOffset(&mCurrentSelectionOffset); + + return NS_OK; +} + +// This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime +// for XPCOM's rap sheet +bool // static +ContentIsDescendantOf(nsINode* aPossibleDescendant, + nsINode* aPossibleAncestor) +{ + NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!"); + NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!"); + + do { + if (aPossibleDescendant == aPossibleAncestor) + return true; + aPossibleDescendant = aPossibleDescendant->GetParentNode(); + } while (aPossibleDescendant); + + return false; +} + +// 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<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode; + int32_t currentAnchorOffset = mCurrentSelectionOffset; + + // now remember the new focus position resulting from the event + rv = SaveCurrentSelectionPosition(); + NS_ENSURE_SUCCESS(rv, rv); + + bool shouldPost; + mozInlineSpellStatus status(this); + rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset, + currentAnchorNode, currentAnchorOffset, + mCurrentSelectionAnchorNode, mCurrentSelectionOffset, + &shouldPost); + NS_ENSURE_SUCCESS(rv, rv); + if (shouldPost) { + rv = ScheduleSpellCheck(status); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent) +{ + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType.EqualsLiteral("blur")) { + return Blur(aEvent); + } + if (eventType.EqualsLiteral("click")) { + return MouseClick(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + + return NS_OK; +} + +nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent) +{ + // force spellcheck on blur, for instance when tabbing out of a textbox + HandleNavigationEvent(true); + return NS_OK; +} + +nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent) +{ + nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent); + NS_ENSURE_TRUE(mouseEvent, NS_OK); + + // ignore any errors from HandleNavigationEvent as we don't want to prevent + // anyone else from seeing this event. + int16_t button; + mouseEvent->GetButton(&button); + HandleNavigationEvent(button != 0); + return NS_OK; +} + +nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent) +{ + nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent); + NS_ENSURE_TRUE(keyEvent, NS_OK); + + uint32_t keyCode; + keyEvent->GetKeyCode(&keyCode); + + // we only care about navigation keys that moved selection + switch (keyCode) + { + case nsIDOMKeyEvent::DOM_VK_RIGHT: + case nsIDOMKeyEvent::DOM_VK_LEFT: + HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1); + break; + case nsIDOMKeyEvent::DOM_VK_UP: + case nsIDOMKeyEvent::DOM_VK_DOWN: + case nsIDOMKeyEvent::DOM_VK_HOME: + case nsIDOMKeyEvent::DOM_VK_END: + case nsIDOMKeyEvent::DOM_VK_PAGE_UP: + case nsIDOMKeyEvent::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. + nsCOMPtr<nsIEditorSpellCheck> spellCheck = mSpellCheck ? mSpellCheck : + mPendingSpellCheck; + if (!spellCheck) { + return NS_OK; + } + + if (NS_FAILED(spellCheck->GetCurrentDictionary(mPreviousDictionary))) { + mPreviousDictionary.Truncate(); + } + + 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); + + nsAutoString currentDictionary; + if (!mSpellCheck || + NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) { + currentDictionary.Truncate(); + } + + 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 000000000..86d91c2c0 --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellChecker.h @@ -0,0 +1,272 @@ +/* -*- 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 __mozinlinespellchecker_h__ +#define __mozinlinespellchecker_h__ + +#include "mozilla/EditorBase.h" +#include "nsRange.h" +#include "nsIEditorSpellCheck.h" +#include "nsIEditActionListener.h" +#include "nsIInlineSpellChecker.h" +#include "nsIDOMTreeWalker.h" +#include "nsWeakReference.h" +#include "nsIDOMEventListener.h" +#include "nsWeakReference.h" +#include "mozISpellI18NUtil.h" +#include "nsCycleCollectionParticipant.h" + +// X.h defines KeyPress +#ifdef KeyPress +#undef KeyPress +#endif + +class mozInlineSpellWordUtil; +class mozInlineSpellChecker; +class mozInlineSpellResume; +class InitEditorSpellCheckCallback; +class UpdateCurrentDictionaryCallback; +class mozInlineSpellResume; + +class mozInlineSpellStatus +{ +public: + explicit mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker); + + nsresult InitForEditorChange(EditAction aAction, + nsIDOMNode* aAnchorNode, int32_t aAnchorOffset, + nsIDOMNode* aPreviousNode, int32_t aPreviousOffset, + nsIDOMNode* aStartNode, int32_t aStartOffset, + nsIDOMNode* aEndNode, int32_t aEndOffset); + nsresult InitForNavigation(bool aForceCheck, int32_t aNewPositionOffset, + nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset, + nsIDOMNode* aNewAnchorNode, int32_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; + + // The total number of words checked in this sequence, using this tally tells + // us when to stop. This count is preserved as we continue checking in new + // messages. + int32_t mWordCount; + + // what happened? + enum Operation { eOpChange, // for SpellCheckAfterChange except deleteSelection + eOpChangeDelete, // for SpellCheckAfterChange 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 + nsCOMPtr<nsIDOMRange> 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 + nsCOMPtr<nsIDOMRange> 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); + + nsresult GetDocument(nsIDOMDocument** aDocument); + nsresult PositionToCollapsedRange(nsIDOMDocument* aDocument, + nsIDOMNode* aNode, int32_t aOffset, + nsIDOMRange** aRange); +}; + +class mozInlineSpellChecker final : public nsIInlineSpellChecker, + public nsIEditActionListener, + 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; + + nsWeakPtr mEditor; + nsCOMPtr<nsIEditorSpellCheck> mSpellCheck; + nsCOMPtr<nsIEditorSpellCheck> mPendingSpellCheck; + nsCOMPtr<nsIDOMTreeWalker> mTreeWalker; + nsCOMPtr<mozISpellI18NUtil> mConverter; + + int32_t mNumWordsInSpellSelection; + int32_t mMaxNumWordsInSpellSelection; + + // How many misspellings we can add at once. This is often less than the max + // total number of misspellings. When you have a large textarea prepopulated + // with text with many misspellings, we can hit this limit. By making it + // lower than the total number of misspelled words, new text typed by the + // user can also have spellchecking in it. + int32_t mMaxMisspellingsPerCheck; + + // 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<nsIDOMNode> mCurrentSelectionAnchorNode; + int32_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; + + // Maintains state during the asynchronous UpdateCurrentDictionary call. + nsString mPreviousDictionary; + +public: + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIEDITACTIONLISTENER + NS_DECL_NSIINLINESPELLCHECKER + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozInlineSpellChecker, nsIDOMEventListener) + + // 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 Blur(nsIDOMEvent* aEvent); + nsresult MouseClick(nsIDOMEvent* aMouseEvent); + nsresult KeyPress(nsIDOMEvent* aKeyEvent); + + mozInlineSpellChecker(); + + // spell checks all of the words between two nodes + nsresult SpellCheckBetweenNodes(nsIDOMNode *aStartNode, + int32_t aStartOffset, + nsIDOMNode *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(nsIEditor* aEditor, nsINode *aNode); + + nsresult SpellCheckAfterChange(nsIDOMNode* aCursorNode, int32_t aCursorOffset, + nsIDOMNode* aPreviousNode, int32_t aPreviousOffset, + nsISelection* aSpellCheckSelection); + + // spell check the text contained within aRange, potentially scheduling + // another check in the future if the time threshold is reached + nsresult ScheduleSpellCheck(const mozInlineSpellStatus& aStatus); + + nsresult DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil, + mozilla::dom::Selection* aSpellCheckSelection, + mozInlineSpellStatus* aStatus); + nsresult DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, + mozilla::dom::Selection *aSpellCheckSelection, + mozInlineSpellStatus* aStatus, + bool* aDoneChecking); + + // helper routine to determine if a point is inside of the passed in selection. + nsresult IsPointInSelection(nsISelection *aSelection, + nsIDOMNode *aNode, + int32_t aOffset, + nsIDOMRange **aRange); + + nsresult CleanupRangesInSelection(mozilla::dom::Selection *aSelection); + + nsresult RemoveRange(mozilla::dom::Selection *aSpellCheckSelection, + nsRange *aRange); + nsresult AddRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange); + bool SpellCheckSelectionIsFull() { return mNumWordsInSpellSelection >= mMaxNumWordsInSpellSelection; } + + nsresult MakeSpellCheckRange(nsIDOMNode* aStartNode, int32_t aStartOffset, + nsIDOMNode* 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); + + nsresult GetSpellCheckSelection(nsISelection ** aSpellCheckSelection); + nsresult SaveCurrentSelectionPosition(); + + nsresult ResumeCheck(mozInlineSpellStatus* aStatus); + +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, + nsIEditor* aEditor = nullptr); + void NotifyObservers(const char* aTopic, nsIEditor* aEditor); +}; + +#endif /* __mozinlinespellchecker_h__ */ diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp new file mode 100644 index 000000000..3aef1533d --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp @@ -0,0 +1,1085 @@ +/* -*- 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 "nsDebug.h" +#include "nsIAtom.h" +#include "nsComponentManagerUtils.h" +#include "nsIDOMCSSStyleDeclaration.h" +#include "nsIDOMElement.h" +#include "nsIDOMRange.h" +#include "nsIEditor.h" +#include "nsIDOMNode.h" +#include "nsUnicharUtilCIID.h" +#include "nsUnicodeProperties.h" +#include "nsServiceManagerUtils.h" +#include "nsIContent.h" +#include "nsTextFragment.h" +#include "mozilla/dom/Element.h" +#include "nsRange.h" +#include "nsContentUtils.h" +#include "nsIFrame.h" +#include <algorithm> +#include "mozilla/BinarySearch.h" + +using namespace mozilla; + +// IsIgnorableCharacter +// +// These characters are ones that we should ignore in input. + +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(char16_t ch) +{ + return (ch == '\'' || + ch == 0x2019 || // RIGHT SINGLE QUOTATION MARK + ch == 0x00B7); // MIDDLE DOT +} + +// mozInlineSpellWordUtil::Init + +nsresult +mozInlineSpellWordUtil::Init(nsWeakPtr aWeakEditor) +{ + nsresult rv; + + // getting the editor can fail commonly because the editor was detached, so + // don't assert + nsCOMPtr<nsIEditor> editor = do_QueryReferent(aWeakEditor, &rv); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIDOMDocument> domDoc; + rv = editor->GetDocument(getter_AddRefs(domDoc)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER); + + mDOMDocument = domDoc; + mDocument = do_QueryInterface(domDoc); + + // Find the root node for the editor. For contenteditable we'll need something + // cleverer here. + nsCOMPtr<nsIDOMElement> rootElt; + rv = editor->GetRootElement(getter_AddRefs(rootElt)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINode> rootNode = do_QueryInterface(rootElt); + mRootNode = rootNode; + NS_ASSERTION(mRootNode, "GetRootElement returned null *and* claimed to suceed!"); + return NS_OK; +} + +static inline bool +IsTextNode(nsINode* aNode) +{ + return aNode->IsNodeOfType(nsINode::eTEXT); +} + +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) +{ + NS_PRECONDITION(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) +{ + NS_PRECONDITION(aNode, "Null starting node?"); + NS_ASSERTION(!IsTextNode(aNode), "FindNextTextNode should start with a non-text node"); + + nsINode* checkNode; + // Need to start at the aOffset'th child + nsIContent* child = aNode->GetChildAt(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 && !IsTextNode(checkNode)) { + checkNode = checkNode->GetNextNode(aRoot); + } + return checkNode; +} + +// mozInlineSpellWordUtil::SetEnd +// +// 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::SetEnd(nsINode* aEndNode, int32_t aEndOffset) +{ + NS_PRECONDITION(aEndNode, "Null end node?"); + + NS_ASSERTION(mRootNode, "Not initialized"); + + InvalidateWords(); + + if (!IsTextNode(aEndNode)) { + // End at the start of the first text node after aEndNode/aEndOffset. + aEndNode = FindNextTextNode(aEndNode, aEndOffset, mRootNode); + aEndOffset = 0; + } + mSoftEnd = NodeOffset(aEndNode, aEndOffset); + return NS_OK; +} + +nsresult +mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset) +{ + InvalidateWords(); + + if (!IsTextNode(aNode)) { + // Start at the start of the first text node after aNode/aOffset. + aNode = FindNextTextNode(aNode, aOffset, mRootNode); + aOffset = 0; + } + mSoftBegin = NodeOffset(aNode, aOffset); + + 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); +} + +// mozInlineSpellWordUtil::GetRangeForWord + +nsresult +mozInlineSpellWordUtil::GetRangeForWord(nsIDOMNode* aWordNode, + int32_t aWordOffset, + nsRange** aRange) +{ + // Set our soft end and start + nsCOMPtr<nsINode> wordNode = do_QueryInterface(aWordNode); + NodeOffset pt = NodeOffset(wordNode, 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 nsSubstring& 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. + +nsresult +mozInlineSpellWordUtil::GetNextWord(nsAString& aText, nsRange** aRange, + bool* aSkipChecking) +{ +#ifdef DEBUG_SPELLCHECK + printf("GetNextWord called; mNextWordIndex=%d\n", mNextWordIndex); +#endif + + if (mNextWordIndex < 0 || + mNextWordIndex >= int32_t(mRealWords.Length())) { + mNextWordIndex = -1; + *aRange = nullptr; + *aSkipChecking = true; + return NS_OK; + } + + const RealWord& word = mRealWords[mNextWordIndex]; + nsresult rv = MakeRangeForWord(word, aRange); + NS_ENSURE_SUCCESS(rv, rv); + ++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 NS_OK; +} + +// 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 (!mDOMDocument) + return NS_ERROR_NOT_INITIALIZED; + + RefPtr<nsRange> range = new nsRange(aBegin.mNode); + nsresult rv = range->Set(aBegin.mNode, aBegin.mOffset, + aEnd.mNode, aEnd.mOffset); + NS_ENSURE_SUCCESS(rv, rv); + range.forget(aRange); + + return NS_OK; +} + +/*********** DOM text extraction ************/ + +// 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(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; +} + +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 aNode 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(nsINode* aNode, + int32_t aBeforeOffset, + int32_t* aSeparatorOffset) +{ + // aNode is actually an nsIContent, since it's eTEXT + nsIContent* content = static_cast<nsIContent*>(aNode); + const nsTextFragment* textFragment = content->GetText(); + NS_ASSERTION(textFragment, "Where is our text?"); + for (int32_t i = std::min(aBeforeOffset, int32_t(textFragment->GetLength())) - 1; i >= 0; --i) { + if (IsDOMWordSeparator(textFragment->CharAt(i))) { + // Be greedy, find as many separators as we can + for (int32_t j = i - 1; j >= 0; --j) { + if (IsDOMWordSeparator(textFragment->CharAt(j))) { + i = j; + } else { + break; + } + } + *aSeparatorOffset = i; + return true; + } + } + return false; +} + +/** + * 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 (!IsTextNode(aNode)) + return false; + + return TextNodeContainsDOMWordSeparator(aNode, 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. + if (!element->GetPrimaryFrame()) + return false; + + // Anything that's not an inline element is a break element. + // XXXbz should replaced inlines be break elements, though? + return element->GetPrimaryFrame()->StyleDisplay()->mDisplay != + StyleDisplay::Inline; +} + +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(nsSubstring& 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)) { + nsINode* prevNode = node->GetPreviousSibling(); + while (prevNode && IsTextNode(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 (!nsContentUtils::ContentIsDescendantOf(node, 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 (IsTextNode(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.RemoveElementAt(mSoftTextDOMMapping.Length() - 1); + 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 + +mozInlineSpellWordUtil::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; +} + +/*********** 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 +struct MOZ_STACK_CLASS WordSplitState +{ + mozInlineSpellWordUtil* mWordUtil; + const nsDependentSubstring mDOMWordText; + int32_t mDOMWordOffset; + CharClass mCurCharClass; + + WordSplitState(mozInlineSpellWordUtil* aWordUtil, + const nsString& aString, int32_t aStart, int32_t aLen) + : mWordUtil(aWordUtil), mDOMWordText(aString, aStart, aLen), + 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(); + + // 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); +}; + +// WordSplitState::ClassifyCharacter + +CharClass +WordSplitState::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. + nsIUGenCategory::nsUGenCategory + charCategory = mozilla::unicode::GetGenCategory(mDOMWordText[aIndex]); + if (charCategory == nsIUGenCategory::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 == nsIUGenCategory::kSeparator || + charCategory == nsIUGenCategory::kOther || + charCategory == nsIUGenCategory::kPunctuation || + charCategory == nsIUGenCategory::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 + +void +WordSplitState::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 + +void +WordSplitState::AdvanceThroughSeparators() +{ + while (mCurCharClass == CHAR_CLASS_SEPARATOR) + Advance(); +} + +// WordSplitState::AdvanceThroughWord + +void +WordSplitState::AdvanceThroughWord() +{ + while (mCurCharClass == CHAR_CLASS_WORD) + Advance(); +} + + +// WordSplitState::IsSpecialWord + +bool +WordSplitState::IsSpecialWord() +{ + // 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 + +bool +WordSplitState::ShouldSkipWord(int32_t aStart, int32_t aLength) +{ + int32_t last = aStart + aLength; + + // check to see if the word contains a digit + for (int32_t i = aStart; i < last; i ++) { + if (unicode::GetGenCategory(mDOMWordText[i]) == nsIUGenCategory::kNumber) { + return true; + } + } + + // not special + return false; +} + +// mozInlineSpellWordUtil::SplitDOMWord + +nsresult +mozInlineSpellWordUtil::SplitDOMWord(int32_t aStart, int32_t aEnd) +{ + WordSplitState state(this, mSoftText, aStart, aEnd - aStart); + 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 000000000..b28d24ae5 --- /dev/null +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.h @@ -0,0 +1,179 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsIDOMDocument.h" +#include "nsIDocument.h" +#include "nsString.h" +#include "nsTArray.h" + +//#define DEBUG_SPELLCHECK + +class nsRange; +class nsINode; + +/** + * 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 SetEnd to 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 document's root element. + * 3. Call SetPosition to initialize the current position inside the + * previously given range. + * 4. Call GetNextWord over and over until it returns false. + */ + +class mozInlineSpellWordUtil +{ +public: + struct NodeOffset { + nsINode* mNode; + int32_t mOffset; + + 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); + } + }; + + mozInlineSpellWordUtil() + : mRootNode(nullptr), + mSoftBegin(nullptr, 0), mSoftEnd(nullptr, 0), + mNextWordIndex(-1), mSoftTextValid(false) {} + + nsresult Init(nsWeakPtr aWeakEditor); + + nsresult SetEnd(nsINode* aEndNode, int32_t aEndOffset); + + // 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 SetPosition(nsINode* aNode, int32_t aOffset); + + // 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(nsIDOMNode* aWordNode, int32_t aWordOffset, + nsRange** aRange); + + // Moves to the the next word in the range, and retrieves it's text and range. + // An empty word and a nullptr range are 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). + nsresult GetNextWord(nsAString& aText, nsRange** aRange, + bool* aSkipChecking); + + // Call to normalize some punctuation. This function takes an autostring + // so we can access characters directly. + static void NormalizeWord(nsSubstring& aWord); + + nsIDOMDocument* GetDOMDocument() const { return mDOMDocument; } + nsIDocument* GetDocument() const { return mDocument; } + nsINode* GetRootNode() { return mRootNode; } + +private: + + // cached stuff for the editor, set by Init + nsCOMPtr<nsIDOMDocument> mDOMDocument; + nsCOMPtr<nsIDocument> mDocument; + + // 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); + + // Convenience functions, object must be initialized + nsresult MakeRange(NodeOffset aBegin, NodeOffset aEnd, nsRange** aRange); + nsresult MakeRangeForWord(const RealWord& aWord, nsRange** aRange); +}; + +#endif diff --git a/extensions/spellcheck/src/mozPersonalDictionary.cpp b/extensions/spellcheck/src/mozPersonalDictionary.cpp new file mode 100644 index 000000000..efaf14356 --- /dev/null +++ b/extensions/spellcheck/src/mozPersonalDictionary.cpp @@ -0,0 +1,471 @@ +/* -*- 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 "nsIUnicharInputStream.h" +#include "nsReadableUtils.h" +#include "nsIFile.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsIObserverService.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIWeakReference.h" +#include "nsCRT.h" +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsTArray.h" +#include "nsStringEnumerator.h" +#include "nsUnicharInputStream.h" +#include "nsIRunnable.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "prio.h" +#include "mozilla/Move.h" + +#define MOZ_PERSONAL_DICT_NAME "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_CYCLE_COLLECTING_ADDREF(mozPersonalDictionary) +NS_IMPL_CYCLE_COLLECTING_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_ENTRIES_CYCLE_COLLECTION(mozPersonalDictionary) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(mozPersonalDictionary, mEncoder) + +class mozPersonalDictionaryLoader final : public mozilla::Runnable +{ +public: + explicit mozPersonalDictionaryLoader(mozPersonalDictionary *dict) : mDict(dict) + { + } + + NS_IMETHOD Run() override + { + mDict->SyncLoad(); + + // Release the dictionary on the main thread + NS_ReleaseOnMainThread(mDict.forget()); + + return NS_OK; + } + +private: + RefPtr<mozPersonalDictionary> mDict; +}; + +class mozPersonalDictionarySave final : public mozilla::Runnable +{ +public: + explicit mozPersonalDictionarySave(mozPersonalDictionary *aDict, + nsCOMPtr<nsIFile> aFile, + nsTArray<nsString> &&aDictWords) + : mDictWords(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, 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(mDict.forget()); + + 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(NS_LITERAL_STRING(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.get()); + } + } 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(NS_LITERAL_STRING(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, mozilla::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 char16_t *aWord, const char16_t *aLanguage, bool *aResult) +{ + NS_ENSURE_ARG_POINTER(aWord); + NS_ENSURE_ARG_POINTER(aResult); + + WaitForLoad(); + + *aResult = (mDictionaryTable.GetEntry(aWord) || mIgnoreTable.GetEntry(aWord)); + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::AddWord(const char16_t *aWord, const char16_t *aLang) +{ + nsresult res; + WaitForLoad(); + + mDictionaryTable.PutEntry(aWord); + res = Save(); + return res; +} + +NS_IMETHODIMP mozPersonalDictionary::RemoveWord(const char16_t *aWord, const char16_t *aLang) +{ + nsresult res; + WaitForLoad(); + + mDictionaryTable.RemoveEntry(aWord); + res = Save(); + return res; +} + +NS_IMETHODIMP mozPersonalDictionary::IgnoreWord(const char16_t *aWord) +{ + // avoid adding duplicate words to the ignore list + if (aWord && !mIgnoreTable.GetEntry(aWord)) + mIgnoreTable.PutEntry(aWord); + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::EndSession() +{ + WaitForLoad(); + + WaitForSave(); + mIgnoreTable.Clear(); + return NS_OK; +} + +NS_IMETHODIMP mozPersonalDictionary::AddCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP mozPersonalDictionary::RemoveCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP mozPersonalDictionary::GetCorrection(const char16_t *word, char16_t ***words, uint32_t *count) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +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 000000000..1a9f082d0 --- /dev/null +++ b/extensions/spellcheck/src/mozPersonalDictionary.h @@ -0,0 +1,83 @@ +/* -*- 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 "nsIUnicodeEncoder.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_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_MOZIPERSONALDICTIONARY + NS_DECL_NSIOBSERVER + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozPersonalDictionary, mozIPersonalDictionary) + + 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<nsUnicharPtrHashKey> mDictionaryTable; + nsTHashtable<nsUnicharPtrHashKey> mIgnoreTable; + + /*Encoder to use to compare with spellchecker word */ + nsCOMPtr<nsIUnicodeEncoder> mEncoder; + +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 000000000..771006be3 --- /dev/null +++ b/extensions/spellcheck/src/mozSpellChecker.cpp @@ -0,0 +1,565 @@ +/* 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 "nsIServiceManager.h" +#include "mozISpellI18NManager.h" +#include "nsIStringEnumerator.h" +#include "nsICategoryManager.h" +#include "nsISupportsPrimitives.h" +#include "nsISimpleEnumerator.h" +#include "mozilla/PRemoteSpellcheckEngineChild.h" +#include "mozilla/dom/ContentChild.h" +#include "nsXULAppAPI.h" + +using mozilla::dom::ContentChild; +using mozilla::PRemoteSpellcheckEngineChild; +using mozilla::RemoteSpellcheckEngineChild; + +#define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" + +NS_IMPL_CYCLE_COLLECTING_ADDREF(mozSpellChecker) +NS_IMPL_CYCLE_COLLECTING_RELEASE(mozSpellChecker) + +NS_INTERFACE_MAP_BEGIN(mozSpellChecker) + NS_INTERFACE_MAP_ENTRY(nsISpellChecker) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpellChecker) + NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozSpellChecker) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, + mTsDoc, + mPersonalDictionary) + +mozSpellChecker::mozSpellChecker() + : mEngine(nullptr) +{ +} + +mozSpellChecker::~mozSpellChecker() +{ + if (mPersonalDictionary) { + // mPersonalDictionary->Save(); + mPersonalDictionary->EndSession(); + } + mSpellCheckingEngine = nullptr; + mPersonalDictionary = nullptr; + + if (mEngine) { + MOZ_ASSERT(XRE_IsContentProcess()); + mEngine->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; +} + +NS_IMETHODIMP +mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc) +{ + mTsDoc = aDoc; + mFromStart = aFromStartofDoc; + return NS_OK; +} + + +NS_IMETHODIMP +mozSpellChecker::NextMisspelledWord(nsAString &aWord, nsTArray<nsString> *aSuggestions) +{ + if(!aSuggestions||!mConverter) + return NS_ERROR_NULL_POINTER; + + int32_t selOffset; + int32_t begin,end; + nsresult result; + result = SetupDoc(&selOffset); + bool isMisspelled,done; + if (NS_FAILED(result)) + return result; + + while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) + { + nsString str; + result = mTsDoc->GetCurrentTextBlock(&str); + + if (NS_FAILED(result)) + return result; + do{ + result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); + if(NS_SUCCEEDED(result)&&(begin != -1)){ + const nsAString &currWord = Substring(str, begin, end - begin); + result = CheckWord(currWord, &isMisspelled, aSuggestions); + if(isMisspelled){ + aWord = currWord; + mTsDoc->SetSelection(begin, end-begin); + // After ScrollSelectionIntoView(), the pending notifications might + // be flushed and PresShell/PresContext/Frames may be dead. + // See bug 418470. + mTsDoc->ScrollSelectionIntoView(); + return NS_OK; + } + } + selOffset = end; + }while(end != -1); + mTsDoc->NextBlock(); + selOffset=0; + } + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray<nsString> *aSuggestions) +{ + nsresult result; + bool correct; + + if (XRE_IsContentProcess()) { + nsString wordwrapped = nsString(aWord); + bool rv; + if (aSuggestions) { + rv = mEngine->SendCheckAndSuggest(wordwrapped, aIsMisspelled, aSuggestions); + } else { + rv = mEngine->SendCheck(wordwrapped, aIsMisspelled); + } + return rv ? NS_OK : NS_ERROR_NOT_AVAILABLE; + } + + if(!mSpellCheckingEngine) { + return NS_ERROR_NULL_POINTER; + } + *aIsMisspelled = false; + result = mSpellCheckingEngine->Check(PromiseFlatString(aWord).get(), &correct); + NS_ENSURE_SUCCESS(result, result); + if(!correct){ + if(aSuggestions){ + uint32_t count,i; + char16_t **words; + + result = mSpellCheckingEngine->Suggest(PromiseFlatString(aWord).get(), &words, &count); + NS_ENSURE_SUCCESS(result, result); + nsString* suggestions = aSuggestions->AppendElements(count); + for(i=0;i<count;i++){ + suggestions[i].Assign(words[i]); + } + + if (count) + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); + } + *aIsMisspelled = true; + } + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences) +{ + if(!mConverter) + return NS_ERROR_NULL_POINTER; + + nsAutoString newWord(aNewWord); // sigh + + if(aAllOccurrences){ + int32_t selOffset; + int32_t startBlock,currentBlock,currOffset; + int32_t begin,end; + bool done; + nsresult result; + nsAutoString str; + + // find out where we are + result = SetupDoc(&selOffset); + if(NS_FAILED(result)) + return result; + result = GetCurrentBlockIndex(mTsDoc,&startBlock); + if(NS_FAILED(result)) + return result; + + //start at the beginning + result = mTsDoc->FirstBlock(); + currOffset=0; + currentBlock = 0; + while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) + { + result = mTsDoc->GetCurrentTextBlock(&str); + do{ + result = mConverter->FindNextWord(str.get(),str.Length(),currOffset,&begin,&end); + if(NS_SUCCEEDED(result)&&(begin != -1)){ + 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; + } + mTsDoc->SetSelection(begin, end-begin); + mTsDoc->InsertText(&newWord); + mTsDoc->GetCurrentTextBlock(&str); + end += (aNewWord.Length() - aOldWord.Length()); // recursion was cute in GEB, not here. + } + } + currOffset = end; + }while(currOffset != -1); + mTsDoc->NextBlock(); + currentBlock++; + currOffset=0; + } + + // We are done replacing. Put the selection point back where we found it (or equivalent); + result = mTsDoc->FirstBlock(); + currentBlock = 0; + while(( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) &&(currentBlock < startBlock)){ + mTsDoc->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(mTsDoc->IsDone(&done)) && !done ){ + nsString str; + result = mTsDoc->GetCurrentTextBlock(&str); + result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); + if(end == -1) + { + mTsDoc->NextBlock(); + selOffset=0; + result = mTsDoc->GetCurrentTextBlock(&str); + result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); + mTsDoc->SetSelection(begin, 0); + } + else + mTsDoc->SetSelection(begin, 0); + } + } + else{ + mTsDoc->InsertText(&newWord); + } + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::IgnoreAll(const nsAString &aWord) +{ + if(mPersonalDictionary){ + mPersonalDictionary->IgnoreWord(PromiseFlatString(aWord).get()); + } + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::AddWordToPersonalDictionary(const nsAString &aWord) +{ + nsresult res; + char16_t empty=0; + if (!mPersonalDictionary) + return NS_ERROR_NULL_POINTER; + res = mPersonalDictionary->AddWord(PromiseFlatString(aWord).get(),&empty); + return res; +} + +NS_IMETHODIMP +mozSpellChecker::RemoveWordFromPersonalDictionary(const nsAString &aWord) +{ + nsresult res; + char16_t empty=0; + if (!mPersonalDictionary) + return NS_ERROR_NULL_POINTER; + res = mPersonalDictionary->RemoveWord(PromiseFlatString(aWord).get(),&empty); + return res; +} + +NS_IMETHODIMP +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; +} + +NS_IMETHODIMP +mozSpellChecker::GetDictionaryList(nsTArray<nsString> *aDictionaryList) +{ + if (XRE_IsContentProcess()) { + ContentChild *child = ContentChild::GetSingleton(); + child->GetAvailableDictionaries(*aDictionaryList); + return NS_OK; + } + + nsresult rv; + + // For catching duplicates + nsTHashtable<nsStringHashKey> 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]; + + uint32_t count = 0; + char16_t **words = nullptr; + engine->GetDictionaryList(&words, &count); + for (uint32_t k = 0; k < count; k++) { + nsAutoString dictName; + + dictName.Assign(words[k]); + + // Skip duplicate dictionaries. Only take the first one + // for each name. + if (dictionaries.Contains(dictName)) + continue; + + dictionaries.PutEntry(dictName); + + if (!aDictionaryList->AppendElement(dictName)) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); + } + + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary) +{ + if (XRE_IsContentProcess()) { + aDictionary = mCurrentDictionary; + return NS_OK; + } + + if (!mSpellCheckingEngine) { + aDictionary.Truncate(); + return NS_OK; + } + + nsXPIDLString dictname; + mSpellCheckingEngine->GetDictionary(getter_Copies(dictname)); + aDictionary = dictname; + return NS_OK; +} + +NS_IMETHODIMP +mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary) +{ + if (XRE_IsContentProcess()) { + nsString wrappedDict = nsString(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(PromiseFlatString(aDictionary).get()); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<mozIPersonalDictionary> personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); + mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get()); + + nsXPIDLString language; + nsCOMPtr<mozISpellI18NManager> serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return serv->GetUtil(language.get(),getter_AddRefs(mConverter)); + } + } + + mSpellCheckingEngine = nullptr; + + // We could not find any engine with the requested dictionary + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult +mozSpellChecker::SetupDoc(int32_t *outBlockOffset) +{ + nsresult rv; + + nsITextServicesDocument::TSDBlockSelectionStatus blockStatus; + int32_t selOffset; + int32_t selLength; + *outBlockOffset = 0; + + if (!mFromStart) + { + rv = mTsDoc->LastSelectedBlock(&blockStatus, &selOffset, &selLength); + if (NS_SUCCEEDED(rv) && (blockStatus != nsITextServicesDocument::eBlockNotFound)) + { + switch (blockStatus) + { + case nsITextServicesDocument::eBlockOutside: // No TB in S, but found one before/after S. + case nsITextServicesDocument::eBlockPartial: // S begins or ends in TB but extends outside of TB. + // the TS doc points to the block we want. + *outBlockOffset = selOffset + selLength; + break; + + case nsITextServicesDocument::eBlockInside: // S extends beyond the start and end of TB. + // we want the block after this one. + rv = mTsDoc->NextBlock(); + *outBlockOffset = 0; + break; + + case nsITextServicesDocument::eBlockContains: // TB contains entire S. + *outBlockOffset = selOffset + selLength; + break; + + case nsITextServicesDocument::eBlockNotFound: // There is no text block (TB) in or before the selection (S). + default: + NS_NOTREACHED("Shouldn't ever get this status"); + } + } + else //failed to get last sel block. Just start at beginning + { + rv = mTsDoc->FirstBlock(); + *outBlockOffset = 0; + } + + } + else // we want the first block + { + rv = mTsDoc->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(nsITextServicesDocument *aDoc, int32_t *outBlockIndex) +{ + int32_t blockIndex = 0; + bool isDone = false; + nsresult result = NS_OK; + + do + { + aDoc->PrevBlock(); + + result = aDoc->IsDone(&isDone); + + if (!isDone) + blockIndex ++; + + } while (NS_SUCCEEDED(result) && !isDone); + + *outBlockIndex = 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 (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 000000000..883dee38d --- /dev/null +++ b/extensions/spellcheck/src/mozSpellChecker.h @@ -0,0 +1,76 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsISpellChecker.h" +#include "nsString.h" +#include "nsITextServicesDocument.h" +#include "mozIPersonalDictionary.h" +#include "mozISpellCheckingEngine.h" +#include "nsClassHashtable.h" +#include "nsTArray.h" +#include "mozISpellI18NUtil.h" +#include "nsCycleCollectionParticipant.h" +#include "RemoteSpellCheckEngineChild.h" + +namespace mozilla { +class PRemoteSpellcheckEngineChild; +class RemoteSpellcheckEngineChild; +} // namespace mozilla + +class mozSpellChecker : public nsISpellChecker +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(mozSpellChecker) + + mozSpellChecker(); + + nsresult Init(); + + // nsISpellChecker + NS_IMETHOD SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc) override; + NS_IMETHOD NextMisspelledWord(nsAString &aWord, nsTArray<nsString> *aSuggestions) override; + NS_IMETHOD CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray<nsString> *aSuggestions) override; + NS_IMETHOD Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences) override; + NS_IMETHOD IgnoreAll(const nsAString &aWord) override; + + NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord) override; + NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord) override; + NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList) override; + + NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList) override; + NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary) override; + NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) override; + + void DeleteRemoteEngine() { + mEngine = nullptr; + } + +protected: + virtual ~mozSpellChecker(); + + nsCOMPtr<mozISpellI18NUtil> mConverter; + nsCOMPtr<nsITextServicesDocument> mTsDoc; + nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary; + + nsCOMPtr<mozISpellCheckingEngine> mSpellCheckingEngine; + bool mFromStart; + + nsString mCurrentDictionary; + + nsresult SetupDoc(int32_t *outBlockOffset); + + nsresult GetCurrentBlockIndex(nsITextServicesDocument *aDoc, int32_t *outBlockIndex); + + nsresult GetEngineList(nsCOMArray<mozISpellCheckingEngine> *aDictionaryList); + + mozilla::PRemoteSpellcheckEngineChild *mEngine; +}; +#endif // mozSpellChecker_h__ diff --git a/extensions/spellcheck/src/mozSpellCheckerFactory.cpp b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp new file mode 100644 index 000000000..301f5d284 --- /dev/null +++ b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "mozilla/ModuleUtils.h" +#include "mozHunspell.h" +#include "mozHunspellDirProvider.h" +#include "mozSpellChecker.h" +#include "mozInlineSpellChecker.h" +#include "nsTextServicesCID.h" +#include "mozPersonalDictionary.h" +#include "mozSpellI18NManager.h" +#include "nsIFile.h" + +#define NS_SPELLCHECKER_CID \ +{ /* 8227f019-afc7-461e-b030-9f185d7a0e29 */ \ +0x8227F019, 0xAFC7, 0x461e, \ +{ 0xB0, 0x30, 0x9F, 0x18, 0x5D, 0x7A, 0x0E, 0x29} } + +#define MOZ_INLINESPELLCHECKER_CID \ +{ /* 9FE5D975-09BD-44aa-A01A-66402EA28657 */ \ +0x9fe5d975, 0x9bd, 0x44aa, \ +{ 0xa0, 0x1a, 0x66, 0x40, 0x2e, 0xa2, 0x86, 0x57} } + +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozHunspell, Init) +NS_GENERIC_FACTORY_CONSTRUCTOR(mozHunspellDirProvider) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozSpellChecker, Init) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozPersonalDictionary, Init) +NS_GENERIC_FACTORY_CONSTRUCTOR(mozSpellI18NManager) +NS_GENERIC_FACTORY_CONSTRUCTOR(mozInlineSpellChecker) + +NS_DEFINE_NAMED_CID(MOZ_HUNSPELL_CID); +NS_DEFINE_NAMED_CID(HUNSPELLDIRPROVIDER_CID); +NS_DEFINE_NAMED_CID(NS_SPELLCHECKER_CID); +NS_DEFINE_NAMED_CID(MOZ_PERSONALDICTIONARY_CID); +NS_DEFINE_NAMED_CID(MOZ_SPELLI18NMANAGER_CID); +NS_DEFINE_NAMED_CID(MOZ_INLINESPELLCHECKER_CID); + +static const mozilla::Module::CIDEntry kSpellcheckCIDs[] = { + { &kMOZ_HUNSPELL_CID, false, nullptr, mozHunspellConstructor }, + { &kHUNSPELLDIRPROVIDER_CID, false, nullptr, mozHunspellDirProviderConstructor }, + { &kNS_SPELLCHECKER_CID, false, nullptr, mozSpellCheckerConstructor }, + { &kMOZ_PERSONALDICTIONARY_CID, false, nullptr, mozPersonalDictionaryConstructor }, + { &kMOZ_SPELLI18NMANAGER_CID, false, nullptr, mozSpellI18NManagerConstructor }, + { &kMOZ_INLINESPELLCHECKER_CID, false, nullptr, mozInlineSpellCheckerConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kSpellcheckContracts[] = { + { MOZ_HUNSPELL_CONTRACTID, &kMOZ_HUNSPELL_CID }, + { mozHunspellDirProvider::kContractID, &kHUNSPELLDIRPROVIDER_CID }, + { NS_SPELLCHECKER_CONTRACTID, &kNS_SPELLCHECKER_CID }, + { MOZ_PERSONALDICTIONARY_CONTRACTID, &kMOZ_PERSONALDICTIONARY_CID }, + { MOZ_SPELLI18NMANAGER_CONTRACTID, &kMOZ_SPELLI18NMANAGER_CID }, + { MOZ_INLINESPELLCHECKER_CONTRACTID, &kMOZ_INLINESPELLCHECKER_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kSpellcheckCategories[] = { + { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "spellcheck-directory-provider", mozHunspellDirProvider::kContractID }, + { nullptr } +}; + +const mozilla::Module kSpellcheckModule = { + mozilla::Module::kVersion, + kSpellcheckCIDs, + kSpellcheckContracts, + kSpellcheckCategories +}; + +NSMODULE_DEFN(mozSpellCheckerModule) = &kSpellcheckModule; diff --git a/extensions/spellcheck/src/mozSpellI18NManager.cpp b/extensions/spellcheck/src/mozSpellI18NManager.cpp new file mode 100644 index 000000000..7b77e707e --- /dev/null +++ b/extensions/spellcheck/src/mozSpellI18NManager.cpp @@ -0,0 +1,32 @@ +/* -*- 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 "mozSpellI18NManager.h" +#include "mozEnglishWordUtils.h" +#include "nsString.h" +#include "mozilla/RefPtr.h" + +NS_IMPL_ISUPPORTS(mozSpellI18NManager, mozISpellI18NManager) + +mozSpellI18NManager::mozSpellI18NManager() +{ +} + +mozSpellI18NManager::~mozSpellI18NManager() +{ +} + +NS_IMETHODIMP mozSpellI18NManager::GetUtil(const char16_t *aLanguage, mozISpellI18NUtil **_retval) +{ + if (!_retval) { + return NS_ERROR_NULL_POINTER; + } + + // XXX TODO Actually handle multiple languages. + RefPtr<mozEnglishWordUtils> utils = new mozEnglishWordUtils; + utils.forget(_retval); + + return NS_OK; +} diff --git a/extensions/spellcheck/src/mozSpellI18NManager.h b/extensions/spellcheck/src/mozSpellI18NManager.h new file mode 100644 index 000000000..3f075f69c --- /dev/null +++ b/extensions/spellcheck/src/mozSpellI18NManager.h @@ -0,0 +1,30 @@ +/* -*- 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 mozSpellI18NManager_h__ +#define mozSpellI18NManager_h__ + +#include "nsCOMPtr.h" +#include "mozISpellI18NManager.h" +#include "nsCycleCollectionParticipant.h" + +#define MOZ_SPELLI18NMANAGER_CONTRACTID "@mozilla.org/spellchecker/i18nmanager;1" +#define MOZ_SPELLI18NMANAGER_CID \ +{ /* {AEB8936F-219C-4D3C-8385-D9382DAA551A} */ \ +0xaeb8936f, 0x219c, 0x4d3c, \ + { 0x83, 0x85, 0xd9, 0x38, 0x2d, 0xaa, 0x55, 0x1a } } + +class mozSpellI18NManager : public mozISpellI18NManager +{ +protected: + virtual ~mozSpellI18NManager(); +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISPELLI18NMANAGER + + mozSpellI18NManager(); +}; +#endif + |