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 264991 009bf768c97c8fb41b24be3de4280c429babe6be
parent 264990 04b6f94fbe8aa09125271dbf752a8bef1bc93793
child 264992 41c45fa8ce5e2fb3491429bb1826494f55202885
push id15422
push userkwierso@gmail.com
push dateTue, 29 Sep 2015 23:15:56 +0000
treeherderfx-team@662c10eb329b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1205983
milestone44.0a1
backs outf2c49c0ab84fbd74b6fb0ff53dee360e820272b5
a81630dba992c4ec559837f702d3e59d65bbf38f
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