Bug 496360 part.2 IMEContentObserver shouldn't dispatch TextChangeEvent while editor handles an edit action r=ehsan+smaug, sr=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 31 Jul 2014 13:37:59 +0900
changeset 218623 2cfc400dd0addfb65dc4dd754c948f92590a39ae
parent 218622 e27aee35d0bf9be7f0963f6aa0f764cb5c7f74c8
child 218624 c076e87996951d8f3e2697a3c1c014e3d931f7e2
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, smaug
bugs496360
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 496360 part.2 IMEContentObserver shouldn't dispatch TextChangeEvent while editor handles an edit action r=ehsan+smaug, sr=smaug
content/html/content/src/nsTextEditorState.cpp
dom/events/IMEContentObserver.cpp
dom/events/IMEContentObserver.h
editor/libeditor/base/nsEditor.cpp
editor/libeditor/base/nsEditor.h
editor/libeditor/text/nsPlaintextEditor.cpp
editor/nsIEditorObserver.idl
--- a/content/html/content/src/nsTextEditorState.cpp
+++ b/content/html/content/src/nsTextEditorState.cpp
@@ -925,16 +925,28 @@ nsTextInputListener::EditAction()
 
   if (!mSettingValue) {
     mTxtCtrlElement->OnValueChanged(true);
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsTextInputListener::BeforeEditAction()
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextInputListener::CancelEditAction()
+{
+  return NS_OK;
+}
+
 // END nsIEditorObserver
 
 
 nsresult
 nsTextInputListener::UpdateTextInputCommands(const nsAString& commandsToUpdate,
                                              nsISelection* sel,
                                              int16_t reason)
 {
--- a/dom/events/IMEContentObserver.cpp
+++ b/dom/events/IMEContentObserver.cpp
@@ -20,43 +20,69 @@
 #include "nsIDOMDocument.h"
 #include "nsIDOMRange.h"
 #include "nsIFrame.h"
 #include "nsINode.h"
 #include "nsIPresShell.h"
 #include "nsISelectionController.h"
 #include "nsISelectionPrivate.h"
 #include "nsISupports.h"
+#include "nsITextControlElement.h"
 #include "nsIWidget.h"
 #include "nsPresContext.h"
 #include "nsThreadUtils.h"
 #include "nsWeakReference.h"
 
 namespace mozilla {
 
 using namespace widget;
 
-NS_IMPL_CYCLE_COLLECTION(IMEContentObserver,
-                         mWidget, mSelection,
-                         mRootContent, mEditableNode, mDocShell)
+NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver)
+  nsAutoScriptBlocker scriptBlocker;
+
+  tmp->UnregisterObservers(true);
+
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWidget)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootContent)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditor)
+
+  tmp->mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING;
+  tmp->mESM = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWidget)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelection)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootContent)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditor)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver)
  NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
  NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
  NS_INTERFACE_MAP_ENTRY(nsIReflowObserver)
  NS_INTERFACE_MAP_ENTRY(nsIScrollObserver)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorObserver)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISelectionListener)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver)
 
 IMEContentObserver::IMEContentObserver()
   : mESM(nullptr)
+  , mIsEditorInTransaction(false)
 {
 #ifdef DEBUG
   TestMergingTextChangeData();
 #endif
 }
 
 void
 IMEContentObserver::Init(nsIWidget* aWidget,
@@ -67,16 +93,38 @@ IMEContentObserver::Init(nsIWidget* aWid
   mESM->OnStartToObserveContent(this);
 
   mWidget = aWidget;
   mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aContent);
   if (!mEditableNode) {
     return;
   }
 
+  nsCOMPtr<nsITextControlElement> textControlElement =
+    do_QueryInterface(mEditableNode);
+  if (textControlElement) {
+    // This may fail. For example, <input type="button" contenteditable>
+    mEditor = textControlElement->GetTextEditor();
+    if (!mEditor && mEditableNode->IsContent()) {
+      // The element must be an editing host.
+      nsIContent* editingHost = mEditableNode->AsContent()->GetEditingHost();
+      MOZ_ASSERT(editingHost == mEditableNode,
+                 "found editing host should be mEditableNode");
+      if (editingHost == mEditableNode) {
+        mEditor = nsContentUtils::GetHTMLEditor(aPresContext);
+      }
+    }
+  } else {
+    mEditor = nsContentUtils::GetHTMLEditor(aPresContext);
+  }
+  MOZ_ASSERT(mEditor, "Failed to get editor");
+  if (mEditor) {
+    mEditor->AddEditorObserver(this);
+  }
+
   nsIPresShell* presShell = aPresContext->PresShell();
 
   // get selection and root content
   nsCOMPtr<nsISelectionController> selCon;
   if (mEditableNode->IsNodeOfType(nsINode::eCONTENT)) {
     nsIFrame* frame =
       static_cast<nsIContent*>(mEditableNode.get())->GetPrimaryFrame();
     NS_ENSURE_TRUE_VOID(frame);
@@ -154,44 +202,72 @@ IMEContentObserver::ObserveEditableNode(
     // Add scroll position listener and reflow observer to detect position and
     // size changes
     mDocShell->AddWeakScrollObserver(this);
     mDocShell->AddWeakReflowObserver(this);
   }
 }
 
 void
-IMEContentObserver::Destroy()
+IMEContentObserver::UnregisterObservers(bool aPostEvent)
 {
-  // If CreateTextStateManager failed, mRootContent will be null,
-  // and we should not call NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR))
-  if (mRootContent) {
+  if (mEditor) {
+    mEditor->RemoveEditorObserver(this);
+  }
+
+  // If CreateTextStateManager failed, mRootContent will be null, then, we
+  // should not call NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR))
+  if (mRootContent && mWidget) {
     if (IMEStateManager::IsTestingIME() && mEditableNode) {
       nsIDocument* doc = mEditableNode->OwnerDoc();
-      (new AsyncEventDispatcher(doc, NS_LITERAL_STRING("MozIMEFocusOut"),
-                                false, false))->RunDOMEventWhenSafe();
+      if (doc) {
+        nsRefPtr<AsyncEventDispatcher> dispatcher =
+          new AsyncEventDispatcher(doc, NS_LITERAL_STRING("MozIMEFocusOut"),
+                                   false, false);
+        if (aPostEvent) {
+          dispatcher->PostDOMEvent();
+        } else {
+          dispatcher->RunDOMEventWhenSafe();
+        }
+      }
     }
-    mWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR));
+    // A test event handler might destroy the widget.
+    if (mWidget) {
+      mWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR));
+    }
   }
-  // Even if there are some pending notification, it'll never notify the widget.
-  mWidget = nullptr;
+
   if (mUpdatePreference.WantSelectionChange() && mSelection) {
     nsCOMPtr<nsISelectionPrivate> selPrivate(do_QueryInterface(mSelection));
     if (selPrivate) {
       selPrivate->RemoveSelectionListener(this);
     }
   }
-  mSelection = nullptr;
+
   if (mUpdatePreference.WantTextChange() && mRootContent) {
     mRootContent->RemoveMutationObserver(this);
   }
+
   if (mUpdatePreference.WantPositionChanged() && mDocShell) {
     mDocShell->RemoveWeakScrollObserver(this);
     mDocShell->RemoveWeakReflowObserver(this);
   }
+}
+
+void
+IMEContentObserver::Destroy()
+{
+  // WARNING: When you change this method, you have to check Unlink() too.
+
+  UnregisterObservers(false);
+
+  mEditor = nullptr;
+  // Even if there are some pending notification, it'll never notify the widget.
+  mWidget = nullptr;
+  mSelection = nullptr;
   mRootContent = nullptr;
   mEditableNode = nullptr;
   mDocShell = nullptr;
   mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING;
 
   if (mESM) {
     mESM->OnStopObservingContent(this);
     mESM = nullptr;
@@ -373,28 +449,28 @@ public:
     return NS_OK;
   }
 
 private:
   nsRefPtr<IMEContentObserver> mDispatcher;
   IMEContentObserver::TextChangeData mData;
 };
 
-bool
+void
 IMEContentObserver::StoreTextChangeData(const TextChangeData& aTextChangeData)
 {
   MOZ_ASSERT(aTextChangeData.mStartOffset <= aTextChangeData.mRemovedEndOffset,
              "end of removed text must be same or larger than start");
   MOZ_ASSERT(aTextChangeData.mStartOffset <= aTextChangeData.mAddedEndOffset,
              "end of added text must be same or larger than start");
 
   if (!mTextChangeData.mStored) {
     mTextChangeData = aTextChangeData;
     MOZ_ASSERT(mTextChangeData.mStored, "Why mStored is false?");
-    return true;
+    return;
   }
 
   // |mTextChangeData| should represent all modified text ranges and all
   // inserted text ranges.
   // |mStartOffset| and |mRemovedEndOffset| represent all replaced or removed
   // text ranges.  I.e., mStartOffset should be the smallest offset of all
   // modified text ranges in old text.  |mRemovedEndOffset| should be the
   // largest end offset in old text of all modified text ranges.
@@ -441,17 +517,17 @@ IMEContentObserver::StoreTextChangeData(
     // same text because it doesn't make sensce to compare offsets in different
     // text.
     uint32_t newRemovedEndOffsetInOldText =
       newData.mRemovedEndOffset - oldData.Difference();
     mTextChangeData.mRemovedEndOffset =
       std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
     // The new end offset of added text is always the larger offset.
     mTextChangeData.mAddedEndOffset = newData.mAddedEndOffset;
-    return false;
+    return;
   }
 
   if (newData.mStartOffset >= oldData.mStartOffset) {
     // If new start is in the modified range, it means that new data changes
     // a part or all of the range.
     mTextChangeData.mStartOffset = oldData.mStartOffset;
     if (newData.mRemovedEndOffset >= oldData.mAddedEndOffset) {
       // Case 2:
@@ -468,17 +544,17 @@ IMEContentObserver::StoreTextChangeData(
         newData.mRemovedEndOffset - oldData.Difference();
       mTextChangeData.mRemovedEndOffset =
         std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
       // The old end of added text is replaced by new change. So, it should be
       // same as the new start.  On the other hand, the new added end offset is
       // always same or larger.  Therefore, the merged end offset of added
       // text should be the new end offset of added text.
       mTextChangeData.mAddedEndOffset = newData.mAddedEndOffset;
-      return false;
+      return;
     }
 
     // Case 3:
     // If new end of removed text is less than old end of added text, it means
     // that only a part of the modified range is modified again.  Like:
     // added range of old change:             +------------+
     // removed range of new change:               +-----+
     // So, the new end offset of removed text should be same as the old end
@@ -486,17 +562,17 @@ IMEContentObserver::StoreTextChangeData(
     // text should be the old text change's |mRemovedEndOffset|.
     mTextChangeData.mRemovedEndOffset = oldData.mRemovedEndOffset;
     // The old end of added text is moved by new change.  So, we need to cancel
     // the move of the new change for comparing the offsets in same text.
     uint32_t oldAddedEndOffsetInNewText =
       oldData.mAddedEndOffset + newData.Difference();
     mTextChangeData.mAddedEndOffset =
       std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
-    return false;
+    return;
   }
 
   if (newData.mRemovedEndOffset >= oldData.mStartOffset) {
     // If new end of removed text is greater than old start (and new start is
     // less than old start), it means that a part of modified range is modified
     // again and some new text before the modified range is also modified.
     MOZ_ASSERT(newData.mStartOffset < oldData.mStartOffset,
       "new start offset should be less than old one here");
@@ -517,17 +593,17 @@ IMEContentObserver::StoreTextChangeData(
       mTextChangeData.mRemovedEndOffset =
         std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
       // The old end of added text is replaced by new change.  So, the old end
       // offset of added text is same as new text change's start offset.  Then,
       // new change's end offset of added text is always same or larger than
       // it.  Therefore, merged end offset of added text is always the new end
       // offset of added text.
       mTextChangeData.mAddedEndOffset = newData.mAddedEndOffset;
-      return false;
+      return;
     }
 
     // Case 5:
     // If new end of removed text is less than old end of added text, it
     // means that only a part of the modified range is modified again.  Like:
     // added range of old change:             +----------+
     // removed range of new change:      +----------+
     // So, the new end of removed text should be same as old end of removed
@@ -537,17 +613,17 @@ IMEContentObserver::StoreTextChangeData(
     mTextChangeData.mRemovedEndOffset = oldData.mRemovedEndOffset;
     // The old end of added text is moved by this change.  So, we need to
     // cancel the move of the new change for comparing the offsets in same text
     // because it doesn't make sense to compare the offsets in different text.
     uint32_t oldAddedEndOffsetInNewText =
       oldData.mAddedEndOffset + newData.Difference();
     mTextChangeData.mAddedEndOffset =
       std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
-    return false;
+    return;
   }
 
   // Case 6:
   // Otherwise, i.e., both new end of added text and new start are less than
   // old start, text before the modified range is modified.  Like:
   // added range of old change:                  +----------+
   // removed range of new change: +----------+
   MOZ_ASSERT(newData.mStartOffset < oldData.mStartOffset,
@@ -556,18 +632,16 @@ IMEContentObserver::StoreTextChangeData(
   MOZ_ASSERT(newData.mRemovedEndOffset < oldData.mRemovedEndOffset,
      "new removed end offset should be less than old one here");
   mTextChangeData.mRemovedEndOffset = oldData.mRemovedEndOffset;
   // The end of added text should be adjusted with the new difference.
   uint32_t oldAddedEndOffsetInNewText =
     oldData.mAddedEndOffset + newData.Difference();
   mTextChangeData.mAddedEndOffset =
     std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
-
-  return false;
 }
 
 void
 IMEContentObserver::CharacterDataChanged(nsIDocument* aDocument,
                                          nsIContent* aContent,
                                          CharacterDataChangeInfo* aInfo)
 {
   NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT),
@@ -587,19 +661,18 @@ IMEContentObserver::CharacterDataChanged
                                                   &offset,
                                                   LINE_BREAK_TYPE_NATIVE);
   NS_ENSURE_SUCCESS_VOID(rv);
 
   uint32_t oldEnd = offset + aInfo->mChangeEnd - aInfo->mChangeStart;
   uint32_t newEnd = offset + aInfo->mReplaceLength;
 
   TextChangeData data(offset, oldEnd, newEnd, causedByComposition);
-  if (StoreTextChangeData(data)) {
-    nsContentUtils::AddScriptRunner(new TextChangeEvent(this, mTextChangeData));
-  }
+  StoreTextChangeData(data);
+  FlushMergeableNotifications();
 }
 
 void
 IMEContentObserver::NotifyContentAdded(nsINode* aContainer,
                                        int32_t aStartIndex,
                                        int32_t aEndIndex)
 {
   bool causedByComposition = IsEditorHandlingEventForComposition();
@@ -624,19 +697,18 @@ IMEContentObserver::NotifyContentAdded(n
   NS_ENSURE_SUCCESS_VOID(rv);
 
   if (!addingLength) {
     return;
   }
 
   TextChangeData data(offset, offset, offset + addingLength,
                       causedByComposition);
-  if (StoreTextChangeData(data)) {
-    nsContentUtils::AddScriptRunner(new TextChangeEvent(this, mTextChangeData));
-  }
+  StoreTextChangeData(data);
+  FlushMergeableNotifications();
 }
 
 void
 IMEContentObserver::ContentAppended(nsIDocument* aDocument,
                                     nsIContent* aContainer,
                                     nsIContent* aFirstNewContent,
                                     int32_t aNewIndexInContainer)
 {
@@ -688,19 +760,18 @@ IMEContentObserver::ContentRemoved(nsIDo
                                                      LINE_BREAK_TYPE_NATIVE);
   NS_ENSURE_SUCCESS_VOID(rv);
 
   if (!textLength) {
     return;
   }
 
   TextChangeData data(offset, offset + textLength, offset, causedByComposition);
-  if (StoreTextChangeData(data)) {
-    nsContentUtils::AddScriptRunner(new TextChangeEvent(this, mTextChangeData));
-  }
+  StoreTextChangeData(data);
+  FlushMergeableNotifications();
 }
 
 static nsIContent*
 GetContentBR(dom::Element* aElement)
 {
   if (!aElement->IsNodeOfType(nsINode::eCONTENT)) {
     return nullptr;
   }
@@ -747,18 +818,53 @@ IMEContentObserver::AttributeChanged(nsI
   nsresult rv =
     ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, content,
                                                   0, &start,
                                                   LINE_BREAK_TYPE_NATIVE);
   NS_ENSURE_SUCCESS_VOID(rv);
 
   TextChangeData data(start, start + mPreAttrChangeLength,
                       start + postAttrChangeLength, causedByComposition);
-  if (StoreTextChangeData(data)) {
+  StoreTextChangeData(data);
+  FlushMergeableNotifications();
+}
+
+NS_IMETHODIMP
+IMEContentObserver::EditAction()
+{
+  mIsEditorInTransaction = false;
+  FlushMergeableNotifications();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+IMEContentObserver::BeforeEditAction()
+{
+  mIsEditorInTransaction = true;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+IMEContentObserver::CancelEditAction()
+{
+  mIsEditorInTransaction = false;
+  FlushMergeableNotifications();
+  return NS_OK;
+}
+
+void
+IMEContentObserver::FlushMergeableNotifications()
+{
+  if (mIsEditorInTransaction) {
+    return;
+  }
+
+  if (mTextChangeData.mStored) {
     nsContentUtils::AddScriptRunner(new TextChangeEvent(this, mTextChangeData));
+    mTextChangeData.mStored = false;
   }
 }
 
 #ifdef DEBUG
 // Let's test the code of merging multiple text change data in debug build
 // and crash if one of them fails because this feature is very complex but
 // cannot be tested with mochitest.
 void
--- a/dom/events/IMEContentObserver.h
+++ b/dom/events/IMEContentObserver.h
@@ -6,16 +6,18 @@
 
 #ifndef mozilla_IMEContentObserver_h_
 #define mozilla_IMEContentObserver_h_
 
 #include "mozilla/Attributes.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIDocShell.h" // XXX Why does only this need to be included here?
+#include "nsIEditor.h"
+#include "nsIEditorObserver.h"
 #include "nsIReflowObserver.h"
 #include "nsISelectionListener.h"
 #include "nsIScrollObserver.h"
 #include "nsIWidget.h" // for nsIMEUpdatePreference
 #include "nsStubMutationObserver.h"
 #include "nsWeakReference.h"
 
 class nsIContent;
@@ -24,28 +26,30 @@ class nsISelection;
 class nsPresContext;
 
 namespace mozilla {
 
 class EventStateManager;
 
 // IMEContentObserver notifies widget of any text and selection changes
 // in the currently focused editor
-class IMEContentObserver MOZ_FINAL : public nsISelectionListener,
-                                     public nsStubMutationObserver,
-                                     public nsIReflowObserver,
-                                     public nsIScrollObserver,
-                                     public nsSupportsWeakReference
+class IMEContentObserver MOZ_FINAL : public nsISelectionListener
+                                   , public nsStubMutationObserver
+                                   , public nsIReflowObserver
+                                   , public nsIScrollObserver
+                                   , public nsSupportsWeakReference
+                                   , public nsIEditorObserver
 {
 public:
   IMEContentObserver();
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(IMEContentObserver,
                                            nsISelectionListener)
+  NS_DECL_NSIEDITOROBSERVER
   NS_DECL_NSISELECTIONLISTENER
   NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
   NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE
   NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
   NS_DECL_NSIREFLOWOBSERVER
@@ -119,32 +123,41 @@ public:
     }
   };
 
 private:
   ~IMEContentObserver() {}
 
   void NotifyContentAdded(nsINode* aContainer, int32_t aStart, int32_t aEnd);
   void ObserveEditableNode();
-  // Returns true if there is no pending data.
-  bool StoreTextChangeData(const TextChangeData& aTextChangeData);
+  /**
+   *  UnregisterObservers() unresiters all listeners and observers.
+   *  @param aPostEvent     When true, DOM event will be posted to the thread.
+   *                        Otherwise, dispatched when safe.
+   */
+  void UnregisterObservers(bool aPostEvent);
+  void StoreTextChangeData(const TextChangeData& aTextChangeData);
+  void FlushMergeableNotifications();
 
 #ifdef DEBUG
   void TestMergingTextChangeData();
 #endif
 
   nsCOMPtr<nsIWidget> mWidget;
   nsCOMPtr<nsISelection> mSelection;
   nsCOMPtr<nsIContent> mRootContent;
   nsCOMPtr<nsINode> mEditableNode;
   nsCOMPtr<nsIDocShell> mDocShell;
+  nsCOMPtr<nsIEditor> mEditor;
 
   TextChangeData mTextChangeData;
 
   EventStateManager* mESM;
 
   nsIMEUpdatePreference mUpdatePreference;
   uint32_t mPreAttrChangeLength;
+
+  bool mIsEditorInTransaction;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_IMEContentObserver_h_
--- a/editor/libeditor/base/nsEditor.cpp
+++ b/editor/libeditor/base/nsEditor.cpp
@@ -891,16 +891,17 @@ nsEditor::EndTransaction()
 // is unavailable between transaction manager batches.
 
 NS_IMETHODIMP 
 nsEditor::BeginPlaceHolderTransaction(nsIAtom *aName)
 {
   NS_PRECONDITION(mPlaceHolderBatch >= 0, "negative placeholder batch count!");
   if (!mPlaceHolderBatch)
   {
+    NotifyEditorObservers(eNotifyEditorObserversOfBefore);
     // time to turn on the batch
     BeginUpdateViewBatch();
     mPlaceHolderTxn = nullptr;
     mPlaceHolderName = aName;
     nsRefPtr<Selection> selection = GetSelection();
     if (selection) {
       mSelState = new nsSelectionState();
       mSelState->SaveSelection(selection);
@@ -973,18 +974,20 @@ nsEditor::EndPlaceHolderTransaction()
       {
         // in the future we will check to make sure undo is off here,
         // since that is the only known case where the placeholdertxn would disappear on us.
         // For now just removing the assert.
       }
       // notify editor observers of action but if composing, it's done by
       // text event handler.
       if (!mComposition) {
-        NotifyEditorObservers();
+        NotifyEditorObservers(eNotifyEditorObserversOfEnd);
       }
+    } else {
+      NotifyEditorObservers(eNotifyEditorObserversOfCancel);
     }
   }
   mPlaceHolderBatch--;
   
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -1849,27 +1852,45 @@ public:
   }
 
 private:
   nsRefPtr<nsEditor> mEditor;
   nsCOMPtr<nsIContent> mTarget;
   bool mIsComposing;
 };
 
-void nsEditor::NotifyEditorObservers(void)
-{
-  for (int32_t i = 0; i < mEditorObservers.Count(); i++) {
-    mEditorObservers[i]->EditAction();
-  }
-
-  if (!mDispatchInputEvent) {
-    return;
-  }
-
-  FireInputEvent();
+void
+nsEditor::NotifyEditorObservers(NotificationForEditorObservers aNotification)
+{
+  switch (aNotification) {
+    case eNotifyEditorObserversOfEnd:
+      for (int32_t i = 0; i < mEditorObservers.Count(); i++) {
+        mEditorObservers[i]->EditAction();
+      }
+
+      if (!mDispatchInputEvent) {
+        return;
+      }
+
+      FireInputEvent();
+      break;
+    case eNotifyEditorObserversOfBefore:
+      for (int32_t i = 0; i < mEditorObservers.Count(); i++) {
+        mEditorObservers[i]->BeforeEditAction();
+      }
+      break;
+    case eNotifyEditorObserversOfCancel:
+      for (int32_t i = 0; i < mEditorObservers.Count(); i++) {
+        mEditorObservers[i]->CancelEditAction();
+      }
+      break;
+    default:
+      MOZ_CRASH("Handle all notifications here");
+      break;
+  }
 }
 
 void
 nsEditor::FireInputEvent()
 {
   // We don't need to dispatch multiple input events if there is a pending
   // input event.  However, it may have different event target.  If we resolved
   // this issue, we need to manage the pending events in an array.  But it's
@@ -2071,17 +2092,17 @@ nsEditor::EndIMEComposition()
 
   /* reset the data we need to construct a transaction */
   mIMETextNode = nullptr;
   mIMETextOffset = 0;
   mComposition->EndHandlingComposition(this);
   mComposition = nullptr;
 
   // notify editor observers of action
-  NotifyEditorObservers();
+  NotifyEditorObservers(eNotifyEditorObserversOfEnd);
 }
 
 
 NS_IMETHODIMP
 nsEditor::GetPhonetic(nsAString& aPhonetic)
 {
   if (mPhonetic)
     aPhonetic = *mPhonetic;
--- a/editor/libeditor/base/nsEditor.h
+++ b/editor/libeditor/base/nsEditor.h
@@ -173,17 +173,23 @@ public:
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsEditor,
                                            nsIEditor)
 
   /* ------------ utility methods   -------------- */
   already_AddRefed<nsIDOMDocument> GetDOMDocument();
   already_AddRefed<nsIDocument> GetDocument();
   already_AddRefed<nsIPresShell> GetPresShell();
   already_AddRefed<nsIWidget> GetWidget();
-  void NotifyEditorObservers();
+  enum NotificationForEditorObservers
+  {
+    eNotifyEditorObserversOfEnd,
+    eNotifyEditorObserversOfBefore,
+    eNotifyEditorObserversOfCancel
+  };
+  void NotifyEditorObservers(NotificationForEditorObservers aNotification);
 
   /* ------------ nsIEditor methods -------------- */
   NS_DECL_NSIEDITOR
 
   /* ------------ nsIEditorIMESupport methods -------------- */
   NS_DECL_NSIEDITORIMESUPPORT
 
   /* ------------ nsIObserver methods -------------- */
--- a/editor/libeditor/text/nsPlaintextEditor.cpp
+++ b/editor/libeditor/text/nsPlaintextEditor.cpp
@@ -860,16 +860,18 @@ nsPlaintextEditor::UpdateIMEComposition(
 
   nsCOMPtr<nsIPresShell> ps = GetPresShell();
   NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
 
   nsCOMPtr<nsISelection> selection;
   nsresult rv = GetSelection(getter_AddRefs(selection));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
   nsRefPtr<nsCaret> caretP = ps->GetCaret();
 
   {
     TextComposition::TextEventHandlingMarker
       textEventHandlingMarker(mComposition, widgetTextEvent);
 
     nsAutoPlaceHolderBatch batch(this, nsGkAtoms::IMETxnName);
 
@@ -880,17 +882,17 @@ nsPlaintextEditor::UpdateIMEComposition(
     }
   }
 
   // If still composing, we should fire input event via observer.
   // Note that if committed, we don't need to notify it since it will be
   // notified at followed compositionend event.
   // NOTE: We must notify after the auto batch will be gone.
   if (IsIMEComposing()) {
-    NotifyEditorObservers();
+    NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   }
 
   return rv;
 }
 
 already_AddRefed<nsIContent>
 nsPlaintextEditor::GetInputEventTargetContent()
 {
@@ -1095,57 +1097,61 @@ nsPlaintextEditor::Undo(uint32_t aCount)
 {
   // Protect the edit rules object from dying
   nsCOMPtr<nsIEditRules> kungFuDeathGrip(mRules);
 
   nsAutoUpdateViewBatch beginViewBatching(this);
 
   ForceCompositionEnd();
 
+  NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
   nsAutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone);
 
   nsTextRulesInfo ruleInfo(EditAction::undo);
   nsRefPtr<Selection> selection = GetSelection();
   bool cancel, handled;
   nsresult result = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
   
   if (!cancel && NS_SUCCEEDED(result))
   {
     result = nsEditor::Undo(aCount);
     result = mRules->DidDoAction(selection, &ruleInfo, result);
   } 
-   
-  NotifyEditorObservers();
+
+  NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   return result;
 }
 
 NS_IMETHODIMP 
 nsPlaintextEditor::Redo(uint32_t aCount)
 {
   // Protect the edit rules object from dying
   nsCOMPtr<nsIEditRules> kungFuDeathGrip(mRules);
 
   nsAutoUpdateViewBatch beginViewBatching(this);
 
   ForceCompositionEnd();
 
+  NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
   nsAutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone);
 
   nsTextRulesInfo ruleInfo(EditAction::redo);
   nsRefPtr<Selection> selection = GetSelection();
   bool cancel, handled;
   nsresult result = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
   
   if (!cancel && NS_SUCCEEDED(result))
   {
     result = nsEditor::Redo(aCount);
     result = mRules->DidDoAction(selection, &ruleInfo, result);
   } 
-   
-  NotifyEditorObservers();
+
+  NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   return result;
 }
 
 bool
 nsPlaintextEditor::CanCutOrCopy()
 {
   nsCOMPtr<nsISelection> selection;
   if (NS_FAILED(GetSelection(getter_AddRefs(selection))))
--- a/editor/nsIEditorObserver.idl
+++ b/editor/nsIEditorObserver.idl
@@ -4,23 +4,33 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 /*
 Editor Observer interface to outside world
 */
 
-[scriptable, uuid(e52a09fd-d33a-4f85-be0a-fbd348f0fa27)]
+[scriptable, uuid(f3ee57a6-890c-4ce0-a584-8a84bba0292e)]
 
 /**
  * A generic editor observer interface. 
  * <P>
  * nsIEditorObserver is the interface used by applications wishing to be notified
  * when the editor has completed a user action. 
  *
  */
 interface nsIEditorObserver : nsISupports {
   /** 
    * Called after the editor completes a user action.
    */
   void EditAction();
+  /**
+   * Called when editor starts to handle a user action.  I.e., This must be
+   * called before the first DOM change.
+   */
+  void BeforeEditAction();
+  /**
+   * Called after BeforeEditAction() is called but EditorAction() won't be
+   * called.
+   */
+  void CancelEditAction();
 };