Backed out 2 changesets (bug 1205983) for memory leaks on a CLOSED TREE
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 29 Sep 2015 13:58:35 +0200
changeset 300128 009bf768c97c8fb41b24be3de4280c429babe6be
parent 300127 04b6f94fbe8aa09125271dbf752a8bef1bc93793
child 300129 41c45fa8ce5e2fb3491429bb1826494f55202885
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1205983
milestone44.0a1
backs outf2c49c0ab84fbd74b6fb0ff53dee360e820272b5
a81630dba992c4ec559837f702d3e59d65bbf38f
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 2 changesets (bug 1205983) for memory leaks on a CLOSED TREE Backed out changeset f2c49c0ab84f (bug 1205983) Backed out changeset a81630dba992 (bug 1205983)
editor/composer/nsEditorSpellCheck.cpp
editor/composer/test/chrome.ini
editor/composer/test/test_bug1205983.html
editor/composer/test/test_bug697981.html
editor/libeditor/nsEditor.cpp
editor/libeditor/nsEditor.h
editor/libeditor/nsEditorEventListener.cpp
editor/nsIEditorSpellCheck.idl
editor/txtsvc/nsISpellChecker.h
extensions/spellcheck/hunspell/glue/mozHunspell.cpp
extensions/spellcheck/idl/mozISpellCheckingEngine.idl
extensions/spellcheck/src/mozInlineSpellChecker.cpp
extensions/spellcheck/src/mozSpellChecker.cpp
extensions/spellcheck/src/mozSpellChecker.h
extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
--- a/editor/composer/nsEditorSpellCheck.cpp
+++ b/editor/composer/nsEditorSpellCheck.cpp
@@ -632,16 +632,23 @@ nsEditorSpellCheck::SetCurrentDictionary
              NS_ConvertUTF16toUTF8(aDictionary).get());
 #endif
     }
   }
   return mSpellChecker->SetCurrentDictionary(aDictionary);
 }
 
 NS_IMETHODIMP
+nsEditorSpellCheck::CheckCurrentDictionary()
+{
+  mSpellChecker->CheckCurrentDictionary();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsEditorSpellCheck::UninitSpellChecker()
 {
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
 
   // Cleanup - kill the spell checker
   DeleteSuggestedWordList();
   mDictionaryList.Clear();
   mDictionaryIndex = 0;
--- a/editor/composer/test/chrome.ini
+++ b/editor/composer/test/chrome.ini
@@ -4,9 +4,8 @@ skip-if = buildapp == 'b2g' || os == 'an
 [test_async_UpdateCurrentDictionary.html]
 [test_bug338427.html]
 [test_bug434998.xul]
 [test_bug678842.html]
 [test_bug697981.html]
 [test_bug717433.html]
 [test_bug1204147.html]
 [test_bug1200533.html]
-[test_bug1205983.html]
deleted file mode 100644
--- a/editor/composer/test/test_bug1205983.html
+++ /dev/null
@@ -1,122 +0,0 @@
-<!DOCTYPE html>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
--->
-<head>
-  <title>Test for Bug 1205983</title>
-  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205983">Mozilla Bug 1205983</a>
-<p id="display"></p>
-</div>
-
-<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
-<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
-
-<pre id="test">
-<script class="testbody" type="text/javascript">
-
-function getMisspelledWords(editor) {
-  return editor.selectionController.getSelection(Components.interfaces.nsISelectionController.SELECTION_SPELLCHECK).toString();
-}
-
-var elem_de;
-var editor_de;
-var selcon_de;
-var de_DE;
-var hunspell;
-
-/** Test for Bug 1205983 **/
-SimpleTest.waitForExplicitFinish();
-SimpleTest.waitForFocus(function() {
-  Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
-
-  var dir = Components.classes["@mozilla.org/file/directory_service;1"]
-                      .getService(Components.interfaces.nsIProperties)
-                      .get("CurWorkD", Components.interfaces.nsIFile);
-  dir.append("tests");
-  dir.append("editor");
-  dir.append("composer");
-  dir.append("test");
-
-  hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
-                       .getService(Components.interfaces.mozISpellCheckingEngine);
-
-  // Install de-DE dictionary.
-  de_DE = dir.clone();
-  de_DE.append("de-DE");
-  is(de_DE.exists(), true, "true expected (de_DE directory should exist)");
-  hunspell.addDirectory(de_DE);
-
-  document.getElementById('de-DE').focus();
-});
-
-function deFocus() {
-  elem_de = document.getElementById('de-DE');
-
-  onSpellCheck(elem_de, function () {
-    var Ci = Components.interfaces;
-    var editingSession = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIWebNavigation)
-                               .QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIEditingSession);
-    editor_de = editingSession.getEditorForWindow(window);
-    selcon_de = editor_de.selectionController;
-    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-
-    // Check that we spelled in German, so there is only one misspelled word.
-    is(sel.toString(), "German", "one misspelled word expected: German");
-
-    // Now focus the textarea, which requires English spelling.
-    document.getElementById('en-US').focus();
-  });
-}
-
-function enFocus() {
-  var elem_en = document.getElementById('en-US');
-  var editor_en = elem_en.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor;
-  editor_en.setSpellcheckUserOverride(true);
-  var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
-
-  onSpellCheck(elem_en, function () {
-    var spellchecker = inlineSpellChecker.spellChecker;
-    try {
-      currentDictonary = spellchecker.GetCurrentDictionary();
-    } catch(e) {}
-
-    // Check that the English dictionary is loaded and that the spell check has worked.
-    is(currentDictonary, "en-US", "expected en-US");
-    is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
-
-    // So far all was boring. The important thing is whether the spell check result
-    // in the de-DE editor is still the same. After losing focus, no spell check
-    // updates should take place there.
-    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-    is(sel.toString(), "German", "one misspelled word expected: German");
-
-    // Remove the fake de_DE dictionary again.
-    hunspell.removeDirectory(de_DE);
-
-    // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
-    elem_de.onfocus = null;
-    elem_de.blur();
-    elem_de.focus();
-
-    // After removal, the de_DE editor should refresh the spelling with en-US.
-    onSpellCheck(elem_de, function () {
-      var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-      is(sel.toString(), "heute" + "ist" + "ein" + "guter",
-         "some misspelled words expected: heute ist ein guter");
-
-      SimpleTest.finish();
-    });
-  });
-}
-
-</script>
-</pre>
-</body>
-</html>
--- a/editor/composer/test/test_bug697981.html
+++ b/editor/composer/test/test_bug697981.html
@@ -93,21 +93,16 @@ function enFocus() {
     // So far all was boring. The important thing is whether the spell check result
     // in the de-DE editor is still the same. After losing focus, no spell check
     // updates should take place there.
     is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
 
     // Remove the fake de_DE dictionary again.
     hunspell.removeDirectory(de_DE);
 
-    // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
-    elem_de.onfocus = null;
-    elem_de.blur();
-    elem_de.focus();
-
     // After removal, the de_DE editor should refresh the spelling with en-US.
     onSpellCheck(elem_de, function () {
       spellchecker = inlineSpellChecker.spellChecker;
       try {
         currentDictonary = spellchecker.GetCurrentDictionary();
       } catch(e) {}
 
       // Check that the default English dictionary is loaded and that the spell check has worked.
--- a/editor/libeditor/nsEditor.cpp
+++ b/editor/libeditor/nsEditor.cpp
@@ -19,16 +19,17 @@
 #include "EditTxn.h"                    // for EditTxn
 #include "IMETextTxn.h"                 // for IMETextTxn
 #include "InsertNodeTxn.h"              // for InsertNodeTxn
 #include "InsertTextTxn.h"              // for InsertTextTxn
 #include "JoinNodeTxn.h"                // for JoinNodeTxn
 #include "PlaceholderTxn.h"             // for PlaceholderTxn
 #include "SplitNodeTxn.h"               // for SplitNodeTxn
 #include "mozFlushType.h"               // for mozFlushType::Flush_Frames
+#include "mozISpellCheckingEngine.h"
 #include "mozInlineSpellChecker.h"      // for mozInlineSpellChecker
 #include "mozilla/CheckedInt.h"         // for CheckedInt
 #include "mozilla/IMEStateManager.h"    // for IMEStateManager
 #include "mozilla/Preferences.h"        // for Preferences
 #include "mozilla/dom/Selection.h"      // for Selection, etc
 #include "mozilla/Services.h"           // for GetObserverService
 #include "mozilla/TextComposition.h"    // for TextComposition
 #include "mozilla/TextEvents.h"
@@ -73,16 +74,17 @@
 #include "nsIEditActionListener.h"      // for nsIEditActionListener
 #include "nsIEditorObserver.h"          // for nsIEditorObserver
 #include "nsIEditorSpellCheck.h"        // for nsIEditorSpellCheck
 #include "nsIFrame.h"                   // for nsIFrame
 #include "nsIHTMLDocument.h"            // for nsIHTMLDocument
 #include "nsIInlineSpellChecker.h"      // for nsIInlineSpellChecker, etc
 #include "nsNameSpaceManager.h"        // for kNameSpaceID_None, etc
 #include "nsINode.h"                    // for nsINode, etc
+#include "nsIObserverService.h"         // for nsIObserverService
 #include "nsIPlaintextEditor.h"         // for nsIPlaintextEditor, etc
 #include "nsIPresShell.h"               // for nsIPresShell
 #include "nsISelectionController.h"     // for nsISelectionController, etc
 #include "nsISelectionDisplay.h"        // for nsISelectionDisplay, etc
 #include "nsISupportsBase.h"            // for nsISupports
 #include "nsISupportsUtils.h"           // for NS_ADDREF, NS_IF_ADDREF
 #include "nsITransaction.h"             // for nsITransaction
 #include "nsITransactionManager.h"
@@ -141,16 +143,17 @@ nsEditor::nsEditor()
 ,  mDocDirtyState(-1)
 ,  mSpellcheckCheckboxState(eTriUnset)
 ,  mShouldTxnSetSelection(true)
 ,  mDidPreDestroy(false)
 ,  mDidPostCreate(false)
 ,  mDispatchInputEvent(true)
 ,  mIsInEditAction(false)
 ,  mHidingCaret(false)
+,  mObservingDictionaryUpdates(false)
 {
 }
 
 nsEditor::~nsEditor()
 {
   NS_ASSERTION(!mDocWeak || mDidPreDestroy, "Why PreDestroy hasn't been called?");
 
   if (mComposition) {
@@ -196,16 +199,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsEditor)
  NS_INTERFACE_MAP_ENTRY(nsIPhonetic)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY(nsIEditorIMESupport)
  NS_INTERFACE_MAP_ENTRY(nsIEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsEditor)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsEditor)
 
 
 NS_IMETHODIMP
@@ -295,16 +299,23 @@ nsEditor::PostCreate()
 
     // nuke the modification count, so the doc appears unmodified
     // do this before we notify listeners
     ResetModificationCount();
 
     // update the UI with our state
     NotifyDocumentListeners(eDocumentCreated);
     NotifyDocumentListeners(eDocumentStateChanged);
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->AddObserver(this,
+                       SPELLCHECK_DICTIONARY_REMOVE_NOTIFICATION,
+                       false);
+    }
   }
 
   // update nsTextStateManager and caret if we have focus
   nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
   if (focusedContent) {
     nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(focusedContent);
     if (target) {
       InitializeSelection(target);
@@ -432,16 +443,24 @@ nsEditor::GetDesiredSpellCheckState()
 NS_IMETHODIMP
 nsEditor::PreDestroy(bool aDestroyingFrames)
 {
   if (mDidPreDestroy)
     return NS_OK;
 
   IMEStateManager::OnEditorDestroying(this);
 
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this,
+                        SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION);
+    obs->RemoveObserver(this,
+                        SPELLCHECK_DICTIONARY_REMOVE_NOTIFICATION);
+  }
+
   // Let spellchecker clean up its observers etc. It is important not to
   // actually free the spellchecker here, since the spellchecker could have
   // caused flush notifications, which could have gotten here if a textbox
   // is being removed. Setting the spellchecker to nullptr could free the
   // object that is still in use! It will be freed when the editor is
   // destroyed.
   if (mInlineSpellChecker)
     mInlineSpellChecker->Cleanup(aDestroyingFrames);
@@ -1290,16 +1309,45 @@ NS_IMETHODIMP nsEditor::GetInlineSpellCh
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP nsEditor::Observe(nsISupports* aSubj, const char *aTopic,
+                                const char16_t *aData)
+{
+  NS_ASSERTION(!strcmp(aTopic,
+                       SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION) ||
+               !strcmp(aTopic,
+                       SPELLCHECK_DICTIONARY_REMOVE_NOTIFICATION),
+               "Unexpected observer topic");
+
+  // When mozInlineSpellChecker::CanEnableInlineSpellChecking changes
+  SyncRealTimeSpell();
+
+  // When nsIEditorSpellCheck::GetCurrentDictionary changes
+  if (mInlineSpellChecker) {
+    // Do the right thing in the spellchecker, if the dictionary is no longer
+    // available. This will not set a new dictionary.
+    nsCOMPtr<nsIEditorSpellCheck> editorSpellCheck;
+    mInlineSpellChecker->GetSpellChecker(getter_AddRefs(editorSpellCheck));
+    if (editorSpellCheck) {
+      editorSpellCheck->CheckCurrentDictionary();
+    }
+
+    // update the inline spell checker to reflect the new current dictionary
+    mInlineSpellChecker->SpellCheckRange(nullptr); // causes recheck
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP nsEditor::SyncRealTimeSpell()
 {
   bool enable = GetDesiredSpellCheckState();
 
   // Initializes mInlineSpellChecker
   nsCOMPtr<nsIInlineSpellChecker> spellChecker;
   GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
 
@@ -5153,16 +5201,39 @@ void
 nsEditor::OnFocus(nsIDOMEventTarget* aFocusEventTarget)
 {
   InitializeSelection(aFocusEventTarget);
   if (mInlineSpellChecker) {
     mInlineSpellChecker->UpdateCurrentDictionary();
   }
 }
 
+void
+nsEditor::StartWatchingDictionaryChanges()
+{
+  if (!mObservingDictionaryUpdates) {
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->AddObserver(this, SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION, false);
+    }
+    mObservingDictionaryUpdates = true;
+  }
+}
+
+void
+nsEditor::StopWatchingDictionaryChanges()
+{
+  // Removing an observer that wasn't added doesn't cause any harm.
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this, SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION);
+  }
+  mObservingDictionaryUpdates = false;
+}
+
 NS_IMETHODIMP
 nsEditor::GetSuppressDispatchingInputEvent(bool *aSuppressed)
 {
   NS_ENSURE_ARG_POINTER(aSuppressed);
   *aSuppressed = !mDispatchInputEvent;
   return NS_OK;
 }
 
--- a/editor/libeditor/nsEditor.h
+++ b/editor/libeditor/nsEditor.h
@@ -135,16 +135,17 @@ inline bool operator!(const EditAction& 
 /** implementation of an editor object.  it will be the controller/focal point
  *  for the main editor services. i.e. the GUIManager, publishing, transaction
  *  manager, event interfaces. the idea for the event interfaces is to have them
  *  delegate the actual commands to the editor independent of the XPFE implementation.
  */
 class nsEditor : public nsIEditor,
                  public nsIEditorIMESupport,
                  public nsSupportsWeakReference,
+                 public nsIObserver,
                  public nsIPhonetic
 {
 public:
 
   enum IterDirection
   {
     kIterForward,
     kIterBackward
@@ -182,16 +183,19 @@ public:
   void NotifyEditorObservers(NotificationForEditorObservers aNotification);
 
   /* ------------ nsIEditor methods -------------- */
   NS_DECL_NSIEDITOR
 
   /* ------------ nsIEditorIMESupport methods -------------- */
   NS_DECL_NSIEDITORIMESUPPORT
 
+  /* ------------ nsIObserver methods -------------- */
+  NS_DECL_NSIOBSERVER
+
   // nsIPhonetic
   NS_DECL_NSIPHONETIC
 
 public:
 
   virtual bool IsModifiableNode(nsINode *aNode);
 
   virtual nsresult InsertTextImpl(const nsAString& aStringToInsert,
@@ -240,16 +244,19 @@ public:
 
   // IME event handlers
   virtual nsresult BeginIMEComposition(mozilla::WidgetCompositionEvent* aEvent);
   virtual nsresult UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent) = 0;
   void EndIMEComposition();
 
   void SwitchTextDirectionTo(uint32_t aDirection);
 
+  void StartWatchingDictionaryChanges();
+  void StopWatchingDictionaryChanges();
+
 protected:
   nsresult DetermineCurrentDirection();
   void FireInputEvent();
 
   /** Create a transaction for setting aAttribute to aValue on aElement.  Never
     * returns null.
     */
   already_AddRefed<mozilla::dom::ChangeAttributeTxn>
@@ -882,16 +889,17 @@ protected:
   uint8_t           mSpellcheckCheckboxState; // a Tristate value
 
   bool mShouldTxnSetSelection;  // turn off for conservative selection adjustment by txns
   bool mDidPreDestroy;    // whether PreDestroy has been called
   bool mDidPostCreate;    // whether PostCreate has been called
   bool mDispatchInputEvent;
   bool mIsInEditAction;   // true while the instance is handling an edit action
   bool mHidingCaret;      // whether caret is hidden forcibly.
+  bool mObservingDictionaryUpdates;  // whether the editor is observing dictionary changes.
 
   friend bool NSCanUnload(nsISupports* serviceMgr);
   friend class nsAutoTxnsConserveSelection;
   friend class nsAutoSelectionReset;
   friend class nsAutoRules;
   friend class nsRangeUpdater;
 };
 
--- a/editor/libeditor/nsEditorEventListener.cpp
+++ b/editor/libeditor/nsEditorEventListener.cpp
@@ -1109,32 +1109,36 @@ nsEditorEventListener::Focus(nsIDOMEvent
       nsCOMPtr<nsIDOMElement> element;
       fm->GetFocusedElement(getter_AddRefs(element));
       if (!SameCOMIdentity(element, target)) {
         return NS_OK;
       }
     }
   }
 
+  mEditor->StartWatchingDictionaryChanges();
+
   mEditor->OnFocus(target);
 
   nsCOMPtr<nsIPresShell> ps = GetPresShell();
   NS_ENSURE_TRUE(ps, NS_OK);
   nsCOMPtr<nsIContent> focusedContent = mEditor->GetFocusedContentForIME();
   IMEStateManager::OnFocusInEditor(ps->GetPresContext(), focusedContent,
                                    mEditor);
 
   return NS_OK;
 }
 
 nsresult
 nsEditorEventListener::Blur(nsIDOMEvent* aEvent)
 {
   NS_ENSURE_TRUE(aEvent, NS_OK);
 
+  mEditor->StopWatchingDictionaryChanges();
+
   // check if something else is focused. If another element is focused, then
   // we should not change the selection.
   nsIFocusManager* fm = nsFocusManager::GetFocusManager();
   NS_ENSURE_TRUE(fm, NS_OK);
 
   nsCOMPtr<nsIDOMElement> element;
   fm->GetFocusedElement(getter_AddRefs(element));
   if (!element) {
--- a/editor/nsIEditorSpellCheck.idl
+++ b/editor/nsIEditorSpellCheck.idl
@@ -4,20 +4,26 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIEditor;
 interface nsITextServicesFilter;
 interface nsIEditorSpellCheckCallback;
 
-[scriptable, uuid(a171c25f-e4a8-4d08-adef-b797e6377bdc)]
+[scriptable, uuid(dd32ef3b-a7d8-43d1-9617-5f2dddbe29eb)]
 interface nsIEditorSpellCheck : nsISupports
 {
 
+  /**
+   * Call this on any change in installed dictionaries to ensure that the spell
+   * checker is not using a current dictionary which is no longer available.
+   */
+  void checkCurrentDictionary();
+
  /**
    * Returns true if we can enable spellchecking. If there are no available
    * dictionaries, this will return false.
    */
   boolean       canSpellCheck();
 
   /**
    * Turns on the spell checker for the given editor. enableSelectionChecking
--- a/editor/txtsvc/nsISpellChecker.h
+++ b/editor/txtsvc/nsISpellChecker.h
@@ -109,14 +109,20 @@ public:
 
   /**
    * Tells the spellchecker to use a specific dictionary.
    * @param aDictionary a string that is in the list returned
    * by GetDictionaryList() or an empty string. If aDictionary is
    * empty string, spellchecker will be disabled.
    */
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) = 0;
+
+  /**
+   * Call this on any change in installed dictionaries to ensure that the spell
+   * checker is not using a current dictionary which is no longer available.
+   */
+  NS_IMETHOD CheckCurrentDictionary() = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsISpellChecker, NS_ISPELLCHECKER_IID)
 
 #endif // nsISpellChecker_h__
 
--- a/extensions/spellcheck/hunspell/glue/mozHunspell.cpp
+++ b/extensions/spellcheck/hunspell/glue/mozHunspell.cpp
@@ -155,16 +155,22 @@ NS_IMETHODIMP mozHunspell::SetDictionary
     delete mHunspell;
     mHunspell = nullptr;
     mDictionary.Truncate();
     mAffixFileName.Truncate();
     mLanguage.Truncate();
     mDecoder = nullptr;
     mEncoder = nullptr;
 
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->NotifyObservers(nullptr,
+                           SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                           nullptr);
+    }
     return NS_OK;
   }
 
   nsIFile* affFile = mDictionaries.GetWeak(nsDependentString(aDictionary));
   if (!affFile)
     return NS_ERROR_FILE_NOT_FOUND;
 
   nsAutoCString dictFileName, affFileName;
@@ -215,16 +221,23 @@ NS_IMETHODIMP mozHunspell::SetDictionary
   if (pos == -1)
     pos = mDictionary.FindChar('_');
 
   if (pos == -1)
     mLanguage.Assign(mDictionary);
   else
     mLanguage = Substring(mDictionary, 0, pos);
 
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->NotifyObservers(nullptr,
+                         SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                         nullptr);
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP mozHunspell::GetLanguage(char16_t **aLanguage)
 {
   NS_ENSURE_ARG_POINTER(aLanguage);
 
   if (mDictionary.IsEmpty())
@@ -586,24 +599,16 @@ NS_IMETHODIMP mozHunspell::AddDirectory(
   LoadDictionaryList(true);
   return NS_OK;
 }
 
 NS_IMETHODIMP mozHunspell::RemoveDirectory(nsIFile *aDir)
 {
   mDynamicDirectories.RemoveObject(aDir);
   LoadDictionaryList(true);
-
-#ifdef MOZ_THUNDERBIRD
-  /*
-   * This notification is needed for Thunderbird. Thunderbird derives the dictionary
-   * from the document's "lang" attribute. If a dictionary is removed,
-   * we need to change the "lang" attribute.
-   */
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->NotifyObservers(nullptr,
                          SPELLCHECK_DICTIONARY_REMOVE_NOTIFICATION,
                          nullptr);
   }
-#endif
   return NS_OK;
 }
--- a/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
+++ b/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
@@ -97,11 +97,13 @@ interface mozISpellCheckingEngine : nsIS
    */
   void removeDirectory(in nsIFile dir);
 };
 
 %{C++
 #define DICTIONARY_SEARCH_DIRECTORY "DictD"
 #define DICTIONARY_SEARCH_DIRECTORY_LIST "DictDL"
 
+#define SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION \
+  "spellcheck-dictionary-update"
 #define SPELLCHECK_DICTIONARY_REMOVE_NOTIFICATION \
   "spellcheck-dictionary-remove"
 %}
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -2016,18 +2016,20 @@ nsresult mozInlineSpellChecker::CurrentD
   ChangeNumPendingSpellChecks(-1);
 
   nsAutoString currentDictionary;
   if (!mSpellCheck ||
       NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) {
     currentDictionary.Truncate();
   }
 
-  nsresult rv = SpellCheckRange(nullptr);
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (!mPreviousDictionary.Equals(currentDictionary)) {
+    nsresult rv = SpellCheckRange(nullptr);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 mozInlineSpellChecker::GetSpellCheckPending(bool* aPending)
 {
   *aPending = mNumPendingSpellChecks > 0;
--- a/extensions/spellcheck/src/mozSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozSpellChecker.cpp
@@ -423,16 +423,41 @@ mozSpellChecker::SetCurrentDictionary(co
   }
 
   mSpellCheckingEngine = nullptr;
 
   // We could not find any engine with the requested dictionary
   return NS_ERROR_NOT_AVAILABLE;
 }
 
+NS_IMETHODIMP 
+mozSpellChecker::CheckCurrentDictionary()
+{
+  // If the current dictionary has been uninstalled, we need to stop using it.
+  // This happens when there is a current engine, but that engine has no
+  // current dictionary.
+
+  if (!mSpellCheckingEngine) {
+    // We didn't have a current dictionary
+    return NS_OK;
+  }
+
+  nsXPIDLString dictname;
+  mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
+
+  if (!dictname.IsEmpty()) {
+    // We still have a current dictionary
+    return NS_OK;
+  }
+
+  // We had a current dictionary, but it has gone, so we cannot use it anymore.
+  mSpellCheckingEngine = nullptr;
+  return NS_OK;
+}
+
 nsresult
 mozSpellChecker::SetupDoc(int32_t *outBlockOffset)
 {
   nsresult  rv;
 
   nsITextServicesDocument::TSDBlockSelectionStatus blockStatus;
   int32_t selOffset;
   int32_t selLength;
--- a/extensions/spellcheck/src/mozSpellChecker.h
+++ b/extensions/spellcheck/src/mozSpellChecker.h
@@ -43,16 +43,17 @@ public:
 
   NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord) override;
   NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord) override;
   NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList) override;
 
   NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList) override;
   NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary) override;
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) override;
+  NS_IMETHOD CheckCurrentDictionary() override;
 
   void DeleteRemoteEngine() {
     mEngine = nullptr;
   }
 
 protected:
   virtual ~mozSpellChecker();
 
--- a/extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
+++ b/extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
@@ -77,35 +77,27 @@ function RunTest() {
     onSpellCheck(textbox, function () {
       // test that base dictionary is in use
       is(getMisspelledWords(editor), "Frühstück" + "qwertyu", "base misspellings");
       is(getCurrentDictionary(editor), "base-utf", "current dictionary");
 
       // select map dictionary
       setCurrentDictionary(editor, "maputf");
 
-      // Focus again, so the spelling gets updated.
-      textbox.blur();
-      textbox.focus();
-
       onSpellCheck(textbox, function () {
         // test that map dictionary is in use
-        is(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings (1)");
+        is(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings");
         is(getCurrentDictionary(editor), "maputf", "current dictionary");
 
         // uninstall map dictionary
         hunspell.removeDirectory(map);
 
-        // Focus again, so the spelling gets updated.
-        textbox.blur();
-        textbox.focus();
-
         onSpellCheck(textbox, function () {
           // test that map dictionary is not in use
-          isnot(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings (2)");
+          isnot(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings");
           isnot(getCurrentDictionary(editor), "maputf", "current dictionary");
 
           // test that base dictionary is available and map dictionary is unavailable
           var dicts = getDictionaryList(editor);
           isnot(dicts.indexOf("base-utf"), -1, "base is available");
           is(dicts.indexOf("maputf"), -1, "map is unavailable");
 
           // uninstall base dictionary