diff options
Diffstat (limited to 'extensions/spellcheck/src/mozSpellChecker.cpp')
-rw-r--r-- | extensions/spellcheck/src/mozSpellChecker.cpp | 557 |
1 files changed, 557 insertions, 0 deletions
diff --git a/extensions/spellcheck/src/mozSpellChecker.cpp b/extensions/spellcheck/src/mozSpellChecker.cpp new file mode 100644 index 0000000000..b1c242da80 --- /dev/null +++ b/extensions/spellcheck/src/mozSpellChecker.cpp @@ -0,0 +1,557 @@ +/* vim: set ts=2 sts=2 sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozSpellChecker.h" +#include "nsIStringEnumerator.h" +#include "nsICategoryManager.h" +#include "nsISupportsPrimitives.h" +#include "nsISimpleEnumerator.h" +#include "mozEnglishWordUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/PRemoteSpellcheckEngineChild.h" +#include "mozilla/TextServicesDocument.h" +#include "nsXULAppAPI.h" +#include "RemoteSpellCheckEngineChild.h" + +using mozilla::GenericPromise; +using mozilla::PRemoteSpellcheckEngineChild; +using mozilla::RemoteSpellcheckEngineChild; +using mozilla::TextServicesDocument; +using mozilla::dom::ContentChild; + +#define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" + +NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, mTextServicesDocument, + mPersonalDictionary) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(mozSpellChecker, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(mozSpellChecker, Release) + +mozSpellChecker::mozSpellChecker() : mEngine(nullptr) {} + +mozSpellChecker::~mozSpellChecker() { + if (mPersonalDictionary) { + // mPersonalDictionary->Save(); + mPersonalDictionary->EndSession(); + } + mSpellCheckingEngine = nullptr; + mPersonalDictionary = nullptr; + + if (mEngine) { + MOZ_ASSERT(XRE_IsContentProcess()); + RemoteSpellcheckEngineChild::Send__delete__(mEngine); + MOZ_ASSERT(!mEngine); + } +} + +nsresult mozSpellChecker::Init() { + mSpellCheckingEngine = nullptr; + if (XRE_IsContentProcess()) { + mozilla::dom::ContentChild* contentChild = + mozilla::dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + mEngine = new RemoteSpellcheckEngineChild(this); + contentChild->SendPRemoteSpellcheckEngineConstructor(mEngine); + } else { + mPersonalDictionary = + do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); + } + + return NS_OK; +} + +TextServicesDocument* mozSpellChecker::GetTextServicesDocument() { + return mTextServicesDocument; +} + +nsresult mozSpellChecker::SetDocument( + TextServicesDocument* aTextServicesDocument, bool aFromStartofDoc) { + mTextServicesDocument = aTextServicesDocument; + mFromStart = aFromStartofDoc; + return NS_OK; +} + +nsresult mozSpellChecker::NextMisspelledWord(nsAString& aWord, + nsTArray<nsString>& aSuggestions) { + if (NS_WARN_IF(!mConverter)) { + return NS_ERROR_NOT_INITIALIZED; + } + + int32_t selOffset; + nsresult result; + result = SetupDoc(&selOffset); + if (NS_FAILED(result)) return result; + + bool done; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + int32_t begin, end; + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + while (mConverter->FindNextWord(str, selOffset, &begin, &end)) { + const nsDependentSubstring currWord(str, begin, end - begin); + bool isMisspelled; + result = CheckWord(currWord, &isMisspelled, &aSuggestions); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + if (isMisspelled) { + aWord = currWord; + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, end - begin); + // After ScrollSelectionIntoView(), the pending notifications might + // be flushed and PresShell/PresContext/Frames may be dead. + // See bug 418470. + mTextServicesDocument->ScrollSelectionIntoView(); + return NS_OK; + } + selOffset = end; + } + mTextServicesDocument->NextBlock(); + selOffset = 0; + } + return NS_OK; +} + +RefPtr<mozilla::CheckWordPromise> mozSpellChecker::CheckWords( + const nsTArray<nsString>& aWords) { + if (XRE_IsContentProcess()) { + return mEngine->CheckWords(aWords); + } + + nsTArray<bool> misspells; + misspells.SetCapacity(aWords.Length()); + for (auto& word : aWords) { + bool misspelled; + nsresult rv = CheckWord(word, &misspelled, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return mozilla::CheckWordPromise::CreateAndReject(rv, __func__); + } + misspells.AppendElement(misspelled); + } + return mozilla::CheckWordPromise::CreateAndResolve(std::move(misspells), + __func__); +} + +nsresult mozSpellChecker::CheckWord(const nsAString& aWord, bool* aIsMisspelled, + nsTArray<nsString>* aSuggestions) { + nsresult result; + bool correct; + + if (XRE_IsContentProcess()) { + MOZ_ASSERT(aSuggestions, "Use CheckWords if content process"); + if (!mEngine->SendCheckAndSuggest(nsString(aWord), aIsMisspelled, + aSuggestions)) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; + } + + if (!mSpellCheckingEngine) { + return NS_ERROR_NULL_POINTER; + } + *aIsMisspelled = false; + result = mSpellCheckingEngine->Check(aWord, &correct); + NS_ENSURE_SUCCESS(result, result); + if (!correct) { + if (aSuggestions) { + result = mSpellCheckingEngine->Suggest(aWord, *aSuggestions); + NS_ENSURE_SUCCESS(result, result); + } + *aIsMisspelled = true; + } + return NS_OK; +} + +nsresult mozSpellChecker::Replace(const nsAString& aOldWord, + const nsAString& aNewWord, + bool aAllOccurrences) { + if (NS_WARN_IF(!mConverter)) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aAllOccurrences) { + MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord); + return NS_OK; + } + + int32_t selOffset; + int32_t startBlock; + int32_t begin, end; + bool done; + nsresult result; + + // find out where we are + result = SetupDoc(&selOffset); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + result = GetCurrentBlockIndex(mTextServicesDocument, &startBlock); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + + // start at the beginning + result = mTextServicesDocument->FirstBlock(); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + int32_t currOffset = 0; + int32_t currentBlock = 0; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + while (mConverter->FindNextWord(str, currOffset, &begin, &end)) { + if (aOldWord.Equals(Substring(str, begin, end - begin))) { + // if we are before the current selection point but in the same + // block move the selection point forwards + if (currentBlock == startBlock && begin < selOffset) { + selOffset += int32_t(aNewWord.Length()) - int32_t(aOldWord.Length()); + if (selOffset < begin) { + selOffset = begin; + } + } + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, end - begin); + MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord); + mTextServicesDocument->GetCurrentTextBlock(str); + end += (aNewWord.Length() - + aOldWord.Length()); // recursion was cute in GEB, not here. + } + currOffset = end; + } + mTextServicesDocument->NextBlock(); + currentBlock++; + currOffset = 0; + } + + // We are done replacing. Put the selection point back where we found it + // (or equivalent); + result = mTextServicesDocument->FirstBlock(); + if (NS_WARN_IF(NS_FAILED(result))) { + return result; + } + currentBlock = 0; + while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done && + currentBlock < startBlock) { + mTextServicesDocument->NextBlock(); + } + + // After we have moved to the block where the first occurrence of replace + // was done, put the selection to the next word following it. In case there + // is no word following it i.e if it happens to be the last word in that + // block, then move to the next block and put the selection to the first + // word in that block, otherwise when the Setupdoc() is called, it queries + // the LastSelectedBlock() and the selection offset of the last occurrence + // of the replaced word is taken instead of the first occurrence and things + // get messed up as reported in the bug 244969 + + if (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { + nsAutoString str; + mTextServicesDocument->GetCurrentTextBlock(str); + if (mConverter->FindNextWord(str, selOffset, &begin, &end)) { + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, 0); + return NS_OK; + } + mTextServicesDocument->NextBlock(); + mTextServicesDocument->GetCurrentTextBlock(str); + if (mConverter->FindNextWord(str, 0, &begin, &end)) { + MOZ_KnownLive(mTextServicesDocument)->SetSelection(begin, 0); + } + } + return NS_OK; +} + +nsresult mozSpellChecker::IgnoreAll(const nsAString& aWord) { + if (mPersonalDictionary) { + mPersonalDictionary->IgnoreWord(aWord); + } + return NS_OK; +} + +nsresult mozSpellChecker::AddWordToPersonalDictionary(const nsAString& aWord) { + nsresult res; + if (NS_WARN_IF(!mPersonalDictionary)) { + return NS_ERROR_NOT_INITIALIZED; + } + res = mPersonalDictionary->AddWord(aWord); + return res; +} + +nsresult mozSpellChecker::RemoveWordFromPersonalDictionary( + const nsAString& aWord) { + nsresult res; + if (NS_WARN_IF(!mPersonalDictionary)) { + return NS_ERROR_NOT_INITIALIZED; + } + res = mPersonalDictionary->RemoveWord(aWord); + return res; +} + +nsresult mozSpellChecker::GetPersonalDictionary(nsTArray<nsString>* aWordList) { + if (!aWordList || !mPersonalDictionary) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsIStringEnumerator> words; + mPersonalDictionary->GetWordList(getter_AddRefs(words)); + + bool hasMore; + nsAutoString word; + while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) { + words->GetNext(word); + aWordList->AppendElement(word); + } + return NS_OK; +} + +nsresult mozSpellChecker::GetDictionaryList( + nsTArray<nsCString>* aDictionaryList) { + MOZ_ASSERT(aDictionaryList->IsEmpty()); + if (XRE_IsContentProcess()) { + ContentChild* child = ContentChild::GetSingleton(); + child->GetAvailableDictionaries(*aDictionaryList); + return NS_OK; + } + + nsresult rv; + + // For catching duplicates + nsTHashtable<nsCStringHashKey> dictionaries; + + nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines; + rv = GetEngineList(&spellCheckingEngines); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { + nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i]; + + nsTArray<nsCString> dictNames; + engine->GetDictionaryList(dictNames); + for (auto& dictName : dictNames) { + // Skip duplicate dictionaries. Only take the first one + // for each name. + if (dictionaries.Contains(dictName)) continue; + + dictionaries.PutEntry(dictName); + aDictionaryList->AppendElement(dictName); + } + } + + return NS_OK; +} + +nsresult mozSpellChecker::GetCurrentDictionary(nsACString& aDictionary) { + if (XRE_IsContentProcess()) { + aDictionary = mCurrentDictionary; + return NS_OK; + } + + if (!mSpellCheckingEngine) { + aDictionary.Truncate(); + return NS_OK; + } + + return mSpellCheckingEngine->GetDictionary(aDictionary); +} + +nsresult mozSpellChecker::SetCurrentDictionary(const nsACString& aDictionary) { + if (XRE_IsContentProcess()) { + nsCString wrappedDict = nsCString(aDictionary); + bool isSuccess; + mEngine->SendSetDictionary(wrappedDict, &isSuccess); + if (!isSuccess) { + mCurrentDictionary.Truncate(); + return NS_ERROR_NOT_AVAILABLE; + } + + mCurrentDictionary = wrappedDict; + return NS_OK; + } + + // Calls to mozISpellCheckingEngine::SetDictionary might destroy us + RefPtr<mozSpellChecker> kungFuDeathGrip = this; + + mSpellCheckingEngine = nullptr; + + if (aDictionary.IsEmpty()) { + return NS_OK; + } + + nsresult rv; + nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines; + rv = GetEngineList(&spellCheckingEngines); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { + // We must set mSpellCheckingEngine before we call SetDictionary, since + // SetDictionary calls back to this spell checker to check if the + // dictionary was set + mSpellCheckingEngine = spellCheckingEngines[i]; + + rv = mSpellCheckingEngine->SetDictionary(aDictionary); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<mozIPersonalDictionary> personalDictionary = + do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); + mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get()); + + mConverter = new mozEnglishWordUtils; + return NS_OK; + } + } + + mSpellCheckingEngine = nullptr; + + // We could not find any engine with the requested dictionary + return NS_ERROR_NOT_AVAILABLE; +} + +RefPtr<GenericPromise> mozSpellChecker::SetCurrentDictionaryFromList( + const nsTArray<nsCString>& aList) { + if (aList.IsEmpty()) { + return GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__); + } + + if (XRE_IsContentProcess()) { + // mCurrentDictionary will be set by RemoteSpellCheckEngineChild + return mEngine->SetCurrentDictionaryFromList(aList); + } + + for (auto& dictionary : aList) { + nsresult rv = SetCurrentDictionary(dictionary); + if (NS_SUCCEEDED(rv)) { + return GenericPromise::CreateAndResolve(true, __func__); + } + } + // We could not find any engine with the requested dictionary + return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); +} + +nsresult mozSpellChecker::SetupDoc(int32_t* outBlockOffset) { + nsresult rv; + + TextServicesDocument::BlockSelectionStatus blockStatus; + int32_t selOffset; + int32_t selLength; + *outBlockOffset = 0; + + if (!mFromStart) { + rv = MOZ_KnownLive(mTextServicesDocument) + ->LastSelectedBlock(&blockStatus, &selOffset, &selLength); + if (NS_SUCCEEDED(rv) && + blockStatus != + TextServicesDocument::BlockSelectionStatus::eBlockNotFound) { + switch (blockStatus) { + // No TB in S, but found one before/after S. + case TextServicesDocument::BlockSelectionStatus::eBlockOutside: + // S begins or ends in TB but extends outside of TB. + case TextServicesDocument::BlockSelectionStatus::eBlockPartial: + // the TS doc points to the block we want. + *outBlockOffset = selOffset + selLength; + break; + + // S extends beyond the start and end of TB. + case TextServicesDocument::BlockSelectionStatus::eBlockInside: + // we want the block after this one. + rv = mTextServicesDocument->NextBlock(); + *outBlockOffset = 0; + break; + + // TB contains entire S. + case TextServicesDocument::BlockSelectionStatus::eBlockContains: + *outBlockOffset = selOffset + selLength; + break; + + // There is no text block (TB) in or before the selection (S). + case TextServicesDocument::BlockSelectionStatus::eBlockNotFound: + default: + MOZ_ASSERT_UNREACHABLE("Shouldn't ever get this status"); + } + } + // Failed to get last sel block. Just start at beginning + else { + rv = mTextServicesDocument->FirstBlock(); + *outBlockOffset = 0; + } + + } + // We want the first block + else { + rv = mTextServicesDocument->FirstBlock(); + mFromStart = false; + } + return rv; +} + +// utility method to discover which block we're in. The TSDoc interface doesn't +// give us this, because it can't assume a read-only document. shamelessly +// stolen from nsTextServicesDocument +nsresult mozSpellChecker::GetCurrentBlockIndex( + TextServicesDocument* aTextServicesDocument, int32_t* aOutBlockIndex) { + int32_t blockIndex = 0; + bool isDone = false; + nsresult result = NS_OK; + + do { + aTextServicesDocument->PrevBlock(); + result = aTextServicesDocument->IsDone(&isDone); + if (!isDone) { + blockIndex++; + } + } while (NS_SUCCEEDED(result) && !isDone); + + *aOutBlockIndex = blockIndex; + + return result; +} + +nsresult mozSpellChecker::GetEngineList( + nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines) { + MOZ_ASSERT(!XRE_IsContentProcess()); + + nsresult rv; + bool hasMoreEngines; + + nsCOMPtr<nsICategoryManager> catMgr = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + if (!catMgr) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsISimpleEnumerator> catEntries; + + // Get contract IDs of registrated external spell-check engines and + // append one of HunSpell at the end. + rv = catMgr->EnumerateCategory("spell-check-engine", + getter_AddRefs(catEntries)); + if (NS_FAILED(rv)) return rv; + + while (NS_SUCCEEDED(catEntries->HasMoreElements(&hasMoreEngines)) && + hasMoreEngines) { + nsCOMPtr<nsISupports> elem; + rv = catEntries->GetNext(getter_AddRefs(elem)); + + nsCOMPtr<nsISupportsCString> entry = do_QueryInterface(elem, &rv); + if (NS_FAILED(rv)) return rv; + + nsCString contractId; + rv = entry->GetData(contractId); + if (NS_FAILED(rv)) return rv; + + // Try to load spellchecker engine. Ignore errors silently + // except for the last one (HunSpell). + nsCOMPtr<mozISpellCheckingEngine> engine = + do_GetService(contractId.get(), &rv); + if (NS_SUCCEEDED(rv)) { + aSpellCheckingEngines->AppendObject(engine); + } + } + + // Try to load HunSpell spellchecker engine. + nsCOMPtr<mozISpellCheckingEngine> engine = + do_GetService(DEFAULT_SPELL_CHECKER, &rv); + if (NS_FAILED(rv)) { + // Fail if not succeeded to load HunSpell. Ignore errors + // for external spellcheck engines. + return rv; + } + aSpellCheckingEngines->AppendObject(engine); + + return NS_OK; +} |