Backed out 2 changesets (bug 1943998) for causing wd failures @ phases.py CLOSED...
[gecko.git] / editor / spellchecker / EditorSpellCheck.cpp
blobc9f5a6619e65196eeb9dcc02dea5b8eb1039ea9e
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sts=2 sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "EditorSpellCheck.h"
9 #include "EditorBase.h" // for EditorBase
10 #include "HTMLEditor.h" // for HTMLEditor
11 #include "TextServicesDocument.h" // for TextServicesDocument
13 #include "mozilla/Attributes.h" // for final
14 #include "mozilla/dom/Element.h" // for Element
15 #include "mozilla/dom/Promise.h"
16 #include "mozilla/dom/Selection.h"
17 #include "mozilla/dom/StaticRange.h"
18 #include "mozilla/intl/Locale.h" // for mozilla::intl::Locale
19 #include "mozilla/intl/LocaleService.h" // for retrieving app locale
20 #include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
21 #include "mozilla/Logging.h" // for mozilla::LazyLogModule
22 #include "mozilla/mozalloc.h" // for operator delete, etc
23 #include "mozilla/mozSpellChecker.h" // for mozSpellChecker
24 #include "mozilla/Preferences.h" // for Preferences
26 #include "nsAString.h" // for nsAString::IsEmpty, etc
27 #include "nsComponentManagerUtils.h" // for do_CreateInstance
28 #include "nsDebug.h" // for NS_ENSURE_TRUE, etc
29 #include "nsDependentSubstring.h" // for Substring
30 #include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
31 #include "nsIContent.h" // for nsIContent
32 #include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
33 #include "mozilla/dom/Document.h" // for Document
34 #include "nsIEditor.h" // for nsIEditor
35 #include "nsILoadContext.h"
36 #include "nsISupports.h" // for nsISupports
37 #include "nsISupportsUtils.h" // for NS_ADDREF
38 #include "nsIURI.h" // for nsIURI
39 #include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
40 #include "nsVariant.h" // for nsIWritableVariant, etc
41 #include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
42 #include "nsRange.h"
43 #include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
44 #include "nsServiceManagerUtils.h" // for do_GetService
45 #include "nsString.h" // for nsAutoString, nsString, etc
46 #include "nsStringFwd.h" // for nsAFlatString
47 #include "nsStyleUtil.h" // for nsStyleUtil
48 #include "nsXULAppAPI.h" // for XRE_GetProcessType
50 namespace mozilla {
52 using namespace dom;
53 using intl::LocaleService;
54 using intl::OSPreferences;
56 static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");
58 class UpdateDictionaryHolder {
59 private:
60 EditorSpellCheck* mSpellCheck;
62 public:
63 explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
64 if (mSpellCheck) {
65 mSpellCheck->BeginUpdateDictionary();
69 ~UpdateDictionaryHolder() {
70 if (mSpellCheck) {
71 mSpellCheck->EndUpdateDictionary();
76 #define CPS_PREF_NAME u"spellcheck.lang"_ns
78 /**
79 * Gets the URI of aEditor's document.
81 static nsIURI* GetDocumentURI(EditorBase* aEditor) {
82 MOZ_ASSERT(aEditor);
84 Document* doc = aEditor->AsEditorBase()->GetDocument();
85 if (NS_WARN_IF(!doc)) {
86 return nullptr;
89 return doc->GetDocumentURI();
92 static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
93 Document* doc = aEditor->AsEditorBase()->GetDocument();
94 if (NS_WARN_IF(!doc)) {
95 return nullptr;
98 return doc->GetLoadContext();
101 static nsCString DictionariesToString(
102 const nsTArray<nsCString>& aDictionaries) {
103 nsCString asString;
104 for (const auto& dictionary : aDictionaries) {
105 asString.Append(dictionary);
106 asString.Append(',');
108 return asString;
111 static void StringToDictionaries(const nsCString& aString,
112 nsTArray<nsCString>& aDictionaries) {
113 nsTArray<nsCString> asDictionaries;
114 for (const nsACString& token :
115 nsCCharSeparatedTokenizer(aString, ',').ToRange()) {
116 if (token.IsEmpty()) {
117 continue;
119 aDictionaries.AppendElement(token);
124 * Fetches the dictionary stored in content prefs and maintains state during the
125 * fetch, which is asynchronous.
127 class DictionaryFetcher final : public nsIContentPrefCallback2 {
128 public:
129 NS_DECL_ISUPPORTS
131 DictionaryFetcher(EditorSpellCheck* aSpellCheck,
132 nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
133 : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
135 NS_IMETHOD Fetch(nsIEditor* aEditor);
137 NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
138 nsCOMPtr<nsIVariant> value;
139 nsresult rv = aPref->GetValue(getter_AddRefs(value));
140 NS_ENSURE_SUCCESS(rv, rv);
141 nsCString asString;
142 value->GetAsACString(asString);
143 StringToDictionaries(asString, mDictionaries);
144 return NS_OK;
147 NS_IMETHOD HandleCompletion(uint16_t reason) override {
148 mSpellCheck->DictionaryFetched(this);
149 return NS_OK;
152 NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }
154 nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
155 uint32_t mGroup;
156 RefPtr<nsAtom> mRootContentLang;
157 RefPtr<nsAtom> mRootDocContentLang;
158 nsTArray<nsCString> mDictionaries;
160 private:
161 ~DictionaryFetcher() {}
163 RefPtr<EditorSpellCheck> mSpellCheck;
166 NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
168 class ContentPrefInitializerRunnable final : public Runnable {
169 public:
170 ContentPrefInitializerRunnable(nsIEditor* aEditor,
171 nsIContentPrefCallback2* aCallback)
172 : Runnable("ContentPrefInitializerRunnable"),
173 mEditorBase(aEditor->AsEditorBase()),
174 mCallback(aCallback) {}
176 NS_IMETHOD Run() override {
177 if (mEditorBase->Destroyed()) {
178 mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
179 return NS_OK;
182 nsCOMPtr<nsIContentPrefService2> contentPrefService =
183 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
184 if (NS_WARN_IF(!contentPrefService)) {
185 mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
186 return NS_OK;
189 nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
190 if (NS_WARN_IF(!docUri)) {
191 mCallback->HandleError(NS_ERROR_FAILURE);
192 return NS_OK;
195 nsAutoCString docUriSpec;
196 nsresult rv = docUri->GetSpec(docUriSpec);
197 if (NS_WARN_IF(NS_FAILED(rv))) {
198 mCallback->HandleError(rv);
199 return NS_OK;
202 rv = contentPrefService->GetByDomainAndName(
203 NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
204 GetLoadContext(mEditorBase), mCallback);
205 if (NS_WARN_IF(NS_FAILED(rv))) {
206 mCallback->HandleError(rv);
207 return NS_OK;
209 return NS_OK;
212 private:
213 RefPtr<EditorBase> mEditorBase;
214 nsCOMPtr<nsIContentPrefCallback2> mCallback;
217 NS_IMETHODIMP
218 DictionaryFetcher::Fetch(nsIEditor* aEditor) {
219 NS_ENSURE_ARG_POINTER(aEditor);
221 nsCOMPtr<nsIRunnable> runnable =
222 new ContentPrefInitializerRunnable(aEditor, this);
223 NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
224 EventQueuePriority::Idle);
226 return NS_OK;
230 * Stores the current dictionary for aEditor's document URL.
232 static nsresult StoreCurrentDictionaries(
233 EditorBase* aEditorBase, const nsTArray<nsCString>& aDictionaries) {
234 NS_ENSURE_ARG_POINTER(aEditorBase);
236 nsresult rv;
238 nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
239 if (NS_WARN_IF(!docUri)) {
240 return NS_ERROR_FAILURE;
243 nsAutoCString docUriSpec;
244 rv = docUri->GetSpec(docUriSpec);
245 NS_ENSURE_SUCCESS(rv, rv);
247 RefPtr<nsVariant> prefValue = new nsVariant();
249 nsCString asString = DictionariesToString(aDictionaries);
250 prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString));
252 nsCOMPtr<nsIContentPrefService2> contentPrefService =
253 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
254 NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
256 return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
257 CPS_PREF_NAME, prefValue,
258 GetLoadContext(aEditorBase), nullptr);
262 * Forgets the current dictionary stored for aEditor's document URL.
264 static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) {
265 NS_ENSURE_ARG_POINTER(aEditorBase);
267 nsresult rv;
269 nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
270 if (NS_WARN_IF(!docUri)) {
271 return NS_ERROR_FAILURE;
274 nsAutoCString docUriSpec;
275 rv = docUri->GetSpec(docUriSpec);
276 NS_ENSURE_SUCCESS(rv, rv);
278 nsCOMPtr<nsIContentPrefService2> contentPrefService =
279 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
280 NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
282 return contentPrefService->RemoveByDomainAndName(
283 NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
284 GetLoadContext(aEditorBase), nullptr);
287 NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
288 NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
290 NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
291 NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
292 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
293 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
294 NS_INTERFACE_MAP_END
296 NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)
298 EditorSpellCheck::EditorSpellCheck()
299 : mTxtSrvFilterType(0),
300 mSuggestedWordIndex(0),
301 mDictionaryFetcherGroup(0),
302 mUpdateDictionaryRunning(false) {}
304 EditorSpellCheck::~EditorSpellCheck() {
305 // Make sure we blow the spellchecker away, just in
306 // case it hasn't been destroyed already.
307 mSpellChecker = nullptr;
310 mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }
312 // The problem is that if the spell checker does not exist, we can not tell
313 // which dictionaries are installed. This function works around the problem,
314 // allowing callers to ask if we can spell check without actually doing so (and
315 // enabling or disabling UI as necessary). This just creates a spellcheck
316 // object if needed and asks it for the dictionary list.
317 NS_IMETHODIMP
318 EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
319 RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
320 if (!spellChecker) {
321 spellChecker = mozSpellChecker::Create();
322 MOZ_ASSERT(spellChecker);
324 nsTArray<nsCString> dictList;
325 nsresult rv = spellChecker->GetDictionaryList(&dictList);
326 if (NS_WARN_IF(NS_FAILED(rv))) {
327 return rv;
330 *aCanSpellCheck = !dictList.IsEmpty();
331 return NS_OK;
334 // Instances of this class can be used as either runnables or RAII helpers.
335 class CallbackCaller final : public Runnable {
336 public:
337 explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
338 : mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}
340 ~CallbackCaller() { Run(); }
342 NS_IMETHOD Run() override {
343 if (mCallback) {
344 mCallback->EditorSpellCheckDone();
345 mCallback = nullptr;
347 return NS_OK;
350 private:
351 nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
354 NS_IMETHODIMP
355 EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
356 bool aEnableSelectionChecking,
357 nsIEditorSpellCheckCallback* aCallback) {
358 NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
359 mEditor = aEditor->AsEditorBase();
361 RefPtr<Document> doc = mEditor->GetDocument();
362 if (NS_WARN_IF(!doc)) {
363 return NS_ERROR_FAILURE;
366 nsresult rv;
368 // We can spell check with any editor type
369 RefPtr<TextServicesDocument> textServicesDocument =
370 new TextServicesDocument();
371 textServicesDocument->SetFilterType(mTxtSrvFilterType);
373 // EditorBase::AddEditActionListener() needs to access mSpellChecker and
374 // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
375 // initialize them before calling TextServicesDocument::InitWithEditor()
376 // since it calls EditorBase::AddEditActionListener().
377 mSpellChecker = mozSpellChecker::Create();
378 MOZ_ASSERT(mSpellChecker);
379 rv = mSpellChecker->SetDocument(textServicesDocument, true);
380 if (NS_WARN_IF(NS_FAILED(rv))) {
381 return rv;
384 // Pass the editor to the text services document
385 rv = textServicesDocument->InitWithEditor(aEditor);
386 NS_ENSURE_SUCCESS(rv, rv);
388 if (aEnableSelectionChecking) {
389 // Find out if the section is collapsed or not.
390 // If it isn't, we want to spellcheck just the selection.
392 RefPtr<Selection> selection;
393 aEditor->GetSelection(getter_AddRefs(selection));
394 if (NS_WARN_IF(!selection)) {
395 return NS_ERROR_FAILURE;
398 if (selection->RangeCount()) {
399 RefPtr<const nsRange> range = selection->GetRangeAt(0);
400 NS_ENSURE_STATE(range);
402 if (!range->Collapsed()) {
403 // We don't want to touch the range in the selection,
404 // so create a new copy of it.
405 RefPtr<StaticRange> staticRange =
406 StaticRange::Create(range, IgnoreErrors());
407 if (NS_WARN_IF(!staticRange)) {
408 return NS_ERROR_FAILURE;
411 // Make sure the new range spans complete words.
412 rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
413 if (NS_WARN_IF(NS_FAILED(rv))) {
414 return rv;
417 // Now tell the text services that you only want
418 // to iterate over the text in this range.
419 rv = textServicesDocument->SetExtent(staticRange);
420 if (NS_WARN_IF(NS_FAILED(rv))) {
421 return rv;
426 // do not fail if UpdateCurrentDictionary fails because this method may
427 // succeed later.
428 rv = UpdateCurrentDictionary(aCallback);
429 if (NS_FAILED(rv) && aCallback) {
430 // However, if it does fail, we still need to call the callback since we
431 // discard the failure. Do it asynchronously so that the caller is always
432 // guaranteed async behavior.
433 RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
434 rv = doc->Dispatch(caller.forget());
435 NS_ENSURE_SUCCESS(rv, rv);
438 return NS_OK;
441 NS_IMETHODIMP
442 EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
443 MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
445 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
447 DeleteSuggestedWordList();
448 // Beware! This may flush notifications via synchronous
449 // ScrollSelectionIntoView.
450 RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
451 return spellChecker->NextMisspelledWord(aNextMisspelledWord,
452 mSuggestedWordList);
455 NS_IMETHODIMP
456 EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
457 // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
458 if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
459 aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
460 mSuggestedWordIndex++;
461 } else {
462 // A blank string signals that there are no more strings
463 aSuggestedWord.Truncate();
465 return NS_OK;
468 NS_IMETHODIMP
469 EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
470 bool* aIsMisspelled) {
471 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
473 DeleteSuggestedWordList();
474 return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
475 &mSuggestedWordList);
478 NS_IMETHODIMP
479 EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount,
480 JSContext* aCx, Promise** aPromise) {
481 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
483 nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
484 if (NS_WARN_IF(!globalObject)) {
485 return NS_ERROR_UNEXPECTED;
488 ErrorResult result;
489 RefPtr<Promise> promise = Promise::Create(globalObject, result);
490 if (NS_WARN_IF(result.Failed())) {
491 return result.StealNSResult();
494 mSpellChecker->Suggest(aSuggestedWord, aCount)
495 ->Then(
496 GetMainThreadSerialEventTarget(), __func__,
497 [promise](const CopyableTArray<nsString>& aSuggestions) {
498 promise->MaybeResolve(aSuggestions);
500 [promise](nsresult aError) {
501 promise->MaybeReject(NS_ERROR_FAILURE);
504 promise.forget(aPromise);
505 return NS_OK;
508 RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
509 const nsTArray<nsString>& aSuggestedWords) {
510 if (NS_WARN_IF(!mSpellChecker)) {
511 return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
512 __func__);
515 return mSpellChecker->CheckWords(aSuggestedWords);
518 NS_IMETHODIMP
519 EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
520 const nsAString& aReplaceWord,
521 bool aAllOccurrences) {
522 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
524 RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
525 return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
528 NS_IMETHODIMP
529 EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
530 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
532 return mSpellChecker->IgnoreAll(aWord);
535 NS_IMETHODIMP
536 EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
537 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
539 return mSpellChecker->AddWordToPersonalDictionary(aWord);
542 NS_IMETHODIMP
543 EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
544 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
546 return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
549 NS_IMETHODIMP
550 EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
551 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
553 return mSpellChecker->GetDictionaryList(&aList);
556 NS_IMETHODIMP
557 EditorSpellCheck::GetCurrentDictionaries(nsTArray<nsCString>& aDictionaries) {
558 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
559 return mSpellChecker->GetCurrentDictionaries(aDictionaries);
562 NS_IMETHODIMP
563 EditorSpellCheck::SetCurrentDictionaries(
564 const nsTArray<nsCString>& aDictionaries, JSContext* aCx,
565 Promise** aPromise) {
566 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
568 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
570 // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
571 // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
572 // is on the stack. In other words: Only do this, if the user manually
573 // selected a dictionary to use.
574 if (!mUpdateDictionaryRunning) {
575 // Ignore pending dictionary fetchers by increasing this number.
576 mDictionaryFetcherGroup++;
578 uint32_t flags = 0;
579 mEditor->GetFlags(&flags);
580 if (!(flags & nsIEditor::eEditorMailMask)) {
581 bool contentPrefMatchesUserPref = true;
582 // Check if aDictionaries has the same languages as mPreferredLangs.
583 if (!aDictionaries.IsEmpty()) {
584 if (aDictionaries.Length() != mPreferredLangs.Length()) {
585 contentPrefMatchesUserPref = false;
586 } else {
587 for (const auto& dictName : aDictionaries) {
588 if (mPreferredLangs.IndexOf(dictName) ==
589 nsTArray<nsCString>::NoIndex) {
590 contentPrefMatchesUserPref = false;
591 break;
596 if (!contentPrefMatchesUserPref) {
597 // When user sets dictionary manually, we store this value associated
598 // with editor url, if it doesn't match the document language exactly.
599 // For example on "en" sites, we need to store "en-GB", otherwise
600 // the language might jump back to en-US although the user explicitly
601 // chose otherwise.
602 StoreCurrentDictionaries(mEditor, aDictionaries);
603 #ifdef DEBUG_DICT
604 printf("***** Writing content preferences for |%s|\n",
605 DictionariesToString(aDictionaries).Data());
606 #endif
607 } else {
608 // If user sets a dictionary matching the language defined by
609 // document, we consider content pref has been canceled, and we clear
610 // it.
611 ClearCurrentDictionaries(mEditor);
612 #ifdef DEBUG_DICT
613 printf("***** Clearing content preferences for |%s|\n",
614 DictionariesToString(aDictionaries).Data());
615 #endif
618 // Also store it in as a preference, so we can use it as a fallback.
619 // We don't want this for mail composer because it uses
620 // "spellchecker.dictionary" as a preference.
622 // XXX: Prefs can only be set in the parent process, so this condition is
623 // necessary to stop libpref from throwing errors. But this should
624 // probably be handled in a better way.
625 if (XRE_IsParentProcess()) {
626 nsCString asString = DictionariesToString(aDictionaries);
627 Preferences::SetCString("spellchecker.dictionary", asString);
628 #ifdef DEBUG_DICT
629 printf("***** Possibly storing spellchecker.dictionary |%s|\n",
630 asString.Data());
631 #endif
633 } else {
634 MOZ_ASSERT(flags & nsIEditor::eEditorMailMask);
635 // Since the mail editor can only influence the language selection by the
636 // html lang attribute, set the content-language document to persist
637 // multi language selections.
638 // XXX Why doesn't here use the document of the editor directly?
639 nsCOMPtr<nsIContent> anonymousDivOrEditingHost;
640 if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) {
641 anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost();
642 } else {
643 anonymousDivOrEditingHost = mEditor->GetRoot();
645 RefPtr<Document> ownerDoc = anonymousDivOrEditingHost->OwnerDoc();
646 Document* parentDoc = ownerDoc->GetInProcessParentDocument();
647 if (parentDoc) {
648 parentDoc->SetHeaderData(
649 nsGkAtoms::headerContentLanguage,
650 NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries)));
655 nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
656 if (NS_WARN_IF(!globalObject)) {
657 return NS_ERROR_UNEXPECTED;
660 ErrorResult result;
661 RefPtr<Promise> promise = Promise::Create(globalObject, result);
662 if (NS_WARN_IF(result.Failed())) {
663 return result.StealNSResult();
666 mSpellChecker->SetCurrentDictionaries(aDictionaries)
667 ->Then(
668 GetMainThreadSerialEventTarget(), __func__,
669 [promise]() { promise->MaybeResolveWithUndefined(); },
670 [promise](nsresult aError) {
671 promise->MaybeReject(NS_ERROR_FAILURE);
674 promise.forget(aPromise);
675 return NS_OK;
678 NS_IMETHODIMP
679 EditorSpellCheck::UninitSpellChecker() {
680 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
682 // Cleanup - kill the spell checker
683 DeleteSuggestedWordList();
684 mDictionaryFetcherGroup++;
685 mSpellChecker = nullptr;
686 return NS_OK;
689 NS_IMETHODIMP
690 EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
691 mTxtSrvFilterType = aFilterType;
692 return NS_OK;
695 nsresult EditorSpellCheck::DeleteSuggestedWordList() {
696 mSuggestedWordList.Clear();
697 mSuggestedWordIndex = 0;
698 return NS_OK;
701 NS_IMETHODIMP
702 EditorSpellCheck::UpdateCurrentDictionary(
703 nsIEditorSpellCheckCallback* aCallback) {
704 if (NS_WARN_IF(!mSpellChecker)) {
705 return NS_ERROR_NOT_INITIALIZED;
708 nsresult rv;
710 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
712 // Get language with html5 algorithm
713 const RefPtr<Element> rootEditableElement =
714 [](const EditorBase& aEditorBase) -> Element* {
715 if (!aEditorBase.IsHTMLEditor()) {
716 return aEditorBase.GetRoot();
718 if (aEditorBase.IsMailEditor()) {
719 // Shouldn't run spellcheck in a mail editor without focus
720 // (bug 1507543)
721 // XXX Why doesn't here use the document of the editor directly?
722 Element* const editingHost =
723 aEditorBase.AsHTMLEditor()->ComputeEditingHost();
724 if (!editingHost) {
725 return nullptr;
727 // Try to get topmost document's document element for embedded mail
728 // editor (bug 967494)
729 Document* parentDoc =
730 editingHost->OwnerDoc()->GetInProcessParentDocument();
731 if (!parentDoc) {
732 return editingHost;
734 return parentDoc->GetDocumentElement();
736 return aEditorBase.AsHTMLEditor()->GetFocusedElement();
737 }(*mEditor);
739 if (!rootEditableElement) {
740 return NS_ERROR_FAILURE;
743 RefPtr<DictionaryFetcher> fetcher =
744 new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
745 fetcher->mRootContentLang = rootEditableElement->GetLang();
746 RefPtr<Document> doc = rootEditableElement->GetComposedDoc();
747 NS_ENSURE_STATE(doc);
748 fetcher->mRootDocContentLang = doc->GetContentLanguage();
750 rv = fetcher->Fetch(mEditor);
751 NS_ENSURE_SUCCESS(rv, rv);
753 return NS_OK;
756 // Helper function that iterates over the list of dictionaries and sets the one
757 // that matches based on a given comparison type.
758 bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
759 const nsTArray<nsCString>& aDictList,
760 enum dictCompare aCompareType,
761 nsTArray<nsCString>& aOutList) {
762 for (const auto& dictStr : aDictList) {
763 bool equals = false;
764 switch (aCompareType) {
765 case DICT_NORMAL_COMPARE:
766 equals = aDictName.Equals(dictStr);
767 break;
768 case DICT_COMPARE_CASE_INSENSITIVE:
769 equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
770 break;
771 case DICT_COMPARE_DASHMATCH:
772 equals = nsStyleUtil::DashMatchCompare(
773 NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
774 nsCaseInsensitiveStringComparator);
775 break;
777 if (equals) {
778 // Avoid adding duplicates to aOutList.
779 if (aOutList.IndexOf(dictStr) == nsTArray<nsCString>::NoIndex) {
780 aOutList.AppendElement(dictStr);
782 #ifdef DEBUG_DICT
783 printf("***** Trying |%s|.\n", dictStr.get());
784 #endif
785 // We always break here. We tried to set the dictionary to an existing
786 // dictionary from the list. This must work, if it doesn't, there is
787 // no point trying another one.
788 return true;
791 return false;
794 nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
795 MOZ_ASSERT(aFetcher);
796 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
798 BeginUpdateDictionary();
800 if (aFetcher->mGroup < mDictionaryFetcherGroup) {
801 // SetCurrentDictionary was called after the fetch started. Don't overwrite
802 // that dictionary with the fetched one.
803 EndUpdateDictionary();
804 if (aFetcher->mCallback) {
805 aFetcher->mCallback->EditorSpellCheckDone();
807 return NS_OK;
811 * We try to derive the dictionary to use based on the following priorities:
812 * 1) Content preference, so the language the user set for the site before.
813 * (Introduced in bug 678842 and corrected in bug 717433.)
814 * 2) Language set by the website, or any other dictionary that partly
815 * matches that. (Introduced in bug 338427.)
816 * Eg. if the website is "en-GB", a user who only has "en-US" will get
817 * that. If the website is generic "en", the user will get one of the
818 * "en-*" installed. If application locale or system locale is "en-*",
819 * we get it. If others, it is (almost) random.
820 * However, we prefer what is stored in "spellchecker.dictionary",
821 * so if the user chose "en-AU" before, they will get "en-AU" on a plain
822 * "en" site. (Introduced in bug 682564.)
823 * If the site has multiple languages declared in its Content-Language
824 * header and there is no more specific lang tag in HTML, we try to
825 * enable a dictionary for every content language.
826 * 3) The value of "spellchecker.dictionary" which reflects a previous
827 * language choice of the user (on another site).
828 * (This was the original behaviour before the aforementioned bugs
829 * landed).
830 * 4) The user's locale.
831 * 5) Use the current dictionary that is currently set.
832 * 6) The content of the "LANG" environment variable (if set).
833 * 7) The first spell check dictionary installed.
836 // Get the language from the element or its closest parent according to:
837 // https://html.spec.whatwg.org/#attr-lang
838 // This is used in SetCurrentDictionaries.
839 nsCString contentLangs;
840 // Reset mPreferredLangs so we only get the current state.
841 mPreferredLangs.Clear();
842 if (aFetcher->mRootContentLang) {
843 aFetcher->mRootContentLang->ToUTF8String(contentLangs);
845 #ifdef DEBUG_DICT
846 printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get());
847 #endif
848 if (!contentLangs.IsEmpty()) {
849 mPreferredLangs.AppendElement(contentLangs);
850 } else {
851 // If no luck, try the "Content-Language" header.
852 if (aFetcher->mRootDocContentLang) {
853 aFetcher->mRootDocContentLang->ToUTF8String(contentLangs);
855 #ifdef DEBUG_DICT
856 printf("***** mPreferredLangs (content-language) |%s|\n",
857 contentLangs.get());
858 #endif
859 StringToDictionaries(contentLangs, mPreferredLangs);
862 // We obtain a list of available dictionaries.
863 AutoTArray<nsCString, 8> dictList;
864 nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
865 if (NS_WARN_IF(NS_FAILED(rv))) {
866 EndUpdateDictionary();
867 if (aFetcher->mCallback) {
868 aFetcher->mCallback->EditorSpellCheckDone();
870 return rv;
873 // Priority 1:
874 // If we successfully fetched a dictionary from content prefs, do not go
875 // further. Use this exact dictionary.
876 // Don't use content preferences for editor with eEditorMailMask flag.
877 nsAutoCString dictName;
878 uint32_t flags;
879 mEditor->GetFlags(&flags);
880 if (!(flags & nsIEditor::eEditorMailMask)) {
881 if (!aFetcher->mDictionaries.IsEmpty()) {
882 RefPtr<EditorSpellCheck> self = this;
883 RefPtr<DictionaryFetcher> fetcher = aFetcher;
884 mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries)
885 ->Then(
886 GetMainThreadSerialEventTarget(), __func__,
887 [self, fetcher]() {
888 #ifdef DEBUG_DICT
889 printf("***** Assigned from content preferences |%s|\n",
890 DictionariesToString(fetcher->mDictionaries).Data());
891 #endif
892 // We take an early exit here, so let's not forget to clear
893 // the word list.
894 self->DeleteSuggestedWordList();
896 self->EndUpdateDictionary();
897 if (fetcher->mCallback) {
898 fetcher->mCallback->EditorSpellCheckDone();
901 [self, fetcher](nsresult aError) {
902 if (aError == NS_ERROR_ABORT) {
903 return;
905 // May be dictionary was uninstalled ?
906 // Clear the content preference and continue.
907 ClearCurrentDictionaries(self->mEditor);
909 // Priority 2 or later will handled by the following
910 self->SetFallbackDictionary(fetcher);
912 return NS_OK;
915 SetFallbackDictionary(aFetcher);
916 return NS_OK;
919 void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) {
920 DeleteSuggestedWordList();
921 EndUpdateDictionary();
922 if (aFetcher->mCallback) {
923 aFetcher->mCallback->EditorSpellCheckDone();
927 void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
928 MOZ_ASSERT(mUpdateDictionaryRunning);
930 AutoTArray<nsCString, 6> tryDictList;
932 // We obtain a list of available dictionaries.
933 AutoTArray<nsCString, 8> dictList;
934 nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
935 if (NS_WARN_IF(NS_FAILED(rv))) {
936 EndUpdateDictionary();
937 if (aFetcher->mCallback) {
938 aFetcher->mCallback->EditorSpellCheckDone();
940 return;
943 // Priority 2:
944 // After checking the content preferences, we use the languages of the element
945 // or document.
947 // Get the preference value.
948 nsAutoCString prefDictionariesAsString;
949 Preferences::GetLocalizedCString("spellchecker.dictionary",
950 prefDictionariesAsString);
951 nsTArray<nsCString> prefDictionaries;
952 StringToDictionaries(prefDictionariesAsString, prefDictionaries);
954 nsAutoCString appLocaleStr;
955 // We pick one dictionary for every language that the element or document
956 // indicates it contains.
957 for (const auto& dictName : mPreferredLangs) {
958 // RFC 5646 explicitly states that matches should be case-insensitive.
959 if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
960 tryDictList)) {
961 #ifdef DEBUG_DICT
962 printf("***** Trying from element/doc |%s| \n", dictName.get());
963 #endif
964 continue;
967 // Required dictionary was not available. Try to get a dictionary
968 // matching at least language part of dictName.
969 mozilla::intl::Locale loc;
970 if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() &&
971 loc.Canonicalize().isOk()) {
972 Span<const char> language = loc.Language().Span();
973 nsAutoCString langCode(language.data(), language.size());
975 // Try dictionary.spellchecker preference, if it starts with langCode,
976 // so we don't just get any random dictionary matching the language.
977 bool didAppend = false;
978 for (const auto& dictionary : prefDictionaries) {
979 if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary),
980 NS_ConvertUTF8toUTF16(langCode),
981 nsTDefaultStringComparator)) {
982 #ifdef DEBUG_DICT
983 printf(
984 "***** Trying preference value |%s| since it matches language "
985 "code\n",
986 dictionary.Data());
987 #endif
988 if (BuildDictionaryList(dictionary, dictList,
989 DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
990 didAppend = true;
991 break;
995 if (didAppend) {
996 continue;
999 // Use the application locale dictionary when the required language
1000 // equals applocation locale language.
1001 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
1002 if (!appLocaleStr.IsEmpty()) {
1003 mozilla::intl::Locale appLoc;
1004 auto result =
1005 mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc);
1006 if (result.isOk() && appLoc.Canonicalize().isOk() &&
1007 loc.Language().Span() == appLoc.Language().Span()) {
1008 if (BuildDictionaryList(appLocaleStr, dictList,
1009 DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
1010 continue;
1015 // Use the system locale dictionary when the required language equlas
1016 // system locale language.
1017 nsAutoCString sysLocaleStr;
1018 OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
1019 if (!sysLocaleStr.IsEmpty()) {
1020 mozilla::intl::Locale sysLoc;
1021 auto result =
1022 mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc);
1023 if (result.isOk() && sysLoc.Canonicalize().isOk() &&
1024 loc.Language().Span() == sysLoc.Language().Span()) {
1025 if (BuildDictionaryList(sysLocaleStr, dictList,
1026 DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
1027 continue;
1032 // Use any dictionary with the required language.
1033 #ifdef DEBUG_DICT
1034 printf("***** Trying to find match for language code |%s|\n",
1035 langCode.get());
1036 #endif
1037 BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
1038 tryDictList);
1042 RefPtr<EditorSpellCheck> self = this;
1043 RefPtr<DictionaryFetcher> fetcher = aFetcher;
1044 RefPtr<GenericPromise> promise;
1046 if (tryDictList.IsEmpty()) {
1047 // Proceed to priority 3 if the list of dictionaries is empty.
1048 promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
1049 } else {
1050 promise = mSpellChecker->SetCurrentDictionaries(tryDictList);
1053 // If an error was thrown while setting the dictionary, just
1054 // fail silently so that the spellchecker dialog is allowed to come
1055 // up. The user can manually reset the language to their choice on
1056 // the dialog if it is wrong.
1057 promise->Then(
1058 GetMainThreadSerialEventTarget(), __func__,
1059 [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
1060 [prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(),
1061 self, fetcher]() {
1062 // Build tryDictList with dictionaries for priorities 4 through 7.
1063 // We'll use this list if there is no user preference or trying
1064 // the user preference fails.
1065 AutoTArray<nsCString, 6> tryDictList;
1067 // Priority 4:
1068 // As next fallback, try the current locale.
1069 nsAutoCString appLocaleStr;
1070 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
1071 #ifdef DEBUG_DICT
1072 printf("***** Trying locale |%s|\n", appLocaleStr.get());
1073 #endif
1074 self->BuildDictionaryList(appLocaleStr, dictList,
1075 DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
1077 // Priority 5:
1078 // If we have a current dictionary and we don't have no item in try
1079 // list, don't try anything else.
1080 nsTArray<nsCString> currentDictionaries;
1081 self->GetCurrentDictionaries(currentDictionaries);
1082 if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) {
1083 #ifdef DEBUG_DICT
1084 printf("***** Retrieved current dict |%s|\n",
1085 DictionariesToString(currentDictionaries).Data());
1086 #endif
1087 self->EndUpdateDictionary();
1088 if (fetcher->mCallback) {
1089 fetcher->mCallback->EditorSpellCheckDone();
1091 return;
1094 // Priority 6:
1095 // Try to get current dictionary from environment variable LANG.
1096 // LANG = language[_territory][.charset]
1097 char* env_lang = getenv("LANG");
1098 if (env_lang) {
1099 nsAutoCString lang(env_lang);
1100 // Strip trailing charset, if there is any.
1101 int32_t dot_pos = lang.FindChar('.');
1102 if (dot_pos != -1) {
1103 lang = Substring(lang, 0, dot_pos);
1106 int32_t underScore = lang.FindChar('_');
1107 if (underScore != -1) {
1108 lang.Replace(underScore, 1, '-');
1109 #ifdef DEBUG_DICT
1110 printf("***** Trying LANG from environment |%s|\n", lang.get());
1111 #endif
1112 self->BuildDictionaryList(
1113 lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
1117 // Priority 7:
1118 // If it does not work, pick the first one.
1119 if (!dictList.IsEmpty()) {
1120 self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
1121 tryDictList);
1122 #ifdef DEBUG_DICT
1123 printf("***** Trying first of list |%s|\n", dictList[0].get());
1124 #endif
1127 // Priority 3:
1128 // If the document didn't supply a dictionary or the setting
1129 // failed, try the user preference next.
1130 if (!prefDictionaries.IsEmpty()) {
1131 self->mSpellChecker->SetCurrentDictionaries(prefDictionaries)
1132 ->Then(
1133 GetMainThreadSerialEventTarget(), __func__,
1134 [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
1135 // Priority 3 failed, we'll use the list we built of
1136 // priorities 4 to 7.
1137 [tryDictList = tryDictList.Clone(), self, fetcher]() {
1138 self->mSpellChecker
1139 ->SetCurrentDictionaryFromList(tryDictList)
1140 ->Then(GetMainThreadSerialEventTarget(), __func__,
1141 [self, fetcher]() {
1142 self->SetDictionarySucceeded(fetcher);
1145 } else {
1146 // We don't have a user preference, so we'll try the list we
1147 // built of priorities 4 to 7.
1148 self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
1149 ->Then(
1150 GetMainThreadSerialEventTarget(), __func__,
1151 [self, fetcher]() { self->SetDictionarySucceeded(fetcher); });
1156 } // namespace mozilla