Bug 591780 - Part 1: Backend support for adding and removing spell checker dictionaries at runtime; r=ehsan
authorJesper Kristensen <mail@jesperkristensen.dk>
Tue, 13 Sep 2011 14:58:54 -0400
changeset 76918 e3cfe5ae818ce4ab1e139a03809dc4fdffacceee
parent 76917 a1268291394f18266d1f19510d5e71fc5f8b78b9
child 76919 2705abe8d3f22d1516c3f2c5a79533f0cadabb52
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
reviewersehsan
bugs591780
milestone9.0a1
Bug 591780 - Part 1: Backend support for adding and removing spell checker dictionaries at runtime; r=ehsan
browser/base/content/nsContextMenu.js
editor/composer/src/nsEditorSpellCheck.cpp
editor/idl/nsIEditorSpellCheck.idl
editor/libeditor/base/Makefile.in
editor/libeditor/base/nsEditor.cpp
editor/libeditor/base/nsEditor.h
editor/txtsvc/public/nsISpellChecker.h
extensions/spellcheck/Makefile.in
extensions/spellcheck/hunspell/src/Makefile.in
extensions/spellcheck/hunspell/src/mozHunspell.cpp
extensions/spellcheck/hunspell/src/mozHunspell.h
extensions/spellcheck/idl/mozISpellCheckingEngine.idl
extensions/spellcheck/src/mozInlineSpellChecker.cpp
extensions/spellcheck/src/mozInlineSpellChecker.h
extensions/spellcheck/src/mozSpellChecker.cpp
extensions/spellcheck/src/mozSpellChecker.h
extensions/spellcheck/src/mozSpellCheckerFactory.cpp
extensions/spellcheck/tests/Makefile.in
extensions/spellcheck/tests/chrome/Makefile.in
extensions/spellcheck/tests/chrome/base/Makefile.in
extensions/spellcheck/tests/chrome/base/base_utf.aff
extensions/spellcheck/tests/chrome/base/base_utf.dic
extensions/spellcheck/tests/chrome/map/Makefile.in
extensions/spellcheck/tests/chrome/map/maputf.aff
extensions/spellcheck/tests/chrome/map/maputf.dic
extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -316,20 +316,18 @@ nsContextMenu.prototype = {
                   !this.onTextInput && top.gBidiUI);
   },
 
   initSpellingItems: function() {
     var canSpell = InlineSpellCheckerUI.canSpellCheck;
     var onMisspelling = InlineSpellCheckerUI.overMisspelling;
     this.showItem("spell-check-enabled", canSpell);
     this.showItem("spell-separator", canSpell || this.onEditableArea);
-    if (canSpell) {
-      document.getElementById("spell-check-enabled")
-              .setAttribute("checked", InlineSpellCheckerUI.enabled);
-    }
+    document.getElementById("spell-check-enabled")
+            .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
 
     this.showItem("spell-add-to-dictionary", onMisspelling);
 
     // suggestion list
     this.showItem("spell-suggestions-separator", onMisspelling);
     if (onMisspelling) {
       var suggestionsSeparator =
         document.getElementById("spell-add-to-dictionary");
--- a/editor/composer/src/nsEditorSpellCheck.cpp
+++ b/editor/composer/src/nsEditorSpellCheck.cpp
@@ -553,16 +553,41 @@ nsEditorSpellCheck::SetCurrentDictionary
 
     // Also store it in as a preference. It will be used as a default value
     // when everything else fails.
     Preferences::SetString("spellchecker.dictionary", aDictionary);
   }
   return mSpellChecker->SetCurrentDictionary(aDictionary);
 }
 
+NS_IMETHODIMP
+nsEditorSpellCheck::CheckCurrentDictionary()
+{
+  mSpellChecker->CheckCurrentDictionary();
+
+  // Check if our current dictionary is still available.
+  nsAutoString currentDictionary;
+  nsresult rv = GetCurrentDictionary(currentDictionary);
+  if (NS_SUCCEEDED(rv) && !currentDictionary.IsEmpty()) {
+    return NS_OK;
+  }
+
+  // If our preferred current dictionary has gone, pick another one.
+  nsTArray<nsString> dictList;
+  rv = mSpellChecker->GetDictionaryList(&dictList);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (dictList.Length() > 0) {
+    rv = SetCurrentDictionary(dictList[0]);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP    
 nsEditorSpellCheck::UninitSpellChecker()
 {
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
 
   // Cleanup - kill the spell checker
   DeleteSuggestedWordList();
   mDictionaryList.Clear();
@@ -653,18 +678,16 @@ nsEditorSpellCheck::UpdateCurrentDiction
     if (packageRegistry) {
       nsCAutoString utf8DictName;
       rv = packageRegistry->GetSelectedLocale(NS_LITERAL_CSTRING("global"),
                                               utf8DictName);
       AppendUTF8toUTF16(utf8DictName, dictName);
     }
   }
 
-  SetCurrentDictionary(EmptyString());
-
   if (NS_SUCCEEDED(rv) && !dictName.IsEmpty()) {
     rv = SetCurrentDictionary(dictName);
     if (NS_FAILED(rv)) {
       // required dictionary was not available. Try to get a dictionary
       // matching at least language part of dictName: 
 
       nsAutoString langCode;
       PRInt32 dashIdx = dictName.FindChar('-');
--- a/editor/idl/nsIEditorSpellCheck.idl
+++ b/editor/idl/nsIEditorSpellCheck.idl
@@ -36,20 +36,27 @@
  *
  * ***** END LICENSE BLOCK ***** */
  
 #include "nsISupports.idl"
 
 interface nsIEditor;
 interface nsITextServicesFilter;
 
-[scriptable, uuid(af84da62-588f-409f-847d-feecc991bd93)]
+[scriptable, uuid(334946c3-0e93-4aac-b662-e1b56f95d68b)]
 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.
+   * If the current dictionary is no longer available, then pick another one.
+   */
+  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/libeditor/base/Makefile.in
+++ b/editor/libeditor/base/Makefile.in
@@ -89,9 +89,10 @@ FORCE_STATIC_LIB = 1
 
 include $(topsrcdir)/config/rules.mk
 
 INCLUDES	+= \
 		-I$(topsrcdir)/editor/libeditor/text \
 		-I$(topsrcdir)/content/base/src \
 		-I$(topsrcdir)/content/events/src \
 		-I$(topsrcdir)/layout/style \
+		-I$(topsrcdir)/extensions/spellcheck/src \
 		$(NULL)
--- a/editor/libeditor/base/nsEditor.cpp
+++ b/editor/libeditor/base/nsEditor.cpp
@@ -19,16 +19,17 @@
  * Portions created by the Initial Developer are Copyright (C) 1998
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Pierre Phaneuf <pp@ludusdesign.com>
  *   Daniel Glazman <glazman@netscape.com>
  *   Masayuki Nakano <masayuki@d-toybox.com>
  *   Mats Palmgren <matspal@gmail.com>
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either of the GNU General Public License Version 2 or later (the "GPL"),
  * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -43,16 +44,21 @@
 #include "nsIDOMDocument.h"
 #include "nsIDOMHTMLElement.h"
 #include "nsIDOMNSHTMLElement.h"
 #include "nsIDOMNSEvent.h"
 #include "nsIMEStateManager.h"
 #include "nsFocusManager.h"
 #include "nsUnicharUtils.h"
 #include "nsReadableUtils.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozISpellCheckingEngine.h"
+#include "nsIEditorSpellCheck.h"
+#include "mozInlineSpellChecker.h"
 
 #include "nsIDOMText.h"
 #include "nsIDOMElement.h"
 #include "nsIDOMAttr.h"
 #include "nsIDOMNode.h"
 #include "nsIDOMDocumentFragment.h"
 #include "nsIDOMNamedNodeMap.h"
 #include "nsIDOMNodeList.h"
@@ -202,16 +208,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(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 +302,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_UPDATE_NOTIFICATION,
+                       PR_FALSE);
+    }
   }
 
   // update nsTextStateManager and caret if we have focus
   nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
   if (focusedContent) {
     nsCOMPtr<nsIPresShell> ps = GetPresShell();
     NS_ASSERTION(ps, "no pres shell even though we have focus");
     NS_ENSURE_TRUE(ps, NS_ERROR_UNEXPECTED);
@@ -407,16 +421,22 @@ nsEditor::GetDesiredSpellCheckState()
 }
 
 NS_IMETHODIMP
 nsEditor::PreDestroy(PRBool aDestroyingFrames)
 {
   if (mDidPreDestroy)
     return NS_OK;
 
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this,
+                        SPELLCHECK_DICTIONARY_UPDATE_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 NULL could free the
   // object that is still in use! It will be freed when the editor is
   // destroyed.
   if (mInlineSpellChecker)
     mInlineSpellChecker->Cleanup(aDestroyingFrames);
@@ -1278,16 +1298,23 @@ NS_IMETHODIMP nsEditor::GetInlineSpellCh
 
   if (mDidPreDestroy) {
     // Don't allow people to get or create the spell checker once the editor
     // is going away.
     *aInlineSpellChecker = nsnull;
     return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK;
   }
 
+  // We don't want to show the spell checking UI if there are no spell check dictionaries available.
+  PRBool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking();
+  if (!canSpell) {
+    *aInlineSpellChecker = nsnull;
+    return NS_ERROR_FAILURE;
+  }
+
   nsresult rv;
   if (!mInlineSpellChecker && autoCreate) {
     mInlineSpellChecker = do_CreateInstance(MOZ_INLINESPELLCHECKER_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (mInlineSpellChecker) {
     rv = mInlineSpellChecker->Init(this);
@@ -1296,27 +1323,59 @@ 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 PRUnichar *aData)
+{
+  NS_ASSERTION(!strcmp(aTopic,
+                       SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION),
+               "Unexpected observer topic");
+
+  // When mozInlineSpellChecker::CanEnableInlineSpellChecking changes
+  SyncRealTimeSpell();
+
+  // When nsIEditorSpellCheck::GetCurrentDictionary changes
+  if (mInlineSpellChecker) {
+    // if the current dictionary is no longer available, find another one
+    nsCOMPtr<nsIEditorSpellCheck> editorSpellCheck;
+    mInlineSpellChecker->GetSpellChecker(getter_AddRefs(editorSpellCheck));
+    if (editorSpellCheck) {
+      // Note: This might change the current dictionary, which may call
+      // this observer recursively.
+      editorSpellCheck->CheckCurrentDictionary();
+    }
+
+    // update the inline spell checker to reflect the new current dictionary
+    mInlineSpellChecker->SpellCheckRange(nsnull); // causes recheck
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP nsEditor::SyncRealTimeSpell()
 {
   NS_TIME_FUNCTION;
 
   PRBool enable = GetDesiredSpellCheckState();
 
+  // Initializes mInlineSpellChecker
   nsCOMPtr<nsIInlineSpellChecker> spellChecker;
   GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
 
-  if (spellChecker) {
-    spellChecker->SetEnableRealTimeSpell(enable);
+  if (mInlineSpellChecker) {
+    // We might have a mInlineSpellChecker even if there are no dictionaries
+    // available since we don't destroy the mInlineSpellChecker when the last
+    // dictionariy is removed, but in that case spellChecker is null
+    mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsEditor::SetSpellcheckUserOverride(PRBool enable)
 {
   mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse;
--- a/editor/libeditor/base/nsEditor.h
+++ b/editor/libeditor/base/nsEditor.h
@@ -61,16 +61,17 @@
 #include "nsIDOMElement.h"
 #include "nsSelectionState.h"
 #include "nsIEditorSpellCheck.h"
 #include "nsIInlineSpellChecker.h"
 #include "nsIDOMEventTarget.h"
 #include "nsStubMutationObserver.h"
 #include "nsIViewManager.h"
 #include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
 
 class nsIDOMCharacterData;
 class nsIDOMRange;
 class nsIPresShell;
 class ChangeAttributeTxn;
 class CreateElementTxn;
 class InsertElementTxn;
 class DeleteElementTxn;
@@ -95,16 +96,17 @@ class nsIDOMNSEvent;
 /** 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
@@ -148,16 +150,19 @@ public:
   already_AddRefed<nsIPresShell> GetPresShell();
   void NotifyEditorObservers();
 
   /* ------------ nsIEditor methods -------------- */
   NS_DECL_NSIEDITOR
   /* ------------ nsIEditorIMESupport methods -------------- */
   NS_DECL_NSIEDITORIMESUPPORT
   
+  /* ------------ nsIObserver methods -------------- */
+  NS_DECL_NSIOBSERVER
+
   // nsIPhonetic
   NS_DECL_NSIPHONETIC
 
 public:
 
   
   NS_IMETHOD InsertTextImpl(const nsAString& aStringToInsert, 
                                nsCOMPtr<nsIDOMNode> *aInOutNode, 
--- a/editor/txtsvc/public/nsISpellChecker.h
+++ b/editor/txtsvc/public/nsISpellChecker.h
@@ -39,19 +39,19 @@
 #define nsISpellChecker_h__
 
 #include "nsISupports.h"
 #include "nsTArray.h"
 
 #define NS_SPELLCHECKER_CONTRACTID "@mozilla.org/spellchecker;1"
 
 #define NS_ISPELLCHECKER_IID                    \
-{ /* E75AC48C-E948-452E-8DB3-30FEE29FE3D2 */    \
-0xe75ac48c, 0xe948, 0x452e, \
-  { 0x8d, 0xb3, 0x30, 0xfe, 0xe2, 0x9f, 0xe3, 0xd2 } }
+{ /* 27bff957-b486-40ae-9f5d-af0cdd211868 */    \
+0x27bff957, 0xb486, 0x40ae, \
+  { 0x9f, 0x5d, 0xaf, 0x0c, 0xdd, 0x21, 0x18, 0x68 } }
 
 class nsITextServicesDocument;
 class nsString;
 
 /**
  * A generic interface for a spelling checker.
  */
 class nsISpellChecker  : public nsISupports{
@@ -141,14 +141,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/Makefile.in
+++ b/extensions/spellcheck/Makefile.in
@@ -39,9 +39,13 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE		= spellchecker
 DIRS		= idl locales hunspell src
 
+ifdef ENABLE_TESTS
+DIRS		+= tests
+endif
+
 include $(topsrcdir)/config/rules.mk
--- a/extensions/spellcheck/hunspell/src/Makefile.in
+++ b/extensions/spellcheck/hunspell/src/Makefile.in
@@ -66,11 +66,13 @@ CPPSRCS         += affentry.cpp \
 
 # This variable is referenced in configure.in.  Make sure to change that file
 # too if you need to change this variable.
 DEFINES = -DHUNSPELL_STATIC
 endif
 
 include $(topsrcdir)/config/rules.mk
 
+INCLUDES        += -I$(topsrcdir)/extensions/spellcheck/src
+
 ifdef MOZ_NATIVE_HUNSPELL
 CXXFLAGS += $(MOZ_HUNSPELL_CFLAGS)
 endif
--- a/extensions/spellcheck/hunspell/src/mozHunspell.cpp
+++ b/extensions/spellcheck/hunspell/src/mozHunspell.cpp
@@ -36,16 +36,17 @@
  *                 Varga Daniel
  *                 Chris Halls
  *                 Rene Engelhard
  *                 Bram Moolenaar
  *                 Dafydd Jones
  *                 Harri Pitkanen
  *                 Andras Timar
  *                 Tor Lillqvist
+ *                 Jesper Kristensen (mail@jesperkristensen.dk)
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -65,16 +66,18 @@
 #include "nsIFile.h"
 #include "nsDirectoryServiceUtils.h"
 #include "nsDirectoryServiceDefs.h"
 #include "mozISpellI18NManager.h"
 #include "nsICharsetConverterManager.h"
 #include "nsUnicharUtilCIID.h"
 #include "nsUnicharUtils.h"
 #include "nsCRT.h"
+#include "mozInlineSpellChecker.h"
+#include "mozilla/Services.h"
 #include <stdlib.h>
 #include "nsIMemoryReporter.h"
 
 static NS_DEFINE_CID(kCharsetConverterManagerCID, NS_ICHARSETCONVERTERMANAGER_CID);
 static NS_DEFINE_CID(kUnicharUtilCID, NS_UNICHARUTIL_CID);
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozHunspell)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozHunspell)
@@ -117,18 +120,17 @@ NS_MEMORY_REPORTER_IMPLEMENT(Hunspell,
 nsresult
 mozHunspell::Init()
 {
   if (!mDictionaries.Init())
     return NS_ERROR_OUT_OF_MEMORY;
 
   LoadDictionaryList();
 
-  nsCOMPtr<nsIObserverService> obs =
-    do_GetService("@mozilla.org/observer-service;1");
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->AddObserver(this, "profile-do-change", PR_TRUE);
   }
 
   mHunspellReporter = new NS_MEMORY_REPORTER_NAME(Hunspell);
   NS_RegisterMemoryReporter(mHunspellReporter);
 
   return NS_OK;
@@ -142,60 +144,76 @@ mozHunspell::~mozHunspell()
   NS_UnregisterMemoryReporter(mHunspellReporter);
 }
 
 /* attribute wstring dictionary; */
 NS_IMETHODIMP mozHunspell::GetDictionary(PRUnichar **aDictionary)
 {
   NS_ENSURE_ARG_POINTER(aDictionary);
 
-  if (mDictionary.IsEmpty())
-    return NS_ERROR_NOT_INITIALIZED;
-
   *aDictionary = ToNewUnicode(mDictionary);
   return *aDictionary ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
 }
 
 /* set the Dictionary.
  * This also Loads the dictionary and initializes the converter using the dictionaries converter
  */
 NS_IMETHODIMP mozHunspell::SetDictionary(const PRUnichar *aDictionary)
 {
   NS_ENSURE_ARG_POINTER(aDictionary);
 
-  if (mDictionary.Equals(aDictionary))
+  if (nsDependentString(aDictionary).IsEmpty()) {
+    delete mHunspell;
+    mHunspell = nsnull;
+    mDictionary.AssignLiteral("");
+    mAffixFileName.AssignLiteral("");
+    mLanguage.AssignLiteral("");
+    mDecoder = nsnull;
+    mEncoder = nsnull;
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->NotifyObservers(nsnull,
+                           SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                           nsnull);
+    }
     return NS_OK;
+  }
 
   nsIFile* affFile = mDictionaries.GetWeak(nsDependentString(aDictionary));
   if (!affFile)
     return NS_ERROR_FILE_NOT_FOUND;
 
   nsCAutoString dictFileName, affFileName;
 
   // XXX This isn't really good. nsIFile->NativePath isn't safe for all
   // character sets on Windows.
   // A better way would be to QI to nsILocalFile, and get a filehandle
   // from there. Only problem is that hunspell wants a path
 
   nsresult rv = affFile->GetNativePath(affFileName);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (mAffixFileName.Equals(affFileName.get()))
+    return NS_OK;
+
   dictFileName = affFileName;
   PRInt32 dotPos = dictFileName.RFindChar('.');
   if (dotPos == -1)
     return NS_ERROR_FAILURE;
 
   dictFileName.SetLength(dotPos);
   dictFileName.AppendLiteral(".dic");
 
   // SetDictionary can be called multiple times, so we might have a
   // valid mHunspell instance which needs cleaned up.
   delete mHunspell;
 
   mDictionary = aDictionary;
+  mAffixFileName = affFileName;
 
   mHunspell = new Hunspell(affFileName.get(),
                          dictFileName.get());
   if (!mHunspell)
     return NS_ERROR_OUT_OF_MEMORY;
 
   nsCOMPtr<nsICharsetConverterManager> ccm =
     do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
@@ -217,16 +235,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(nsnull,
+                         SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                         nsnull);
+  }
+
   return NS_OK;
 }
 
 /* readonly attribute wstring language; */
 NS_IMETHODIMP mozHunspell::GetLanguage(PRUnichar **aLanguage)
 {
   NS_ENSURE_ARG_POINTER(aLanguage);
 
@@ -328,28 +353,37 @@ NS_IMETHODIMP mozHunspell::GetDictionary
   }
 
   *aDictionaries = ans.dics;
   *aCount = ans.count;
 
   return NS_OK;
 }
 
+static PLDHashOperator
+FindFirstString(const nsAString& aString, nsIFile* aFile, void* aClosure)
+{
+  nsAString *dic = (nsAString*) aClosure;
+  dic->Assign(aString);
+  return PL_DHASH_STOP;
+}
+
 void
 mozHunspell::LoadDictionaryList()
 {
   mDictionaries.Clear();
 
   nsresult rv;
 
   nsCOMPtr<nsIProperties> dirSvc =
     do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
   if (!dirSvc)
     return;
 
+  // find built in dictionaries
   nsCOMPtr<nsIFile> dictDir;
   rv = dirSvc->Get(DICTIONARY_SEARCH_DIRECTORY,
                    NS_GET_IID(nsIFile), getter_AddRefs(dictDir));
   if (NS_SUCCEEDED(rv)) {
     LoadDictionariesFromDir(dictDir);
   }
   else {
     // try to load gredir/dictionaries
@@ -367,31 +401,55 @@ mozHunspell::LoadDictionaryList()
                      NS_GET_IID(nsIFile), getter_AddRefs(appDir));
     PRBool equals;
     if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(appDir->Equals(greDir, &equals)) && !equals) {
       appDir->AppendNative(NS_LITERAL_CSTRING("dictionaries"));
       LoadDictionariesFromDir(appDir);
     }
   }
 
+  // find dictionaries from extensions requiring restart
   nsCOMPtr<nsISimpleEnumerator> dictDirs;
   rv = dirSvc->Get(DICTIONARY_SEARCH_DIRECTORY_LIST,
                    NS_GET_IID(nsISimpleEnumerator), getter_AddRefs(dictDirs));
   if (NS_FAILED(rv))
     return;
 
   PRBool hasMore;
   while (NS_SUCCEEDED(dictDirs->HasMoreElements(&hasMore)) && hasMore) {
     nsCOMPtr<nsISupports> elem;
     dictDirs->GetNext(getter_AddRefs(elem));
 
     dictDir = do_QueryInterface(elem);
     if (dictDir)
       LoadDictionariesFromDir(dictDir);
   }
+
+  // find dictionaries from restartless extensions
+  for (PRUint32 i = 0; i < mDynamicDirectories.Count(); i++) {
+    LoadDictionariesFromDir(mDynamicDirectories[i]);
+  }
+
+  // Now we have finished updating the list of dictionaries, update the current
+  // dictionary and any editors which may use it.
+  mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking();
+
+  // Check if the current dictionary is still available.
+  // If not, try to replace it with another dictionary of the same language.
+  if (!mDictionary.IsEmpty()) {
+    rv = SetDictionary(mDictionary.get());
+    if (NS_SUCCEEDED(rv))
+      return;
+  }
+
+  // If the current dictionary has gone, and we don't have a good replacement,
+  // set no current dictionary.
+  if (!mDictionary.IsEmpty()) {
+    SetDictionary(EmptyString().get());
+  }
 }
 
 NS_IMETHODIMP
 mozHunspell::LoadDictionariesFromDir(nsIFile* aDir)
 {
   nsresult rv;
 
   PRBool check = PR_FALSE;
@@ -537,8 +595,24 @@ mozHunspell::Observe(nsISupports* aSubj,
 {
   NS_ASSERTION(!strcmp(aTopic, "profile-do-change"),
                "Unexpected observer topic");
 
   LoadDictionaryList();
 
   return NS_OK;
 }
+
+/* void addDirectory(in nsIFile dir); */
+NS_IMETHODIMP mozHunspell::AddDirectory(nsIFile *aDir)
+{
+  mDynamicDirectories.AppendObject(aDir);
+  LoadDictionaryList();
+  return NS_OK;
+}
+
+/* void removeDirectory(in nsIFile dir); */
+NS_IMETHODIMP mozHunspell::RemoveDirectory(nsIFile *aDir)
+{
+  mDynamicDirectories.RemoveObject(aDir);
+  LoadDictionaryList();
+  return NS_OK;
+}
--- a/extensions/spellcheck/hunspell/src/mozHunspell.h
+++ b/extensions/spellcheck/hunspell/src/mozHunspell.h
@@ -36,16 +36,17 @@
  *                 Varga Daniel
  *                 Chris Halls
  *                 Rene Engelhard
  *                 Bram Moolenaar
  *                 Dafydd Jones
  *                 Harri Pitkanen
  *                 Andras Timar
  *                 Tor Lillqvist
+ *                 Jesper Kristensen (mail@jesperkristensen.dk)
  * 
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -59,16 +60,17 @@
 #ifndef mozHunspell_h__
 #define mozHunspell_h__
 
 #include <hunspell.hxx>
 #include "mozISpellCheckingEngine.h"
 #include "mozIPersonalDictionary.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
+#include "nsCOMArray.h"
 #include "nsIObserver.h"
 #include "nsIUnicodeEncoder.h"
 #include "nsIUnicodeDecoder.h"
 #include "nsInterfaceHashtable.h"
 #include "nsWeakReference.h"
 #include "nsCycleCollectionParticipant.h"
 
 #define MOZ_HUNSPELL_CONTRACTID "@mozilla.org/spellchecker/engine;1"
@@ -104,15 +106,19 @@ protected:
   nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary;
   nsCOMPtr<nsIUnicodeEncoder>      mEncoder; 
   nsCOMPtr<nsIUnicodeDecoder>      mDecoder; 
 
   // Hashtable matches dictionary name to .aff file
   nsInterfaceHashtable<nsStringHashKey, nsIFile> mDictionaries;
   nsString  mDictionary;
   nsString  mLanguage;
+  nsCString mAffixFileName;
+
+  // dynamic dirs used to search for dictionaries
+  nsCOMArray<nsIFile> mDynamicDirectories;
 
   Hunspell  *mHunspell;
 
   nsIMemoryReporter* mHunspellReporter;
 };
 
 #endif
--- a/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
+++ b/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
@@ -15,16 +15,17 @@
  * The Original Code is mozilla.org code.
  *
  * The Initial Developer of the Original Code is
  * David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -35,30 +36,41 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
 interface nsIFile;
 interface mozIPersonalDictionary;
 
-[scriptable, uuid(6eb307d6-3567-481a-971a-feb666b8ae72)]
+[scriptable, uuid(8ba643a4-7ddc-4662-b976-7ec123843f10)]
 
 /**
  * This interface represents a SpellChecker.
  */
 
 interface mozISpellCheckingEngine : nsISupports {
   /**
-   * The name of the current dictionary
+   * The name of the current dictionary. Is either a value from
+   * getDictionaryList or the empty string if no dictionary is selected.
+   * Setting this attribute to a value not in getDictionaryList will throw
+   * NS_ERROR_FILE_NOT_FOUND.
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
+   * If the dictionary is changed to no dictionary (the empty string), an
+   * observer is allowed to set another dictionary before it returns.
    */
   attribute wstring dictionary;
 
   /**
    * The language this spellchecker is using when checking
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   readonly attribute wstring language;
 
   /**
    * Does the engine provide its own personal dictionary?
    */
   readonly attribute boolean providesPersonalDictionary;
 
@@ -84,26 +96,45 @@ interface mozISpellCheckingEngine : nsIS
 
   /**
    * Get the list of dictionaries
    */
   void getDictionaryList([array, size_is(count)] out wstring dictionaries, out PRUint32 count);
 
   /**
    * check a word
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   boolean check(in wstring word);
 
   /**
    * get a list of suggestions for a misspelled word
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   void suggest(in wstring word,[array, size_is(count)] out wstring suggestions, out PRUint32 count);
 
   /**
    * Load dictionaries from the specified dir
    */
   void loadDictionariesFromDir(in nsIFile dir);
+
+  /**
+   * Add dictionaries from a directory to the spell checker
+   */
+  void addDirectory(in nsIFile dir);
+
+  /**
+   * Remove dictionaries from a directory from the spell checker
+   */
+  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"
 %}
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -595,19 +595,19 @@ nsresult mozInlineSpellChecker::Cleanup(
 //    This function can be called to see if it seems likely that we can enable
 //    spellchecking before actually creating the InlineSpellChecking objects.
 //
 //    The problem is that we can't get the dictionary list without actually
 //    creating a whole bunch of spellchecking objects. This function tries to
 //    do that and caches the result so we don't have to keep allocating those
 //    objects if there are no dictionaries or spellchecking.
 //
-//    This caching will prevent adding dictionaries at runtime if we start out
-//    with no dictionaries! Installing dictionaries as extensions will require
-//    a restart anyway, so it shouldn't be a problem.
+//    Whenever dictionaries are added or removed at runtime, this value must be
+//    updated before an observer notification is sent out about the change, to
+//    avoid editors getting a wrong cached result.
 
 PRBool // static
 mozInlineSpellChecker::CanEnableInlineSpellChecking()
 {
   nsresult rv;
   if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
     gCanEnableSpellChecking = SpellCheck_NotAvailable;
 
@@ -620,16 +620,22 @@ mozInlineSpellChecker::CanEnableInlineSp
     NS_ENSURE_SUCCESS(rv, PR_FALSE);
 
     if (canSpellCheck)
       gCanEnableSpellChecking = SpellCheck_Available;
   }
   return (gCanEnableSpellChecking == SpellCheck_Available);
 }
 
+void // static
+mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
+{
+  gCanEnableSpellChecking = SpellCheck_Uninitialized;
+}
+
 // mozInlineSpellChecker::RegisterEventListeners
 //
 //    The inline spell checker listens to mouse events and keyboard navigation+ //    events.
 
 nsresult
 mozInlineSpellChecker::RegisterEventListeners()
 {
   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
--- a/extensions/spellcheck/src/mozInlineSpellChecker.h
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.h
@@ -224,18 +224,20 @@ private:
 public:
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIEDITACTIONLISTENER
   NS_DECL_NSIINLINESPELLCHECKER
   NS_DECL_NSIDOMEVENTLISTENER
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozInlineSpellChecker, nsIDOMEventListener)
 
-  // returns true if it looks likely that we can enable real-time spell checking
+  // returns true if there are any spell checking dictionaries available
   static PRBool CanEnableInlineSpellChecking();
+  // update the cached value whenever the list of available dictionaries changes
+  static void UpdateCanEnableInlineSpellChecking();
 
   nsresult Blur(nsIDOMEvent* aEvent);
   nsresult MouseClick(nsIDOMEvent* aMouseEvent);
   nsresult KeyPress(nsIDOMEvent* aKeyEvent);
 
   mozInlineSpellChecker();
   virtual ~mozInlineSpellChecker();
 
--- a/extensions/spellcheck/src/mozSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozSpellChecker.cpp
@@ -13,16 +13,17 @@
  *
  * The Original Code is Mozilla Spellchecker Component.
  *
  * The Initial Developer of the Original Code is David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s): David Einstein Deinst@world.std.com
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -72,19 +73,16 @@ mozSpellChecker::~mozSpellChecker()
 }
 
 nsresult 
 mozSpellChecker::Init()
 {
   mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
   
   mSpellCheckingEngine = nsnull;
-  mCurrentEngineContractId = nsnull;
-  mDictionariesMap.Init();
-  InitSpellCheckDictionaryMap();
 
   return NS_OK;
 } 
 
 NS_IMETHODIMP 
 mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, PRBool aFromStartofDoc)
 {
   mTsDoc = aDoc;
@@ -302,103 +300,132 @@ mozSpellChecker::GetPersonalDictionary(n
   nsAutoString word;
   while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) {
     words->GetNext(word);
     aWordList->AppendElement(word);
   }
   return NS_OK;
 }
 
-struct AppendNewStruct
-{
-  nsTArray<nsString> *dictionaryList;
-  PRBool failed;
-};
-
-static PLDHashOperator
-AppendNewString(const nsAString& aString, nsCString*, void* aClosure)
-{
-  AppendNewStruct *ans = (AppendNewStruct*) aClosure;
-
-  if (!ans->dictionaryList->AppendElement(aString))
-  {
-    ans->failed = PR_TRUE;
-    return PL_DHASH_STOP;
-  }
-
-  return PL_DHASH_NEXT;
-}
-
 NS_IMETHODIMP 
 mozSpellChecker::GetDictionaryList(nsTArray<nsString> *aDictionaryList)
 {
-  AppendNewStruct ans = {aDictionaryList, PR_FALSE};
+  nsresult rv;
+
+  // For catching duplicates
+  nsClassHashtable<nsStringHashKey, nsCString> dictionaries;
+  dictionaries.Init();
+
+  nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+  rv = GetEngineList(&spellCheckingEngines);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (PRUint32 i = 0; i < spellCheckingEngines.Count(); i++) {
+    nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
 
-  mDictionariesMap.EnumerateRead(AppendNewString, &ans);
+    PRUint32 count = 0;
+    PRUnichar **words = NULL;
+    engine->GetDictionaryList(&words, &count);
+    for (PRUint32 k = 0; k < count; k++) {
+      nsAutoString dictName;
+
+      dictName.Assign(words[k]);
 
-  if (ans.failed)
-    return NS_ERROR_OUT_OF_MEMORY;
+      // Skip duplicate dictionaries. Only take the first one
+      // for each name.
+      if (dictionaries.Get(dictName, NULL))
+        continue;
+
+      dictionaries.Put(dictName, NULL);
+
+      if (!aDictionaryList->AppendElement(dictName)) {
+        NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+    }
+
+    NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary)
 {
-  nsXPIDLString dictname;
+  if (!mSpellCheckingEngine) {
+    aDictionary.AssignLiteral("");
+    return NS_OK;
+  }
 
-  if (!mSpellCheckingEngine)
-    return NS_ERROR_NOT_INITIALIZED;
-
+  nsXPIDLString dictname;
   mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
   aDictionary = dictname;
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary)
 {
-  nsresult rv;
-  nsCString *contractId;
+  mSpellCheckingEngine = nsnull;
 
   if (aDictionary.IsEmpty()) {
-    mCurrentEngineContractId = nsnull;
-    mSpellCheckingEngine = nsnull;
     return NS_OK;
   }
 
-  if (!mDictionariesMap.Get(aDictionary, &contractId)){
-    NS_WARNING("Dictionary not found");
-    return NS_ERROR_NOT_AVAILABLE;
-  }
+  nsresult rv;
+  nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+  rv = GetEngineList(&spellCheckingEngines);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  if (!mCurrentEngineContractId || !mCurrentEngineContractId->Equals(*contractId)){
-    mSpellCheckingEngine = do_GetService(contractId->get(), &rv);
-    if (NS_FAILED(rv))
-      return rv;
+  for (PRUint32 i = 0; i < spellCheckingEngines.Count(); i++) {
+    // We must set mSpellCheckingEngine before we call SetDictionary, since
+    // SetDictionary calls back to this spell checker to check if the
+    // dictionary was set
+    mSpellCheckingEngine = spellCheckingEngines[i];
 
-    mCurrentEngineContractId = contractId;
+    rv = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get());
+
+    if (NS_SUCCEEDED(rv)) {
+      nsCOMPtr<mozIPersonalDictionary> personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
+      mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get());
+
+      return NS_OK;
+    }
   }
 
-  nsresult res;
-  res = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get());
-  if(NS_FAILED(res)){
-    NS_WARNING("Dictionary load failed");
-    return res;
+  mSpellCheckingEngine = NULL;
+  
+  // 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;
   }
 
-  mSpellCheckingEngine->SetPersonalDictionary(mPersonalDictionary);
+  nsXPIDLString dictname;
+  mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
 
-  nsXPIDLString language;
-  
-  nsCOMPtr<mozISpellI18NManager> serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &res));
-  if(serv && NS_SUCCEEDED(res)){
-    res = serv->GetUtil(language.get(),getter_AddRefs(mConverter));
+  if (!dictname.IsEmpty()) {
+    // We still have a current dictionary
+    return NS_OK;
   }
-  return res;
+
+  // We had a current dictionary, but it has gone, so we cannot use it anymore.
+  mSpellCheckingEngine = nsnull;
+  return NS_OK;
 }
 
 nsresult
 mozSpellChecker::SetupDoc(PRInt32 *outBlockOffset)
 {
   nsresult  rv;
 
   nsITextServicesDocument::TSDBlockSelectionStatus blockStatus;
@@ -472,21 +499,20 @@ mozSpellChecker::GetCurrentBlockIndex(ns
   } while (NS_SUCCEEDED(result) && !isDone);
   
   *outBlockIndex = blockIndex;
 
   return result;
 }
 
 nsresult
-mozSpellChecker::InitSpellCheckDictionaryMap()
+mozSpellChecker::GetEngineList(nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines)
 {
   nsresult rv;
   PRBool hasMoreEngines;
-  nsTArray<nsCString> contractIds;
 
   nsCOMPtr<nsICategoryManager> catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
   if (!catMgr)
     return NS_ERROR_NULL_POINTER;
 
   nsCOMPtr<nsISimpleEnumerator> catEntries;
 
   // Get contract IDs of registrated external spell-check engines and
@@ -503,57 +529,29 @@ mozSpellChecker::InitSpellCheckDictionar
     if (NS_FAILED(rv))
       return rv;
 
     nsCString contractId;
     rv = entry->GetData(contractId);
     if (NS_FAILED(rv))
       return rv;
 
-    contractIds.AppendElement(contractId);
-  }
-
-  contractIds.AppendElement(NS_LITERAL_CSTRING(DEFAULT_SPELL_CHECKER));
-
-  // Retrieve dictionaries from all available spellcheckers and
-  // fill mDictionariesMap hash (only the first dictionary with the
-  // each name is used).
-  for (PRUint32 i=0;i < contractIds.Length();i++){
-    PRUint32 count,k;
-    PRUnichar **words;
-
-    const nsCString& contractId = contractIds[i];
-
     // Try to load spellchecker engine. Ignore errors silently
     // except for the last one (HunSpell).
     nsCOMPtr<mozISpellCheckingEngine> engine =
       do_GetService(contractId.get(), &rv);
-    if (NS_FAILED(rv)){
-      // Fail if not succeeded to load HunSpell. Ignore errors
-      // for external spellcheck engines.
-      if (i==contractIds.Length()-1){
-        return rv;
-      }
-
-      continue;
+    if (NS_SUCCEEDED(rv)) {
+      aSpellCheckingEngines->AppendObject(engine);
     }
-
-    engine->GetDictionaryList(&words,&count);
-    for(k=0;k<count;k++){
-      nsAutoString dictName;
+  }
 
-      dictName.Assign(words[k]);
-
-      nsCString dictCName = NS_ConvertUTF16toUTF8(dictName);
-
-      // Skip duplicate dictionaries. Only take the first one
-      // for each name.
-      if (mDictionariesMap.Get(dictName, NULL))
-        continue;
-
-      mDictionariesMap.Put(dictName, new nsCString(contractId));
-    }
-
-    NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+  // Try to load HunSpell spellchecker engine.
+  nsCOMPtr<mozISpellCheckingEngine> engine =
+    do_GetService(DEFAULT_SPELL_CHECKER, &rv);
+  if (NS_FAILED(rv)) {
+    // Fail if not succeeded to load HunSpell. Ignore errors
+    // for external spellcheck engines.
+    return rv;
   }
+  aSpellCheckingEngines->AppendObject(engine);
 
   return NS_OK;
 }
--- a/extensions/spellcheck/src/mozSpellChecker.h
+++ b/extensions/spellcheck/src/mozSpellChecker.h
@@ -15,16 +15,17 @@
  * The Original Code is Mozilla Spellchecker Component.
  *
  * The Initial Developer of the Original Code is
  * David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s): David Einstein Deinst@world.std.com
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -34,16 +35,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef mozSpellChecker_h__
 #define mozSpellChecker_h__
 
 #include "nsCOMPtr.h"
+#include "nsCOMArray.h"
 #include "nsISpellChecker.h"
 #include "nsString.h"
 #include "nsITextServicesDocument.h"
 #include "mozIPersonalDictionary.h"
 #include "mozISpellCheckingEngine.h"
 #include "nsClassHashtable.h"
 #include "nsVoidArray.h"
 #include "nsTArray.h"
@@ -70,29 +72,25 @@ public:
 
   NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord);
   NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord);
   NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList);
 
   NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList);
   NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary);
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary);
+  NS_IMETHOD CheckCurrentDictionary();
 
 protected:
   nsCOMPtr<mozISpellI18NUtil> mConverter;
   nsCOMPtr<nsITextServicesDocument> mTsDoc;
   nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary;
 
-  // Hastable maps directory name to the spellchecker contract ID
-  nsClassHashtable<nsStringHashKey, nsCString> mDictionariesMap;
-
-  nsString mDictionaryName;
-  nsCString *mCurrentEngineContractId;
   nsCOMPtr<mozISpellCheckingEngine>  mSpellCheckingEngine;
   PRBool mFromStart;
 
   nsresult SetupDoc(PRInt32 *outBlockOffset);
 
   nsresult GetCurrentBlockIndex(nsITextServicesDocument *aDoc, PRInt32 *outBlockIndex);
 
-  nsresult InitSpellCheckDictionaryMap();
+  nsresult GetEngineList(nsCOMArray<mozISpellCheckingEngine> *aDictionaryList);
 };
 #endif // mozSpellChecker_h__
--- a/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
+++ b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
@@ -54,49 +54,17 @@ 0x8227F019, 0xAFC7, 0x461e,             
 0x9fe5d975, 0x9bd, 0x44aa,                      \
 { 0xa0, 0x1a, 0x66, 0x40, 0x2e, 0xa2, 0x86, 0x57} }
 
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozHunspell, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(mozHunspellDirProvider)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozSpellChecker, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozPersonalDictionary, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(mozSpellI18NManager)
-
-// This special constructor for the inline spell checker asks the inline
-// spell checker if we can create spell checking objects at all (ie, if there
-// are any dictionaries loaded) before trying to create one. The static
-// CanEnableInlineSpellChecking caches the value so this will be faster (we
-// have to run this code for every edit box we create, as well as for every
-// right click in those edit boxes).
-static nsresult
-mozInlineSpellCheckerConstructor(nsISupports *aOuter, REFNSIID aIID,
-                                 void **aResult)
-{
-  if (! mozInlineSpellChecker::CanEnableInlineSpellChecking())
-    return NS_ERROR_FAILURE;
-
-  nsresult rv;
-
-  *aResult = NULL;
-  if (NULL != aOuter) {
-    rv = NS_ERROR_NO_AGGREGATION;
-    return rv;
-  }
-
-  mozInlineSpellChecker* inst = new mozInlineSpellChecker();
-  if (NULL == inst) {
-    rv = NS_ERROR_OUT_OF_MEMORY;
-    return rv;
-  }
-  NS_ADDREF(inst);
-  rv = inst->QueryInterface(aIID, aResult);
-  NS_RELEASE(inst);
-
-  return rv;
-}
+NS_GENERIC_FACTORY_CONSTRUCTOR(mozInlineSpellChecker)
 
 NS_DEFINE_NAMED_CID(MOZ_HUNSPELL_CID);
 NS_DEFINE_NAMED_CID(HUNSPELLDIRPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_SPELLCHECKER_CID);
 NS_DEFINE_NAMED_CID(MOZ_PERSONALDICTIONARY_CID);
 NS_DEFINE_NAMED_CID(MOZ_SPELLI18NMANAGER_CID);
 NS_DEFINE_NAMED_CID(MOZ_INLINESPELLCHECKER_CID);
 
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/Makefile.in
@@ -0,0 +1,48 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS		= chrome
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/Makefile.in
@@ -0,0 +1,54 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = base map
+
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	test_add_remove_dictionaries.xul \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/Makefile.in
@@ -0,0 +1,52 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome/base
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	base_utf.dic \
+				base_utf.aff \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/base_utf.aff
@@ -0,0 +1,198 @@
+# OpenOffice.org’s en_US.aff file
+# with Unicode apostrophe: ’
+
+SET UTF-8
+TRY esianrtolcdugmphbyfvkwzESIANRTOLCDUGMPHBYFVKWZ'
+
+MAXNGRAMSUGS 1
+WORDCHARS .'’
+
+PFX A Y 1
+PFX A   0     re         .
+
+PFX I Y 1
+PFX I   0     in         .
+
+PFX U Y 1
+PFX U   0     un         .
+
+PFX C Y 1
+PFX C   0     de          .
+
+PFX E Y 1
+PFX E   0     dis         .
+
+PFX F Y 1
+PFX F   0     con         .
+
+PFX K Y 1
+PFX K   0     pro         .
+
+SFX V N 2
+SFX V   e     ive        e
+SFX V   0     ive        [^e]
+
+SFX N Y 3
+SFX N   e     ion        e
+SFX N   y     ication    y 
+SFX N   0     en         [^ey] 
+
+SFX X Y 3
+SFX X   e     ions       e
+SFX X   y     ications   y
+SFX X   0     ens        [^ey]
+
+SFX H N 2
+SFX H   y     ieth       y
+SFX H   0     th         [^y] 
+
+SFX Y Y 1
+SFX Y   0     ly         .
+
+SFX G Y 2
+SFX G   e     ing        e
+SFX G   0     ing        [^e] 
+
+SFX J Y 2
+SFX J   e     ings       e
+SFX J   0     ings       [^e]
+
+SFX D Y 4
+SFX D   0     d          e
+SFX D   y     ied        [^aeiou]y
+SFX D   0     ed         [^ey]
+SFX D   0     ed         [aeiou]y
+
+SFX T N 4
+SFX T   0     st         e
+SFX T   y     iest       [^aeiou]y
+SFX T   0     est        [aeiou]y
+SFX T   0     est        [^ey]
+
+SFX R Y 4
+SFX R   0     r          e
+SFX R   y     ier        [^aeiou]y
+SFX R   0     er         [aeiou]y
+SFX R   0     er         [^ey]
+
+SFX Z Y 4
+SFX Z   0     rs         e
+SFX Z   y     iers       [^aeiou]y
+SFX Z   0     ers        [aeiou]y
+SFX Z   0     ers        [^ey]
+
+SFX S Y 4
+SFX S   y     ies        [^aeiou]y
+SFX S   0     s          [aeiou]y
+SFX S   0     es         [sxzh]
+SFX S   0     s          [^sxzhy]
+
+SFX P Y 3
+SFX P   y     iness      [^aeiou]y
+SFX P   0     ness       [aeiou]y
+SFX P   0     ness       [^y]
+
+SFX M Y 1
+SFX M   0     's         .
+
+SFX B Y 3
+SFX B   0     able       [^aeiou]
+SFX B   0     able       ee
+SFX B   e     able       [^aeiou]e
+
+SFX L Y 1
+SFX L   0     ment       .
+
+REP 88
+REP a ei
+REP ei a
+REP a ey
+REP ey a
+REP ai ie
+REP ie ai
+REP are air
+REP are ear
+REP are eir
+REP air are
+REP air ere
+REP ere air
+REP ere ear
+REP ere eir
+REP ear are
+REP ear air
+REP ear ere
+REP eir are
+REP eir ere
+REP ch te
+REP te ch
+REP ch ti
+REP ti ch
+REP ch tu
+REP tu ch
+REP ch s
+REP s ch
+REP ch k
+REP k ch
+REP f ph
+REP ph f
+REP gh f
+REP f gh
+REP i igh
+REP igh i
+REP i uy
+REP uy i
+REP i ee
+REP ee i
+REP j di
+REP di j
+REP j gg
+REP gg j
+REP j ge
+REP ge j
+REP s ti
+REP ti s
+REP s ci
+REP ci s
+REP k cc
+REP cc k
+REP k qu
+REP qu k
+REP kw qu
+REP o eau
+REP eau o
+REP o ew
+REP ew o
+REP oo ew
+REP ew oo
+REP ew ui
+REP ui ew
+REP oo ui
+REP ui oo
+REP ew u
+REP u ew
+REP oo u
+REP u oo
+REP u oe
+REP oe u
+REP u ieu
+REP ieu u
+REP ue ew
+REP ew ue
+REP uff ough
+REP oo ieu
+REP ieu oo
+REP ier ear
+REP ear ier
+REP ear air
+REP air ear
+REP w qu
+REP qu w
+REP z ss
+REP ss z
+REP shun tion
+REP shun sion
+REP shun cion
+McDonalds’sá/w
+McDonald’sszá/g3)	st:McDonald’s	po:noun_prs	is:TRANS
+McDonald’sszal/g4)	st:McDonald’s	po:noun_prs	is:INSTR
+McDonald’ssal/w
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/base_utf.dic
@@ -0,0 +1,29 @@
+28
+created/U
+create/XKVNGADS
+imply/GNSDX
+natural/PUY
+like/USPBY
+convey/BDGS
+look/GZRDS
+text
+hello
+said
+sawyer
+NASA
+rotten
+day
+tomorrow
+seven
+FAQ/SM
+can’t
+doesn’t
+etc
+won’t
+lip
+text
+horrifying
+speech
+suggest
+uncreate/V
+Hunspell
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/Makefile.in
@@ -0,0 +1,52 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome/map
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	maputf.dic \
+				maputf.aff \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/maputf.aff
@@ -0,0 +1,11 @@
+# With MAP suggestion, Hunspell can add missing accents to a word.
+
+SET UTF-8
+
+# switch off ngram suggestion for testing
+MAXNGRAMSUGS 0
+
+MAP 3
+MAP uúü
+MAP öóo
+MAP ß(ss)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/maputf.dic
@@ -0,0 +1,4 @@
+3
+Frühstück
+tükörfúró
+groß
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window title="Add and remove dictionaries test"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="RunTest();">
+  <title>Add and remove dictionaries test</title>
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript">
+  <![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function getMisspelledWords(editor) {
+  return editor.selectionController.getSelection(Components.interfaces.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+function getDictionaryList(editor) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  var o1 = {};
+  spellchecker.GetDictionaryList(o1, {});
+  return o1.value;
+}
+
+function getCurrentDictionary(editor) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  return spellchecker.GetCurrentDictionary();
+}
+
+function setCurrentDictionary(editor, dictionary) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  spellchecker.SetCurrentDictionary(dictionary);
+}
+
+function RunTest() {
+  var editor = document.getElementById('textbox').editor;
+
+  var dir = Components.classes["@mozilla.org/file/directory_service;1"].
+            getService(Components.interfaces.nsIProperties).
+            get("CurWorkD", Components.interfaces.nsIFile);
+  dir.append("chrome");
+  dir.append("extensions");
+  dir.append("spellcheck");
+  dir.append("tests");
+  dir.append("chrome");
+
+  var hunspell = Components
+    .classes["@mozilla.org/spellchecker/engine;1"]
+    .getService(Components.interfaces.mozISpellCheckingEngine);
+
+  // install base dictionary
+  var base = dir.clone();
+  base.append("base");
+  ok(base.exists());
+  hunspell.addDirectory(base);
+
+  // install map dictionary
+  var map = dir.clone();
+  map.append("map");
+  ok(map.exists());
+  hunspell.addDirectory(map);
+
+  // test that base and map dictionaries are available
+  var dicts = getDictionaryList(editor);
+  isnot(dicts.indexOf("base_utf"), -1, "base is available");
+  isnot(dicts.indexOf("maputf"), -1, "map is available");
+
+  // select base dictionary
+  setCurrentDictionary(editor, "base_utf");
+
+  SimpleTest.executeSoon(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");
+
+    SimpleTest.executeSoon(function() {
+      // test that map dictionary is in use
+      is(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings");
+      is(getCurrentDictionary(editor), "maputf", "current dictionary");
+
+      // uninstall map dictionary
+      hunspell.removeDirectory(map);
+
+      SimpleTest.executeSoon(function() {
+        // test that map dictionary is not in use
+        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
+        hunspell.removeDirectory(base);
+
+        SimpleTest.finish();
+      });
+    });
+  });
+}
+  ]]>
+  </script>
+  <textbox id="textbox" spellcheck="true" value="created imply Frühstück tomorrow qwertyu"/>
+</window>