michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: #include "mozSpellChecker.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "mozISpellI18NManager.h" michael@0: #include "nsIStringEnumerator.h" michael@0: #include "nsICategoryManager.h" michael@0: #include "nsISupportsPrimitives.h" michael@0: #include "nsISimpleEnumerator.h" michael@0: michael@0: #define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(mozSpellChecker) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(mozSpellChecker) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN(mozSpellChecker) michael@0: NS_INTERFACE_MAP_ENTRY(nsISpellChecker) michael@0: NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpellChecker) michael@0: NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozSpellChecker) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, michael@0: mTsDoc, michael@0: mPersonalDictionary) michael@0: michael@0: mozSpellChecker::mozSpellChecker() michael@0: { michael@0: } michael@0: michael@0: mozSpellChecker::~mozSpellChecker() michael@0: { michael@0: if(mPersonalDictionary){ michael@0: // mPersonalDictionary->Save(); michael@0: mPersonalDictionary->EndSession(); michael@0: } michael@0: mSpellCheckingEngine = nullptr; michael@0: mPersonalDictionary = nullptr; michael@0: } michael@0: michael@0: nsresult michael@0: mozSpellChecker::Init() michael@0: { michael@0: mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); michael@0: michael@0: mSpellCheckingEngine = nullptr; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc) michael@0: { michael@0: mTsDoc = aDoc; michael@0: mFromStart = aFromStartofDoc; michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::NextMisspelledWord(nsAString &aWord, nsTArray *aSuggestions) michael@0: { michael@0: if(!aSuggestions||!mConverter) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: int32_t selOffset; michael@0: int32_t begin,end; michael@0: nsresult result; michael@0: result = SetupDoc(&selOffset); michael@0: bool isMisspelled,done; michael@0: if (NS_FAILED(result)) michael@0: return result; michael@0: michael@0: while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) michael@0: { michael@0: nsString str; michael@0: result = mTsDoc->GetCurrentTextBlock(&str); michael@0: michael@0: if (NS_FAILED(result)) michael@0: return result; michael@0: do{ michael@0: result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); michael@0: if(NS_SUCCEEDED(result)&&(begin != -1)){ michael@0: const nsAString &currWord = Substring(str, begin, end - begin); michael@0: result = CheckWord(currWord, &isMisspelled, aSuggestions); michael@0: if(isMisspelled){ michael@0: aWord = currWord; michael@0: mTsDoc->SetSelection(begin, end-begin); michael@0: // After ScrollSelectionIntoView(), the pending notifications might michael@0: // be flushed and PresShell/PresContext/Frames may be dead. michael@0: // See bug 418470. michael@0: mTsDoc->ScrollSelectionIntoView(); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: selOffset = end; michael@0: }while(end != -1); michael@0: mTsDoc->NextBlock(); michael@0: selOffset=0; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray *aSuggestions) michael@0: { michael@0: nsresult result; michael@0: bool correct; michael@0: if(!mSpellCheckingEngine) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: *aIsMisspelled = false; michael@0: result = mSpellCheckingEngine->Check(PromiseFlatString(aWord).get(), &correct); michael@0: NS_ENSURE_SUCCESS(result, result); michael@0: if(!correct){ michael@0: if(aSuggestions){ michael@0: uint32_t count,i; michael@0: char16_t **words; michael@0: michael@0: result = mSpellCheckingEngine->Suggest(PromiseFlatString(aWord).get(), &words, &count); michael@0: NS_ENSURE_SUCCESS(result, result); michael@0: for(i=0;iAppendElement(nsDependentString(words[i])); michael@0: } michael@0: michael@0: if (count) michael@0: NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); michael@0: } michael@0: *aIsMisspelled = true; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences) michael@0: { michael@0: if(!mConverter) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: nsAutoString newWord(aNewWord); // sigh michael@0: michael@0: if(aAllOccurrences){ michael@0: int32_t selOffset; michael@0: int32_t startBlock,currentBlock,currOffset; michael@0: int32_t begin,end; michael@0: bool done; michael@0: nsresult result; michael@0: nsAutoString str; michael@0: michael@0: // find out where we are michael@0: result = SetupDoc(&selOffset); michael@0: if(NS_FAILED(result)) michael@0: return result; michael@0: result = GetCurrentBlockIndex(mTsDoc,&startBlock); michael@0: if(NS_FAILED(result)) michael@0: return result; michael@0: michael@0: //start at the beginning michael@0: result = mTsDoc->FirstBlock(); michael@0: currOffset=0; michael@0: currentBlock = 0; michael@0: while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) michael@0: { michael@0: result = mTsDoc->GetCurrentTextBlock(&str); michael@0: do{ michael@0: result = mConverter->FindNextWord(str.get(),str.Length(),currOffset,&begin,&end); michael@0: if(NS_SUCCEEDED(result)&&(begin != -1)){ michael@0: if (aOldWord.Equals(Substring(str, begin, end-begin))) { michael@0: // if we are before the current selection point but in the same block michael@0: // move the selection point forwards michael@0: if((currentBlock == startBlock)&&(begin < selOffset)){ michael@0: selOffset += michael@0: int32_t(aNewWord.Length()) - int32_t(aOldWord.Length()); michael@0: if(selOffset < begin) selOffset=begin; michael@0: } michael@0: mTsDoc->SetSelection(begin, end-begin); michael@0: mTsDoc->InsertText(&newWord); michael@0: mTsDoc->GetCurrentTextBlock(&str); michael@0: end += (aNewWord.Length() - aOldWord.Length()); // recursion was cute in GEB, not here. michael@0: } michael@0: } michael@0: currOffset = end; michael@0: }while(currOffset != -1); michael@0: mTsDoc->NextBlock(); michael@0: currentBlock++; michael@0: currOffset=0; michael@0: } michael@0: michael@0: // We are done replacing. Put the selection point back where we found it (or equivalent); michael@0: result = mTsDoc->FirstBlock(); michael@0: currentBlock = 0; michael@0: while(( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) &&(currentBlock < startBlock)){ michael@0: mTsDoc->NextBlock(); michael@0: } michael@0: michael@0: //After we have moved to the block where the first occurrence of replace was done, put the michael@0: //selection to the next word following it. In case there is no word following it i.e if it happens michael@0: //to be the last word in that block, then move to the next block and put the selection to the michael@0: //first word in that block, otherwise when the Setupdoc() is called, it queries the LastSelectedBlock() michael@0: //and the selection offset of the last occurrence of the replaced word is taken instead of the first michael@0: //occurrence and things get messed up as reported in the bug 244969 michael@0: michael@0: if( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ){ michael@0: nsString str; michael@0: result = mTsDoc->GetCurrentTextBlock(&str); michael@0: result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); michael@0: if(end == -1) michael@0: { michael@0: mTsDoc->NextBlock(); michael@0: selOffset=0; michael@0: result = mTsDoc->GetCurrentTextBlock(&str); michael@0: result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); michael@0: mTsDoc->SetSelection(begin, 0); michael@0: } michael@0: else michael@0: mTsDoc->SetSelection(begin, 0); michael@0: } michael@0: } michael@0: else{ michael@0: mTsDoc->InsertText(&newWord); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::IgnoreAll(const nsAString &aWord) michael@0: { michael@0: if(mPersonalDictionary){ michael@0: mPersonalDictionary->IgnoreWord(PromiseFlatString(aWord).get()); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::AddWordToPersonalDictionary(const nsAString &aWord) michael@0: { michael@0: nsresult res; michael@0: char16_t empty=0; michael@0: if (!mPersonalDictionary) michael@0: return NS_ERROR_NULL_POINTER; michael@0: res = mPersonalDictionary->AddWord(PromiseFlatString(aWord).get(),&empty); michael@0: return res; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::RemoveWordFromPersonalDictionary(const nsAString &aWord) michael@0: { michael@0: nsresult res; michael@0: char16_t empty=0; michael@0: if (!mPersonalDictionary) michael@0: return NS_ERROR_NULL_POINTER; michael@0: res = mPersonalDictionary->RemoveWord(PromiseFlatString(aWord).get(),&empty); michael@0: return res; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::GetPersonalDictionary(nsTArray *aWordList) michael@0: { michael@0: if(!aWordList || !mPersonalDictionary) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: nsCOMPtr words; michael@0: mPersonalDictionary->GetWordList(getter_AddRefs(words)); michael@0: michael@0: bool hasMore; michael@0: nsAutoString word; michael@0: while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) { michael@0: words->GetNext(word); michael@0: aWordList->AppendElement(word); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::GetDictionaryList(nsTArray *aDictionaryList) michael@0: { michael@0: nsresult rv; michael@0: michael@0: // For catching duplicates michael@0: nsClassHashtable dictionaries; michael@0: michael@0: nsCOMArray spellCheckingEngines; michael@0: rv = GetEngineList(&spellCheckingEngines); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { michael@0: nsCOMPtr engine = spellCheckingEngines[i]; michael@0: michael@0: uint32_t count = 0; michael@0: char16_t **words = nullptr; michael@0: engine->GetDictionaryList(&words, &count); michael@0: for (uint32_t k = 0; k < count; k++) { michael@0: nsAutoString dictName; michael@0: michael@0: dictName.Assign(words[k]); michael@0: michael@0: // Skip duplicate dictionaries. Only take the first one michael@0: // for each name. michael@0: if (dictionaries.Get(dictName, nullptr)) michael@0: continue; michael@0: michael@0: dictionaries.Put(dictName, nullptr); michael@0: michael@0: if (!aDictionaryList->AppendElement(dictName)) { michael@0: NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: } michael@0: michael@0: NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary) michael@0: { michael@0: if (!mSpellCheckingEngine) { michael@0: aDictionary.AssignLiteral(""); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsXPIDLString dictname; michael@0: mSpellCheckingEngine->GetDictionary(getter_Copies(dictname)); michael@0: aDictionary = dictname; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary) michael@0: { michael@0: // Calls to mozISpellCheckingEngine::SetDictionary might destroy us michael@0: nsRefPtr kungFuDeathGrip = this; michael@0: michael@0: mSpellCheckingEngine = nullptr; michael@0: michael@0: if (aDictionary.IsEmpty()) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult rv; michael@0: nsCOMArray spellCheckingEngines; michael@0: rv = GetEngineList(&spellCheckingEngines); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { michael@0: // We must set mSpellCheckingEngine before we call SetDictionary, since michael@0: // SetDictionary calls back to this spell checker to check if the michael@0: // dictionary was set michael@0: mSpellCheckingEngine = spellCheckingEngines[i]; michael@0: michael@0: rv = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get()); michael@0: michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMPtr personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); michael@0: mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get()); michael@0: michael@0: nsXPIDLString language; michael@0: nsCOMPtr serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &rv)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return serv->GetUtil(language.get(),getter_AddRefs(mConverter)); michael@0: } michael@0: } michael@0: michael@0: mSpellCheckingEngine = nullptr; michael@0: michael@0: // We could not find any engine with the requested dictionary michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: mozSpellChecker::CheckCurrentDictionary() michael@0: { michael@0: // If the current dictionary has been uninstalled, we need to stop using it. michael@0: // This happens when there is a current engine, but that engine has no michael@0: // current dictionary. michael@0: michael@0: if (!mSpellCheckingEngine) { michael@0: // We didn't have a current dictionary michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsXPIDLString dictname; michael@0: mSpellCheckingEngine->GetDictionary(getter_Copies(dictname)); michael@0: michael@0: if (!dictname.IsEmpty()) { michael@0: // We still have a current dictionary michael@0: return NS_OK; michael@0: } michael@0: michael@0: // We had a current dictionary, but it has gone, so we cannot use it anymore. michael@0: mSpellCheckingEngine = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: mozSpellChecker::SetupDoc(int32_t *outBlockOffset) michael@0: { michael@0: nsresult rv; michael@0: michael@0: nsITextServicesDocument::TSDBlockSelectionStatus blockStatus; michael@0: int32_t selOffset; michael@0: int32_t selLength; michael@0: *outBlockOffset = 0; michael@0: michael@0: if (!mFromStart) michael@0: { michael@0: rv = mTsDoc->LastSelectedBlock(&blockStatus, &selOffset, &selLength); michael@0: if (NS_SUCCEEDED(rv) && (blockStatus != nsITextServicesDocument::eBlockNotFound)) michael@0: { michael@0: switch (blockStatus) michael@0: { michael@0: case nsITextServicesDocument::eBlockOutside: // No TB in S, but found one before/after S. michael@0: case nsITextServicesDocument::eBlockPartial: // S begins or ends in TB but extends outside of TB. michael@0: // the TS doc points to the block we want. michael@0: *outBlockOffset = selOffset + selLength; michael@0: break; michael@0: michael@0: case nsITextServicesDocument::eBlockInside: // S extends beyond the start and end of TB. michael@0: // we want the block after this one. michael@0: rv = mTsDoc->NextBlock(); michael@0: *outBlockOffset = 0; michael@0: break; michael@0: michael@0: case nsITextServicesDocument::eBlockContains: // TB contains entire S. michael@0: *outBlockOffset = selOffset + selLength; michael@0: break; michael@0: michael@0: case nsITextServicesDocument::eBlockNotFound: // There is no text block (TB) in or before the selection (S). michael@0: default: michael@0: NS_NOTREACHED("Shouldn't ever get this status"); michael@0: } michael@0: } michael@0: else //failed to get last sel block. Just start at beginning michael@0: { michael@0: rv = mTsDoc->FirstBlock(); michael@0: *outBlockOffset = 0; michael@0: } michael@0: michael@0: } michael@0: else // we want the first block michael@0: { michael@0: rv = mTsDoc->FirstBlock(); michael@0: mFromStart = false; michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: // utility method to discover which block we're in. The TSDoc interface doesn't give michael@0: // us this, because it can't assume a read-only document. michael@0: // shamelessly stolen from nsTextServicesDocument michael@0: nsresult michael@0: mozSpellChecker::GetCurrentBlockIndex(nsITextServicesDocument *aDoc, int32_t *outBlockIndex) michael@0: { michael@0: int32_t blockIndex = 0; michael@0: bool isDone = false; michael@0: nsresult result = NS_OK; michael@0: michael@0: do michael@0: { michael@0: aDoc->PrevBlock(); michael@0: michael@0: result = aDoc->IsDone(&isDone); michael@0: michael@0: if (!isDone) michael@0: blockIndex ++; michael@0: michael@0: } while (NS_SUCCEEDED(result) && !isDone); michael@0: michael@0: *outBlockIndex = blockIndex; michael@0: michael@0: return result; michael@0: } michael@0: michael@0: nsresult michael@0: mozSpellChecker::GetEngineList(nsCOMArray* aSpellCheckingEngines) michael@0: { michael@0: nsresult rv; michael@0: bool hasMoreEngines; michael@0: michael@0: nsCOMPtr catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID); michael@0: if (!catMgr) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: nsCOMPtr catEntries; michael@0: michael@0: // Get contract IDs of registrated external spell-check engines and michael@0: // append one of HunSpell at the end. michael@0: rv = catMgr->EnumerateCategory("spell-check-engine", getter_AddRefs(catEntries)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: while (catEntries->HasMoreElements(&hasMoreEngines), hasMoreEngines){ michael@0: nsCOMPtr elem; michael@0: rv = catEntries->GetNext(getter_AddRefs(elem)); michael@0: michael@0: nsCOMPtr entry = do_QueryInterface(elem, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsCString contractId; michael@0: rv = entry->GetData(contractId); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Try to load spellchecker engine. Ignore errors silently michael@0: // except for the last one (HunSpell). michael@0: nsCOMPtr engine = michael@0: do_GetService(contractId.get(), &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: aSpellCheckingEngines->AppendObject(engine); michael@0: } michael@0: } michael@0: michael@0: // Try to load HunSpell spellchecker engine. michael@0: nsCOMPtr engine = michael@0: do_GetService(DEFAULT_SPELL_CHECKER, &rv); michael@0: if (NS_FAILED(rv)) { michael@0: // Fail if not succeeded to load HunSpell. Ignore errors michael@0: // for external spellcheck engines. michael@0: return rv; michael@0: } michael@0: aSpellCheckingEngines->AppendObject(engine); michael@0: michael@0: return NS_OK; michael@0: }