Bug 1698786: part 4) Add some logging to `EditorSpellChecker`. r=masayuki
[gecko.git] / editor / spellchecker / EditorSpellCheck.cpp
blob0e6ce3f783199f9b9311cd3cd33c743a16e054d5
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 "mozilla/Attributes.h" // for final
10 #include "mozilla/EditorBase.h" // for EditorBase
11 #include "mozilla/HTMLEditor.h" // for HTMLEditor
12 #include "mozilla/dom/Element.h" // for Element
13 #include "mozilla/dom/Selection.h"
14 #include "mozilla/dom/StaticRange.h"
15 #include "mozilla/intl/LocaleService.h" // for retrieving app locale
16 #include "mozilla/intl/MozLocale.h" // for mozilla::intl::Locale
17 #include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
18 #include "mozilla/Logging.h" // for mozilla::LazyLogModule
19 #include "mozilla/mozalloc.h" // for operator delete, etc
20 #include "mozilla/mozSpellChecker.h" // for mozSpellChecker
21 #include "mozilla/Preferences.h" // for Preferences
22 #include "mozilla/TextServicesDocument.h" // for TextServicesDocument
23 #include "nsAString.h" // for nsAString::IsEmpty, etc
24 #include "nsComponentManagerUtils.h" // for do_CreateInstance
25 #include "nsDebug.h" // for NS_ENSURE_TRUE, etc
26 #include "nsDependentSubstring.h" // for Substring
27 #include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
28 #include "nsIContent.h" // for nsIContent
29 #include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
30 #include "mozilla/dom/Document.h" // for Document
31 #include "nsIEditor.h" // for nsIEditor
32 #include "nsILoadContext.h"
33 #include "nsISupportsBase.h" // for nsISupports
34 #include "nsISupportsUtils.h" // for NS_ADDREF
35 #include "nsIURI.h" // for nsIURI
36 #include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
37 #include "nsVariant.h" // for nsIWritableVariant, etc
38 #include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
39 #include "nsMemory.h" // for nsMemory
40 #include "nsRange.h"
41 #include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
42 #include "nsServiceManagerUtils.h" // for do_GetService
43 #include "nsString.h" // for nsAutoString, nsString, etc
44 #include "nsStringFwd.h" // for nsAFlatString
45 #include "nsStyleUtil.h" // for nsStyleUtil
46 #include "nsXULAppAPI.h" // for XRE_GetProcessType
48 namespace mozilla {
50 using namespace dom;
51 using intl::LocaleService;
52 using intl::OSPreferences;
54 static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");
56 class UpdateDictionaryHolder {
57 private:
58 EditorSpellCheck* mSpellCheck;
60 public:
61 explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
62 if (mSpellCheck) {
63 mSpellCheck->BeginUpdateDictionary();
67 ~UpdateDictionaryHolder() {
68 if (mSpellCheck) {
69 mSpellCheck->EndUpdateDictionary();
74 #define CPS_PREF_NAME u"spellcheck.lang"_ns
76 /**
77 * Gets the URI of aEditor's document.
79 static nsIURI* GetDocumentURI(EditorBase* aEditor) {
80 MOZ_ASSERT(aEditor);
82 Document* doc = aEditor->AsEditorBase()->GetDocument();
83 if (NS_WARN_IF(!doc)) {
84 return nullptr;
87 return doc->GetDocumentURI();
90 static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
91 Document* doc = aEditor->AsEditorBase()->GetDocument();
92 if (NS_WARN_IF(!doc)) {
93 return nullptr;
96 return doc->GetLoadContext();
99 /**
100 * Fetches the dictionary stored in content prefs and maintains state during the
101 * fetch, which is asynchronous.
103 class DictionaryFetcher final : public nsIContentPrefCallback2 {
104 public:
105 NS_DECL_ISUPPORTS
107 DictionaryFetcher(EditorSpellCheck* aSpellCheck,
108 nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
109 : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
111 NS_IMETHOD Fetch(nsIEditor* aEditor);
113 NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
114 nsCOMPtr<nsIVariant> value;
115 nsresult rv = aPref->GetValue(getter_AddRefs(value));
116 NS_ENSURE_SUCCESS(rv, rv);
117 value->GetAsAString(mDictionary);
118 return NS_OK;
121 NS_IMETHOD HandleCompletion(uint16_t reason) override {
122 mSpellCheck->DictionaryFetched(this);
123 return NS_OK;
126 NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }
128 nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
129 uint32_t mGroup;
130 nsString mRootContentLang;
131 nsString mRootDocContentLang;
132 nsString mDictionary;
134 private:
135 ~DictionaryFetcher() {}
137 RefPtr<EditorSpellCheck> mSpellCheck;
140 NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
142 class ContentPrefInitializerRunnable final : public Runnable {
143 public:
144 ContentPrefInitializerRunnable(nsIEditor* aEditor,
145 nsIContentPrefCallback2* aCallback)
146 : Runnable("ContentPrefInitializerRunnable"),
147 mEditorBase(aEditor->AsEditorBase()),
148 mCallback(aCallback) {}
150 NS_IMETHOD Run() override {
151 if (mEditorBase->Destroyed()) {
152 mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
153 return NS_OK;
156 nsCOMPtr<nsIContentPrefService2> contentPrefService =
157 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
158 if (NS_WARN_IF(!contentPrefService)) {
159 mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
160 return NS_OK;
163 nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
164 if (NS_WARN_IF(!docUri)) {
165 mCallback->HandleError(NS_ERROR_FAILURE);
166 return NS_OK;
169 nsAutoCString docUriSpec;
170 nsresult rv = docUri->GetSpec(docUriSpec);
171 if (NS_WARN_IF(NS_FAILED(rv))) {
172 mCallback->HandleError(rv);
173 return NS_OK;
176 rv = contentPrefService->GetByDomainAndName(
177 NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
178 GetLoadContext(mEditorBase), mCallback);
179 if (NS_WARN_IF(NS_FAILED(rv))) {
180 mCallback->HandleError(rv);
181 return NS_OK;
183 return NS_OK;
186 private:
187 RefPtr<EditorBase> mEditorBase;
188 nsCOMPtr<nsIContentPrefCallback2> mCallback;
191 NS_IMETHODIMP
192 DictionaryFetcher::Fetch(nsIEditor* aEditor) {
193 NS_ENSURE_ARG_POINTER(aEditor);
195 nsCOMPtr<nsIRunnable> runnable =
196 new ContentPrefInitializerRunnable(aEditor, this);
197 NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
198 EventQueuePriority::Idle);
200 return NS_OK;
204 * Stores the current dictionary for aEditor's document URL.
206 static nsresult StoreCurrentDictionary(EditorBase* aEditorBase,
207 const nsACString& aDictionary) {
208 NS_ENSURE_ARG_POINTER(aEditorBase);
210 nsresult rv;
212 nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
213 if (NS_WARN_IF(!docUri)) {
214 return NS_ERROR_FAILURE;
217 nsAutoCString docUriSpec;
218 rv = docUri->GetSpec(docUriSpec);
219 NS_ENSURE_SUCCESS(rv, rv);
221 RefPtr<nsVariant> prefValue = new nsVariant();
222 prefValue->SetAsAString(NS_ConvertUTF8toUTF16(aDictionary));
224 nsCOMPtr<nsIContentPrefService2> contentPrefService =
225 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
226 NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
228 return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
229 CPS_PREF_NAME, prefValue,
230 GetLoadContext(aEditorBase), nullptr);
234 * Forgets the current dictionary stored for aEditor's document URL.
236 static nsresult ClearCurrentDictionary(EditorBase* aEditorBase) {
237 NS_ENSURE_ARG_POINTER(aEditorBase);
239 nsresult rv;
241 nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
242 if (NS_WARN_IF(!docUri)) {
243 return NS_ERROR_FAILURE;
246 nsAutoCString docUriSpec;
247 rv = docUri->GetSpec(docUriSpec);
248 NS_ENSURE_SUCCESS(rv, rv);
250 nsCOMPtr<nsIContentPrefService2> contentPrefService =
251 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
252 NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
254 return contentPrefService->RemoveByDomainAndName(
255 NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
256 GetLoadContext(aEditorBase), nullptr);
259 NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
260 NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
262 NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
263 NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
264 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
265 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
266 NS_INTERFACE_MAP_END
268 NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)
270 EditorSpellCheck::EditorSpellCheck()
271 : mTxtSrvFilterType(0),
272 mSuggestedWordIndex(0),
273 mDictionaryIndex(0),
274 mDictionaryFetcherGroup(0),
275 mUpdateDictionaryRunning(false) {}
277 EditorSpellCheck::~EditorSpellCheck() {
278 // Make sure we blow the spellchecker away, just in
279 // case it hasn't been destroyed already.
280 mSpellChecker = nullptr;
283 mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }
285 // The problem is that if the spell checker does not exist, we can not tell
286 // which dictionaries are installed. This function works around the problem,
287 // allowing callers to ask if we can spell check without actually doing so (and
288 // enabling or disabling UI as necessary). This just creates a spellcheck
289 // object if needed and asks it for the dictionary list.
290 NS_IMETHODIMP
291 EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
292 RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
293 if (!spellChecker) {
294 spellChecker = mozSpellChecker::Create();
295 MOZ_ASSERT(spellChecker);
297 nsTArray<nsCString> dictList;
298 nsresult rv = spellChecker->GetDictionaryList(&dictList);
299 if (NS_WARN_IF(NS_FAILED(rv))) {
300 return rv;
303 *aCanSpellCheck = !dictList.IsEmpty();
304 return NS_OK;
307 // Instances of this class can be used as either runnables or RAII helpers.
308 class CallbackCaller final : public Runnable {
309 public:
310 explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
311 : mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}
313 ~CallbackCaller() { Run(); }
315 NS_IMETHOD Run() override {
316 if (mCallback) {
317 mCallback->EditorSpellCheckDone();
318 mCallback = nullptr;
320 return NS_OK;
323 private:
324 nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
327 NS_IMETHODIMP
328 EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
329 bool aEnableSelectionChecking,
330 nsIEditorSpellCheckCallback* aCallback) {
331 NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
332 mEditor = aEditor->AsEditorBase();
334 RefPtr<Document> doc = mEditor->GetDocument();
335 if (NS_WARN_IF(!doc)) {
336 return NS_ERROR_FAILURE;
339 nsresult rv;
341 // We can spell check with any editor type
342 RefPtr<TextServicesDocument> textServicesDocument =
343 new TextServicesDocument();
344 textServicesDocument->SetFilterType(mTxtSrvFilterType);
346 // EditorBase::AddEditActionListener() needs to access mSpellChecker and
347 // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
348 // initialize them before calling TextServicesDocument::InitWithEditor()
349 // since it calls EditorBase::AddEditActionListener().
350 mSpellChecker = mozSpellChecker::Create();
351 MOZ_ASSERT(mSpellChecker);
352 rv = mSpellChecker->SetDocument(textServicesDocument, true);
353 if (NS_WARN_IF(NS_FAILED(rv))) {
354 return rv;
357 // Pass the editor to the text services document
358 rv = textServicesDocument->InitWithEditor(aEditor);
359 NS_ENSURE_SUCCESS(rv, rv);
361 if (aEnableSelectionChecking) {
362 // Find out if the section is collapsed or not.
363 // If it isn't, we want to spellcheck just the selection.
365 RefPtr<Selection> selection;
366 aEditor->GetSelection(getter_AddRefs(selection));
367 if (NS_WARN_IF(!selection)) {
368 return NS_ERROR_FAILURE;
371 if (selection->RangeCount()) {
372 RefPtr<const nsRange> range = selection->GetRangeAt(0);
373 NS_ENSURE_STATE(range);
375 if (!range->Collapsed()) {
376 // We don't want to touch the range in the selection,
377 // so create a new copy of it.
378 RefPtr<StaticRange> staticRange =
379 StaticRange::Create(range, IgnoreErrors());
380 if (NS_WARN_IF(!staticRange)) {
381 return NS_ERROR_FAILURE;
384 // Make sure the new range spans complete words.
385 rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
386 if (NS_WARN_IF(NS_FAILED(rv))) {
387 return rv;
390 // Now tell the text services that you only want
391 // to iterate over the text in this range.
392 rv = textServicesDocument->SetExtent(staticRange);
393 if (NS_WARN_IF(NS_FAILED(rv))) {
394 return rv;
399 // do not fail if UpdateCurrentDictionary fails because this method may
400 // succeed later.
401 rv = UpdateCurrentDictionary(aCallback);
402 if (NS_FAILED(rv) && aCallback) {
403 // However, if it does fail, we still need to call the callback since we
404 // discard the failure. Do it asynchronously so that the caller is always
405 // guaranteed async behavior.
406 RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
407 rv = doc->Dispatch(TaskCategory::Other, caller.forget());
408 NS_ENSURE_SUCCESS(rv, rv);
411 return NS_OK;
414 NS_IMETHODIMP
415 EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
416 MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
418 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
420 DeleteSuggestedWordList();
421 // Beware! This may flush notifications via synchronous
422 // ScrollSelectionIntoView.
423 RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
424 return spellChecker->NextMisspelledWord(aNextMisspelledWord,
425 mSuggestedWordList);
428 NS_IMETHODIMP
429 EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
430 // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
431 if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
432 aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
433 mSuggestedWordIndex++;
434 } else {
435 // A blank string signals that there are no more strings
436 aSuggestedWord.Truncate();
438 return NS_OK;
441 NS_IMETHODIMP
442 EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
443 bool* aIsMisspelled) {
444 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
446 DeleteSuggestedWordList();
447 return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
448 &mSuggestedWordList);
451 RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
452 const nsTArray<nsString>& aSuggestedWords) {
453 if (NS_WARN_IF(!mSpellChecker)) {
454 return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
455 __func__);
458 return mSpellChecker->CheckWords(aSuggestedWords);
461 NS_IMETHODIMP
462 EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
463 const nsAString& aReplaceWord,
464 bool aAllOccurrences) {
465 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
467 RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
468 return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
471 NS_IMETHODIMP
472 EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
473 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
475 return mSpellChecker->IgnoreAll(aWord);
478 NS_IMETHODIMP
479 EditorSpellCheck::GetPersonalDictionary() {
480 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
482 // We can spell check with any editor type
483 mDictionaryList.Clear();
484 mDictionaryIndex = 0;
485 return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
488 NS_IMETHODIMP
489 EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord) {
490 // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
491 if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
492 aDictionaryWord = mDictionaryList[mDictionaryIndex];
493 mDictionaryIndex++;
494 } else {
495 // A blank string signals that there are no more strings
496 aDictionaryWord.Truncate();
499 return NS_OK;
502 NS_IMETHODIMP
503 EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
504 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
506 return mSpellChecker->AddWordToPersonalDictionary(aWord);
509 NS_IMETHODIMP
510 EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
511 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
513 return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
516 NS_IMETHODIMP
517 EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
518 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
520 return mSpellChecker->GetDictionaryList(&aList);
523 NS_IMETHODIMP
524 EditorSpellCheck::GetCurrentDictionary(nsACString& aDictionary) {
525 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
527 return mSpellChecker->GetCurrentDictionary(aDictionary);
530 NS_IMETHODIMP
531 EditorSpellCheck::SetCurrentDictionary(const nsACString& aDictionary) {
532 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
534 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
536 // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
537 // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
538 // is on the stack. In other words: Only do this, if the user manually
539 // selected a dictionary to use.
540 if (!mUpdateDictionaryRunning) {
541 // Ignore pending dictionary fetchers by increasing this number.
542 mDictionaryFetcherGroup++;
544 uint32_t flags = 0;
545 mEditor->GetFlags(&flags);
546 if (!(flags & nsIEditor::eEditorMailMask)) {
547 if (!aDictionary.IsEmpty() &&
548 (mPreferredLang.IsEmpty() ||
549 !mPreferredLang.Equals(aDictionary,
550 nsCaseInsensitiveCStringComparator))) {
551 // When user sets dictionary manually, we store this value associated
552 // with editor url, if it doesn't match the document language exactly.
553 // For example on "en" sites, we need to store "en-GB", otherwise
554 // the language might jump back to en-US although the user explicitly
555 // chose otherwise.
556 StoreCurrentDictionary(mEditor, aDictionary);
557 #ifdef DEBUG_DICT
558 printf("***** Writing content preferences for |%s|\n",
559 aDictionary.get());
560 #endif
561 } else {
562 // If user sets a dictionary matching the language defined by
563 // document, we consider content pref has been canceled, and we clear
564 // it.
565 ClearCurrentDictionary(mEditor);
566 #ifdef DEBUG_DICT
567 printf("***** Clearing content preferences for |%s|\n",
568 aDictionary.get());
569 #endif
572 // Also store it in as a preference, so we can use it as a fallback.
573 // We don't want this for mail composer because it uses
574 // "spellchecker.dictionary" as a preference.
576 // XXX: Prefs can only be set in the parent process, so this condition is
577 // necessary to stop libpref from throwing errors. But this should
578 // probably be handled in a better way.
579 if (XRE_IsParentProcess()) {
580 Preferences::SetCString("spellchecker.dictionary", aDictionary);
581 #ifdef DEBUG_DICT
582 printf("***** Possibly storing spellchecker.dictionary |%s|\n",
583 aDictionary.get());
584 #endif
588 return mSpellChecker->SetCurrentDictionary(aDictionary);
591 NS_IMETHODIMP
592 EditorSpellCheck::UninitSpellChecker() {
593 NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
595 // Cleanup - kill the spell checker
596 DeleteSuggestedWordList();
597 mDictionaryList.Clear();
598 mDictionaryIndex = 0;
599 mDictionaryFetcherGroup++;
600 mSpellChecker = nullptr;
601 return NS_OK;
604 NS_IMETHODIMP
605 EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
606 mTxtSrvFilterType = aFilterType;
607 return NS_OK;
610 nsresult EditorSpellCheck::DeleteSuggestedWordList() {
611 mSuggestedWordList.Clear();
612 mSuggestedWordIndex = 0;
613 return NS_OK;
616 NS_IMETHODIMP
617 EditorSpellCheck::UpdateCurrentDictionary(
618 nsIEditorSpellCheckCallback* aCallback) {
619 if (NS_WARN_IF(!mSpellChecker)) {
620 return NS_ERROR_NOT_INITIALIZED;
623 nsresult rv;
625 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
626 uint32_t flags = 0;
627 mEditor->GetFlags(&flags);
629 // Get language with html5 algorithm
630 nsCOMPtr<nsIContent> rootContent;
631 HTMLEditor* htmlEditor = mEditor->AsHTMLEditor();
632 if (htmlEditor) {
633 if (flags & nsIEditor::eEditorMailMask) {
634 // Always determine the root content for a mail editor,
635 // even if not focused, to enable further processing below.
636 rootContent = htmlEditor->GetActiveEditingHost();
637 } else {
638 rootContent = htmlEditor->GetFocusedContent();
640 } else {
641 rootContent = mEditor->GetRoot();
644 if (!rootContent) {
645 return NS_ERROR_FAILURE;
648 // Try to get topmost document's document element for embedded mail editor.
649 if (flags & nsIEditor::eEditorMailMask) {
650 RefPtr<Document> ownerDoc = rootContent->OwnerDoc();
651 Document* parentDoc = ownerDoc->GetInProcessParentDocument();
652 if (parentDoc) {
653 rootContent = parentDoc->GetDocumentElement();
654 if (!rootContent) {
655 return NS_ERROR_FAILURE;
660 RefPtr<DictionaryFetcher> fetcher =
661 new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
662 rootContent->GetLang(fetcher->mRootContentLang);
663 RefPtr<Document> doc = rootContent->GetComposedDoc();
664 NS_ENSURE_STATE(doc);
665 doc->GetContentLanguage(fetcher->mRootDocContentLang);
667 rv = fetcher->Fetch(mEditor);
668 NS_ENSURE_SUCCESS(rv, rv);
670 return NS_OK;
673 // Helper function that iterates over the list of dictionaries and sets the one
674 // that matches based on a given comparison type.
675 void EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
676 const nsTArray<nsCString>& aDictList,
677 enum dictCompare aCompareType,
678 nsTArray<nsCString>& aOutList) {
679 for (const auto& dictStr : aDictList) {
680 bool equals = false;
681 switch (aCompareType) {
682 case DICT_NORMAL_COMPARE:
683 equals = aDictName.Equals(dictStr);
684 break;
685 case DICT_COMPARE_CASE_INSENSITIVE:
686 equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
687 break;
688 case DICT_COMPARE_DASHMATCH:
689 equals = nsStyleUtil::DashMatchCompare(
690 NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
691 nsCaseInsensitiveStringComparator);
692 break;
694 if (equals) {
695 aOutList.AppendElement(dictStr);
696 #ifdef DEBUG_DICT
697 if (NS_SUCCEEDED(rv)) {
698 printf("***** Trying |%s|.\n", dictStr.get());
700 #endif
701 // We always break here. We tried to set the dictionary to an existing
702 // dictionary from the list. This must work, if it doesn't, there is
703 // no point trying another one.
704 return;
709 nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
710 MOZ_ASSERT(aFetcher);
711 RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
713 BeginUpdateDictionary();
715 if (aFetcher->mGroup < mDictionaryFetcherGroup) {
716 // SetCurrentDictionary was called after the fetch started. Don't overwrite
717 // that dictionary with the fetched one.
718 EndUpdateDictionary();
719 if (aFetcher->mCallback) {
720 aFetcher->mCallback->EditorSpellCheckDone();
722 return NS_OK;
726 * We try to derive the dictionary to use based on the following priorities:
727 * 1) Content preference, so the language the user set for the site before.
728 * (Introduced in bug 678842 and corrected in bug 717433.)
729 * 2) Language set by the website, or any other dictionary that partly
730 * matches that. (Introduced in bug 338427.)
731 * Eg. if the website is "en-GB", a user who only has "en-US" will get
732 * that. If the website is generic "en", the user will get one of the
733 * "en-*" installed. If application locale or system locale is "en-*",
734 * we get it. If others, it is (almost) random.
735 * However, we prefer what is stored in "spellchecker.dictionary",
736 * so if the user chose "en-AU" before, they will get "en-AU" on a plain
737 * "en" site. (Introduced in bug 682564.)
738 * 3) The value of "spellchecker.dictionary" which reflects a previous
739 * language choice of the user (on another site).
740 * (This was the original behaviour before the aforementioned bugs
741 * landed).
742 * 4) The user's locale.
743 * 5) Use the current dictionary that is currently set.
744 * 6) The content of the "LANG" environment variable (if set).
745 * 7) The first spell check dictionary installed.
748 // Get the language from the element or its closest parent according to:
749 // https://html.spec.whatwg.org/#attr-lang
750 // This is used in SetCurrentDictionary.
751 CopyUTF16toUTF8(aFetcher->mRootContentLang, mPreferredLang);
752 #ifdef DEBUG_DICT
753 printf("***** mPreferredLang (element) |%s|\n", mPreferredLang.get());
754 #endif
756 // If no luck, try the "Content-Language" header.
757 if (mPreferredLang.IsEmpty()) {
758 CopyUTF16toUTF8(aFetcher->mRootDocContentLang, mPreferredLang);
759 #ifdef DEBUG_DICT
760 printf("***** mPreferredLang (content-language) |%s|\n",
761 mPreferredLang.get());
762 #endif
765 // We obtain a list of available dictionaries.
766 AutoTArray<nsCString, 8> dictList;
767 nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
768 if (NS_WARN_IF(NS_FAILED(rv))) {
769 EndUpdateDictionary();
770 if (aFetcher->mCallback) {
771 aFetcher->mCallback->EditorSpellCheckDone();
773 return rv;
776 // Priority 1:
777 // If we successfully fetched a dictionary from content prefs, do not go
778 // further. Use this exact dictionary.
779 // Don't use content preferences for editor with eEditorMailMask flag.
780 nsAutoCString dictName;
781 uint32_t flags;
782 mEditor->GetFlags(&flags);
783 if (!(flags & nsIEditor::eEditorMailMask)) {
784 CopyUTF16toUTF8(aFetcher->mDictionary, dictName);
785 if (!dictName.IsEmpty()) {
786 AutoTArray<nsCString, 1> tryDictList;
787 BuildDictionaryList(dictName, dictList, DICT_NORMAL_COMPARE, tryDictList);
789 RefPtr<EditorSpellCheck> self = this;
790 RefPtr<DictionaryFetcher> fetcher = aFetcher;
791 mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
792 ->Then(
793 GetMainThreadSerialEventTarget(), __func__,
794 [self, fetcher]() {
795 #ifdef DEBUG_DICT
796 printf("***** Assigned from content preferences |%s|\n",
797 dictName.get());
798 #endif
799 // We take an early exit here, so let's not forget to clear
800 // the word list.
801 self->DeleteSuggestedWordList();
803 self->EndUpdateDictionary();
804 if (fetcher->mCallback) {
805 fetcher->mCallback->EditorSpellCheckDone();
808 [self, fetcher](nsresult aError) {
809 if (aError == NS_ERROR_ABORT) {
810 return;
812 // May be dictionary was uninstalled ?
813 // Clear the content preference and continue.
814 ClearCurrentDictionary(self->mEditor);
816 // Priority 2 or later will handled by the following
817 self->SetFallbackDictionary(fetcher);
819 return NS_OK;
822 SetFallbackDictionary(aFetcher);
823 return NS_OK;
826 void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
827 MOZ_ASSERT(mUpdateDictionaryRunning);
829 AutoTArray<nsCString, 6> tryDictList;
831 // We obtain a list of available dictionaries.
832 AutoTArray<nsCString, 8> dictList;
833 nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
834 if (NS_WARN_IF(NS_FAILED(rv))) {
835 EndUpdateDictionary();
836 if (aFetcher->mCallback) {
837 aFetcher->mCallback->EditorSpellCheckDone();
839 return;
842 // Priority 2:
843 // After checking the content preferences, we use the language of the element
844 // or document.
845 nsAutoCString dictName(mPreferredLang);
846 #ifdef DEBUG_DICT
847 printf("***** Assigned from element/doc |%s|\n", dictName.get());
848 #endif
850 // Get the preference value.
851 nsAutoCString preferredDict;
852 Preferences::GetLocalizedCString("spellchecker.dictionary", preferredDict);
854 nsAutoCString appLocaleStr;
855 if (!dictName.IsEmpty()) {
856 // RFC 5646 explicitly states that matches should be case-insensitive.
857 BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
858 tryDictList);
860 #ifdef DEBUG_DICT
861 printf("***** Trying from element/doc |%s| \n", dictName.get());
862 #endif
864 // Required dictionary was not available. Try to get a dictionary
865 // matching at least language part of dictName.
866 mozilla::intl::Locale loc = mozilla::intl::Locale(dictName);
867 nsAutoCString langCode(loc.GetLanguage());
869 // Try dictionary.spellchecker preference, if it starts with langCode,
870 // so we don't just get any random dictionary matching the language.
871 if (!preferredDict.IsEmpty() &&
872 nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(preferredDict),
873 NS_ConvertUTF8toUTF16(langCode),
874 nsTDefaultStringComparator)) {
875 #ifdef DEBUG_DICT
876 printf(
877 "***** Trying preference value |%s| since it matches language code\n",
878 preferredDict.get());
879 #endif
880 BuildDictionaryList(preferredDict, dictList,
881 DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
884 if (tryDictList.IsEmpty()) {
885 // Use the application locale dictionary when the required language
886 // equlas applocation locale language.
887 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
888 if (!appLocaleStr.IsEmpty()) {
889 mozilla::intl::Locale appLoc = mozilla::intl::Locale(appLocaleStr);
890 if (langCode.Equals(appLoc.GetLanguage())) {
891 BuildDictionaryList(appLocaleStr, dictList,
892 DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
896 // Use the system locale dictionary when the required language equlas
897 // system locale language.
898 nsAutoCString sysLocaleStr;
899 OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
900 if (!sysLocaleStr.IsEmpty()) {
901 mozilla::intl::Locale sysLoc = mozilla::intl::Locale(sysLocaleStr);
902 if (langCode.Equals(sysLoc.GetLanguage())) {
903 BuildDictionaryList(sysLocaleStr, dictList,
904 DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
909 // Use any dictionary with the required language.
910 #ifdef DEBUG_DICT
911 printf("***** Trying to find match for language code |%s|\n",
912 langCode.get());
913 #endif
914 BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
915 tryDictList);
918 // Priority 3:
919 // If the document didn't supply a dictionary or the setting failed,
920 // try the user preference next.
921 if (!preferredDict.IsEmpty()) {
922 #ifdef DEBUG_DICT
923 printf("***** Trying preference value |%s|\n", preferredDict.get());
924 #endif
925 BuildDictionaryList(preferredDict, dictList, DICT_NORMAL_COMPARE,
926 tryDictList);
929 // Priority 4:
930 // As next fallback, try the current locale.
931 if (appLocaleStr.IsEmpty()) {
932 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
934 #ifdef DEBUG_DICT
935 printf("***** Trying locale |%s|\n", appLocaleStr.get());
936 #endif
937 BuildDictionaryList(appLocaleStr, dictList, DICT_COMPARE_CASE_INSENSITIVE,
938 tryDictList);
940 // Priority 5:
941 // If we have a current dictionary and we don't have no item in try list,
942 // don't try anything else.
943 nsAutoCString currentDictionary;
944 GetCurrentDictionary(currentDictionary);
945 if (!currentDictionary.IsEmpty() && tryDictList.IsEmpty()) {
946 #ifdef DEBUG_DICT
947 printf("***** Retrieved current dict |%s|\n", currentDictionary.get());
948 #endif
949 EndUpdateDictionary();
950 if (aFetcher->mCallback) {
951 aFetcher->mCallback->EditorSpellCheckDone();
953 return;
956 // Priority 6:
957 // Try to get current dictionary from environment variable LANG.
958 // LANG = language[_territory][.charset]
959 char* env_lang = getenv("LANG");
960 if (env_lang) {
961 nsAutoCString lang(env_lang);
962 // Strip trailing charset, if there is any.
963 int32_t dot_pos = lang.FindChar('.');
964 if (dot_pos != -1) {
965 lang = Substring(lang, 0, dot_pos);
968 int32_t underScore = lang.FindChar('_');
969 if (underScore != -1) {
970 lang.Replace(underScore, 1, '-');
971 #ifdef DEBUG_DICT
972 printf("***** Trying LANG from environment |%s|\n", lang.get());
973 #endif
974 BuildDictionaryList(lang, dictList, DICT_COMPARE_CASE_INSENSITIVE,
975 tryDictList);
979 // Priority 7:
980 // If it does not work, pick the first one.
981 if (!dictList.IsEmpty()) {
982 BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
983 tryDictList);
984 #ifdef DEBUG_DICT
985 printf("***** Trying first of list |%s|\n", dictList[0].get());
986 #endif
989 RefPtr<EditorSpellCheck> self = this;
990 RefPtr<DictionaryFetcher> fetcher = aFetcher;
991 mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
992 ->Then(GetMainThreadSerialEventTarget(), __func__, [self, fetcher]() {
993 // If an error was thrown while setting the dictionary, just
994 // fail silently so that the spellchecker dialog is allowed to come
995 // up. The user can manually reset the language to their choice on
996 // the dialog if it is wrong.
997 self->DeleteSuggestedWordList();
998 self->EndUpdateDictionary();
999 if (fetcher->mCallback) {
1000 fetcher->mCallback->EditorSpellCheckDone();
1005 } // namespace mozilla