Bug 1217700 part.3 Expose text change, selection change and position change notifications to nsITextInputProcessorCallback with nsITextInputProcessorNotification r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 20 Apr 2017 20:17:03 +0900
changeset 566332 ab221fb1ba32534ad3df2478051161c37579791e
parent 566331 0ea1fc4888f06e0b9dde84ad60aa7632fc2ccb1a
child 566333 ca065b2e52d9068d713074a5b0f49920f681c40a
push id55180
push userjjong@mozilla.com
push dateFri, 21 Apr 2017 09:36:13 +0000
reviewerssmaug
bugs1217700
milestone55.0a1
Bug 1217700 part.3 Expose text change, selection change and position change notifications to nsITextInputProcessorCallback with nsITextInputProcessorNotification r=smaug For testing IMEContentObserver, text change, selection change and position change notifications should be exposed to JS with nsITextInputProcessorNotification. MozReview-Commit-ID: 3PUhKXRwnAn
dom/base/TextInputProcessor.cpp
dom/base/test/chrome/window_nsITextInputProcessor.xul
dom/interfaces/base/nsITextInputProcessorCallback.idl
--- a/dom/base/TextInputProcessor.cpp
+++ b/dom/base/TextInputProcessor.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gfxPrefs.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TextInputProcessor.h"
+#include "mozilla/widget/IMEData.h"
 #include "nsContentUtils.h"
 #include "nsIDocShell.h"
 #include "nsIWidget.h"
 #include "nsPIDOMWindow.h"
 #include "nsPresContext.h"
 
 using namespace mozilla::widget;
 
@@ -22,35 +23,263 @@ namespace mozilla {
 
 /******************************************************************************
  * TextInputProcessorNotification
  ******************************************************************************/
 
 class TextInputProcessorNotification final :
         public nsITextInputProcessorNotification
 {
+  typedef IMENotification::SelectionChangeData SelectionChangeData;
+  typedef IMENotification::SelectionChangeDataBase SelectionChangeDataBase;
+  typedef IMENotification::TextChangeData TextChangeData;
+  typedef IMENotification::TextChangeDataBase TextChangeDataBase;
+
 public:
   explicit TextInputProcessorNotification(const char* aType)
     : mType(aType)
   {
   }
 
+  explicit TextInputProcessorNotification(
+             const TextChangeDataBase& aTextChangeData)
+    : mType("notify-text-change")
+    , mTextChangeData(aTextChangeData)
+  {
+  }
+
+  explicit TextInputProcessorNotification(
+             const SelectionChangeDataBase& aSelectionChangeData)
+    : mType("notify-selection-change")
+    , mSelectionChangeData(aSelectionChangeData)
+  {
+    // SelectionChangeDataBase::mString still refers nsString instance owned
+    // by aSelectionChangeData.  So, this needs to copy the instance.
+    nsString* string = new nsString(aSelectionChangeData.String());
+    mSelectionChangeData.mString = string;
+  }
+
   NS_DECL_ISUPPORTS
 
   NS_IMETHOD GetType(nsACString& aType) override final
   {
     aType = mType;
     return NS_OK;
   }
 
+  // "notify-text-change" and "notify-selection-change"
+  NS_IMETHOD GetOffset(uint32_t* aOffset) override final
+  {
+    if (NS_WARN_IF(!aOffset)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aOffset = mSelectionChangeData.mOffset;
+      return NS_OK;
+    }
+    if (IsTextChange()) {
+      *aOffset = mTextChangeData.mStartOffset;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // "notify-selection-change"
+  NS_IMETHOD GetText(nsAString& aText) override final
+  {
+    if (IsSelectionChange()) {
+      aText = mSelectionChangeData.String();
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetCollapsed(bool* aCollapsed) override final
+  {
+    if (NS_WARN_IF(!aCollapsed)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aCollapsed = mSelectionChangeData.IsCollapsed();
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetLength(uint32_t* aLength) override final
+  {
+    if (NS_WARN_IF(!aLength)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aLength = mSelectionChangeData.Length();
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetReversed(bool* aReversed) override final
+  {
+    if (NS_WARN_IF(!aReversed)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aReversed = mSelectionChangeData.mReversed;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetWritingMode(nsACString& aWritingMode) override final
+  {
+    if (IsSelectionChange()) {
+      WritingMode writingMode = mSelectionChangeData.GetWritingMode();
+      if (!writingMode.IsVertical()) {
+        aWritingMode.AssignLiteral("horizontal-tb");
+      } else if (writingMode.IsVerticalLR()) {
+        aWritingMode.AssignLiteral("vertical-lr");
+      } else {
+        aWritingMode.AssignLiteral("vertical-rl");
+      }
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetCausedByComposition(bool* aCausedByComposition) override final
+  {
+    if (NS_WARN_IF(!aCausedByComposition)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aCausedByComposition = mSelectionChangeData.mCausedByComposition;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetCausedBySelectionEvent(
+               bool* aCausedBySelectionEvent) override final
+  {
+    if (NS_WARN_IF(!aCausedBySelectionEvent)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aCausedBySelectionEvent = mSelectionChangeData.mCausedBySelectionEvent;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetOccurredDuringComposition(
+               bool* aOccurredDuringComposition) override final
+  {
+    if (NS_WARN_IF(!aOccurredDuringComposition)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsSelectionChange()) {
+      *aOccurredDuringComposition =
+        mSelectionChangeData.mOccurredDuringComposition;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // "notify-text-change"
+  NS_IMETHOD GetRemovedLength(uint32_t* aLength) override final
+  {
+    if (NS_WARN_IF(!aLength)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsTextChange()) {
+      *aLength = mTextChangeData.OldLength();
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetAddedLength(uint32_t* aLength) override final
+  {
+    if (NS_WARN_IF(!aLength)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsTextChange()) {
+      *aLength = mTextChangeData.NewLength();
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetCausedOnlyByComposition(
+               bool* aCausedOnlyByComposition) override final
+  {
+    if (NS_WARN_IF(!aCausedOnlyByComposition)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsTextChange()) {
+      *aCausedOnlyByComposition = mTextChangeData.mCausedOnlyByComposition;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetIncludingChangesDuringComposition(
+               bool* aIncludingChangesDuringComposition) override final
+  {
+    if (NS_WARN_IF(!aIncludingChangesDuringComposition)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsTextChange()) {
+      *aIncludingChangesDuringComposition =
+        mTextChangeData.mIncludingChangesDuringComposition;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  NS_IMETHOD GetIncludingChangesWithoutComposition(
+               bool* aIncludingChangesWithoutComposition) override final
+  {
+    if (NS_WARN_IF(!aIncludingChangesWithoutComposition)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (IsTextChange()) {
+      *aIncludingChangesWithoutComposition =
+        mTextChangeData.mIncludingChangesWithoutComposition;
+      return NS_OK;
+    }
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
 protected:
-  ~TextInputProcessorNotification() { }
+  virtual ~TextInputProcessorNotification()
+  {
+    if (IsSelectionChange()) {
+      delete mSelectionChangeData.mString;
+      mSelectionChangeData.mString = nullptr;
+    }
+  }
+
+  bool IsTextChange() const
+  {
+    return mType.EqualsLiteral("notify-text-change");
+  }
+
+  bool IsSelectionChange() const
+  {
+    return mType.EqualsLiteral("notify-selection-change");
+  }
 
 private:
   nsAutoCString mType;
+  union
+  {
+    TextChangeDataBase mTextChangeData;
+    SelectionChangeDataBase mSelectionChangeData;
+  };
 
   TextInputProcessorNotification() { }
 };
 
 NS_IMPL_ISUPPORTS(TextInputProcessorNotification,
                   nsITextInputProcessorNotification)
 
 /******************************************************************************
@@ -663,16 +892,28 @@ TextInputProcessor::NotifyIME(TextEventD
         break;
       }
       case NOTIFY_IME_OF_FOCUS:
         notification = new TextInputProcessorNotification("notify-focus");
         break;
       case NOTIFY_IME_OF_BLUR:
         notification = new TextInputProcessorNotification("notify-blur");
         break;
+      case NOTIFY_IME_OF_TEXT_CHANGE:
+        notification = new TextInputProcessorNotification(
+                             aNotification.mTextChangeData);
+        break;
+      case NOTIFY_IME_OF_SELECTION_CHANGE:
+        notification = new TextInputProcessorNotification(
+                             aNotification.mSelectionChangeData);
+        break;
+      case NOTIFY_IME_OF_POSITION_CHANGE:
+        notification = new TextInputProcessorNotification(
+                             "notify-position-change");
+        break;
       default:
         return NS_ERROR_NOT_IMPLEMENTED;
     }
     MOZ_RELEASE_ASSERT(notification);
     bool result = false;
     nsresult rv = mCallback->OnNotify(this, notification, &result);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
@@ -696,18 +937,20 @@ TextInputProcessor::NotifyIME(TextEventD
     default:
       return NS_ERROR_NOT_IMPLEMENTED;
   }
 }
 
 NS_IMETHODIMP_(IMENotificationRequests)
 TextInputProcessor::GetIMENotificationRequests()
 {
-  // TextInputProcessor::NotifyIME does not require extra change notifications.
-  return IMENotificationRequests();
+  // TextInputProcessor should support all change notifications.
+  return IMENotificationRequests(
+           IMENotificationRequests::NOTIFY_TEXT_CHANGE |
+           IMENotificationRequests::NOTIFY_POSITION_CHANGE);
 }
 
 NS_IMETHODIMP_(void)
 TextInputProcessor::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher)
 {
   // If This is called while this is being initialized, ignore the call.
   if (!mDispatcher) {
     return;
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -54,16 +54,18 @@ function finish()
   window.close();
 }
 
 function onunload()
 {
   SimpleTest.finish();
 }
 
+const kIsMac = (navigator.platform.indexOf("Mac") == 0);
+
 var iframe = document.getElementById("iframe");
 var childWindow = iframe.contentWindow;
 var textareaInFrame;
 var input = document.getElementById("input");
 var otherWindow = window.opener;
 var otherDocument = otherWindow.document;
 var inputInChildWindow = otherDocument.getElementById("input");
 
@@ -3781,17 +3783,17 @@ function runCommitCompositionTests()
   TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
   TIP.setCaretInPendingComposition(3);
   TIP.flushPendingComposition();
   doCommitWithNullCheck(undefined);
   is(input.value, "",
      description + "doCommitWithNullCheck(undefined) should commit the composition with empty string");
 }
 
-function runUnloadTests1(aNextTest)
+function runUnloadTests1()
 {
   var description = "runUnloadTests1(): ";
 
   var TIP1 = createTIP();
   ok(TIP1.beginInputTransactionForTests(childWindow),
      description + "TIP1.beginInputTransactionForTests() should succeed");
 
   var oldSrc = iframe.src;
@@ -3803,17 +3805,17 @@ function runUnloadTests1(aNextTest)
     childWindow = iframe.contentWindow;
     textareaInFrame = null;
     iframe.addEventListener("load", function () {
       ok(true, description + "old iframe is restored");
       // And also restore the iframe information with restored contents.
       iframe.removeEventListener("load", arguments.callee, true);
       childWindow = iframe.contentWindow;
       textareaInFrame = iframe.contentDocument.getElementById("textarea");
-      setTimeout(aNextTest, 0);
+      SimpleTest.executeSoon(continueTest);
     }, true);
 
     // The composition should be committed internally.  So, another TIP should
     // be able to steal the rights to using TextEventDispatcher.
     var TIP2 = createTIP();
     ok(TIP2.beginInputTransactionForTests(parentWindow),
        description + "TIP2.beginInputTransactionForTests() should succeed");
 
@@ -3843,17 +3845,17 @@ function runUnloadTests1(aNextTest)
   TIP1.flushPendingComposition();
   is(textareaInFrame.value, "foo",
      description + "the textarea in the iframe should have composition string");
 
   // Load different web page on the frame.
   iframe.src = "data:text/html,<body>dummy page</body>";
 }
 
-function runUnloadTests2(aNextTest)
+function runUnloadTests2()
 {
   var description = "runUnloadTests2(): ";
 
   var TIP = createTIP();
   ok(TIP.beginInputTransactionForTests(childWindow),
      description + "TIP.beginInputTransactionForTests() should succeed");
 
   var oldSrc = iframe.src;
@@ -3865,17 +3867,17 @@ function runUnloadTests2(aNextTest)
     childWindow = iframe.contentWindow;
     textareaInFrame = null;
     iframe.addEventListener("load", function () {
       ok(true, description + "old iframe is restored");
       // And also restore the iframe information with restored contents.
       iframe.removeEventListener("load", arguments.callee, true);
       childWindow = iframe.contentWindow;
       textareaInFrame = iframe.contentDocument.getElementById("textarea");
-      setTimeout(aNextTest, 0);
+      SimpleTest.executeSoon(continueTest);
     }, true);
 
     input.focus();
     input.value = "";
 
     // TIP should be still available in the same top level widget.
     TIP.setPendingCompositionString("bar");
     TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
@@ -3906,53 +3908,121 @@ function runUnloadTests2(aNextTest)
   TIP.flushPendingComposition();
   is(textareaInFrame.value, "foo",
      description + "the textarea in the iframe should have composition string");
 
   // Load different web page on the frame.
   iframe.src = "data:text/html,<body>dummy page</body>";
 }
 
-function runCallbackTests(aForTests)
+function* runCallbackTests(aForTests)
 {
   var description = "runCallbackTests(aForTests=" + aForTests + "): ";
 
   input.value = "";
   input.focus();
   input.blur();
 
   var TIP = createTIP();
   var notifications = [];
+  var callContinueTest = false;
   function callback(aTIP, aNotification)
   {
+    if (aTIP == TIP) {
+      notifications.push(aNotification);
+    }
     switch (aNotification.type) {
       case "request-to-commit":
         aTIP.commitComposition();
         break;
       case "request-to-cancel":
         aTIP.cancelComposition();
         break;
     }
-    if (aTIP == TIP) {
-      notifications.push(aNotification);
+    if (callContinueTest) {
+      callContinueTest = false;
+      SimpleTest.executeSoon(continueTest);
     }
     return true;
   }
 
   function dumpUnexpectedNotifications(aExpectedCount)
   {
     if (notifications.length <= aExpectedCount) {
       return;
     }
     for (var i = aExpectedCount; i < notifications.length; i++) {
       ok(false,
          description + "Unexpected notification: " + notifications[i].type);
     }
   }
 
+  function waitUntilNotificationsReceived()
+  {
+    if (notifications.length > 0) {
+      SimpleTest.executeSoon(continueTest);
+    } else {
+      callContinueTest = true;
+    }
+  }
+
+  function checkPositionChangeNotification(aNotification, aDescription)
+  {
+    is(!aNotification || aNotification.type, "notify-position-change",
+       aDescription + " should cause position change notification");
+  }
+
+  function checkSelectionChangeNotification(aNotification, aDescription, aExpected)
+  {
+    is(aNotification.type, "notify-selection-change",
+       aDescription + " should cause selection change notification");
+    if (aNotification.type != "notify-selection-change") {
+      return;
+    }
+    is(aNotification.offset, aExpected.offset,
+       aDescription + " should cause selection change notification whose offset is " + aExpected.offset);
+    is(aNotification.text, aExpected.text,
+       aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'");
+    is(aNotification.collapsed, aExpected.text.length == 0,
+       aDescription + " should cause selection change notification whose collapsed is " + (aExpected.text.length == 0));
+    is(aNotification.length, aExpected.text.length,
+       aDescription + " should cause selection change notification whose length is " + aExpected.text.length);
+    is(aNotification.reversed, aExpected.reversed || false,
+       aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false));
+    is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb",
+       aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb"));
+    is(aNotification.causedByComposition, aExpected.causedByComposition || false,
+       aDescription + " should cause selection change notification whose causedByComposition is " + (aExpected.causedByComposition || false));
+    is(aNotification.causedBySelectionEvent, aExpected.causedBySelectionEvent || false,
+       aDescription + " should cause selection change notification whose causedBySelectionEvent is " + (aExpected.causedBySelectionEvent || false));
+    is(aNotification.occurredDuringComposition, aExpected.occurredDuringComposition || false,
+       aDescription + " should cause cause selection change notification whose occurredDuringComposition is " + (aExpected.occurredDuringComposition || false));
+  }
+
+  function checkTextChangeNotification(aNotification, aDescription, aExpected)
+  {
+    is(aNotification.type, "notify-text-change",
+       aDescription + " should cause text change notification");
+    if (aNotification.type != "notify-text-change") {
+      return;
+    }
+    is(aNotification.offset, aExpected.offset,
+       aDescription + " should cause text change notification whose offset is " + aExpected.offset);
+    is(aNotification.removedLength, aExpected.removedLength,
+       aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength);
+    is(aNotification.addedLength, aExpected.addedLength,
+       aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength);
+    is(aNotification.causedOnlyByComposition, aExpected.causedOnlyByComposition || false,
+       aDescription + " should cause text change notification whose causedOnlyByComposition is " + (aExpected.causedOnlyByComposition || false));
+    is(aNotification.includingChangesDuringComposition, aExpected.includingChangesDuringComposition || false,
+       aDescription + " should cause text change notification whose includingChangesDuringComposition is " + (aExpected.includingChangesDuringComposition || false));
+    is(aNotification.includingChangesWithoutComposition, typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true,
+       aDescription + " should cause text change notification whose includingChangesWithoutComposition is " + (typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true));
+  }
+
   if (aForTests) {
     TIP.beginInputTransactionForTests(window, callback);
   } else {
     TIP.beginInputTransaction(window, callback);
   }
 
   notifications = [];
   input.focus();
@@ -3966,58 +4036,151 @@ function runCallbackTests(aForTests)
   input.blur();
   is(notifications.length, 1,
      description + "input.blur() should cause a notification");
   is(notifications[0].type, "notify-blur",
      description + "input.blur() should cause \"notify-focus\"");
   dumpUnexpectedNotifications(1);
 
   input.focus();
+  notifications = [];
   TIP.setPendingCompositionString("foo");
   TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
   TIP.flushPendingComposition();
+  is(notifications.length, 3,
+     description + "creating composition string 'foo' should cause 3 notifications");
+  checkTextChangeNotification(notifications[0], description + "creating composition string 'foo'",
+                              { offset: 0, removedLength: 0, addedLength: 3,
+                                causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
+  checkSelectionChangeNotification(notifications[1], description + "creating composition string 'foo'",
+                                   { offset: 3, text: "", causedByComposition: true, occurredDuringComposition: true });
+  checkPositionChangeNotification(notifications[2], description + "creating composition string 'foo'");
+  dumpUnexpectedNotifications(3);
+
   notifications = [];
   synthesizeMouseAtCenter(input, {});
-  is(notifications.length, 1,
-     description + "synthesizeMouseAtCenter(input, {}) during composition should cause a notification");
+  is(notifications.length, 3,
+     description + "synthesizeMouseAtCenter(input, {}) during composition should cause 3 notifications");
   is(notifications[0].type, "request-to-commit",
      description + "synthesizeMouseAtCenter(input, {}) during composition should cause \"request-to-commit\"");
+  checkTextChangeNotification(notifications[1], description + "synthesizeMouseAtCenter(input, {}) during composition",
+                              { offset: 0, removedLength: 3, addedLength: 3,
+                                causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
+  checkPositionChangeNotification(notifications[2], description + "synthesizeMouseAtCenter(input, {}) during composition");
+  dumpUnexpectedNotifications(3);
+
+  // XXX On macOS, window.moveBy() doesn't cause notify-position-change.
+  //     Investigate this later (although, we cannot notify position change to
+  //     native IME on macOS).
+  if (!kIsMac) {
+    input.focus();
+    notifications = [];
+    window.moveBy(0, 10);
+    yield waitUntilNotificationsReceived();
+    is(notifications.length, 1,
+       description + "window.moveBy(0, 10) should cause a notification");
+    checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)");
+    dumpUnexpectedNotifications(1);
+
+    input.focus();
+    notifications = [];
+    window.moveBy(10, 0);
+    yield waitUntilNotificationsReceived();
+    is(notifications.length, 1,
+       description + "window.moveBy(10, 0) should cause a notification");
+    checkPositionChangeNotification(notifications[0], description + "window.moveBy(10, 0)");
+    dumpUnexpectedNotifications(1);
+  }
+
+  input.focus();
+  input.value = "abc"
+  notifications = [];
+  input.selectionStart = input.selectionEnd = 0;
+  yield waitUntilNotificationsReceived();
+  notifications = [];
+  var rightArrowKeyEvent =
+    new KeyboardEvent("", { key: "ArrowRight", code: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT });
+  TIP.keydown(rightArrowKeyEvent);
+  TIP.keyup(rightArrowKeyEvent);
+  is(notifications.length, 1,
+     description + "ArrowRight key press should cause a notification");
+  checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press", { offset: 1, text: "" });
+  dumpUnexpectedNotifications(1);
+
+  notifications = [];
+  var shiftKeyEvent =
+    new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT });
+  var leftArrowKeyEvent =
+    new KeyboardEvent("", { key: "ArrowLeft", code: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT });
+  TIP.keydown(shiftKeyEvent);
+  TIP.keydown(leftArrowKeyEvent);
+  TIP.keyup(leftArrowKeyEvent);
+  TIP.keyup(shiftKeyEvent);
+  is(notifications.length, 1,
+     description + "ArrowLeft key press with Shift should cause a notification");
+  checkSelectionChangeNotification(notifications[0], description + "ArrowLeft key press with Shift", { offset: 0, text: "a", reversed: true });
+  dumpUnexpectedNotifications(1);
+
+  TIP.keydown(rightArrowKeyEvent);
+  TIP.keyup(rightArrowKeyEvent);
+  notifications = [];
+  TIP.keydown(shiftKeyEvent);
+  TIP.keydown(rightArrowKeyEvent);
+  TIP.keyup(rightArrowKeyEvent);
+  TIP.keyup(shiftKeyEvent);
+  is(notifications.length, 1,
+     description + "ArrowRight key press with Shift should cause a notification");
+  checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press with Shift", { offset: 1, text: "b" });
   dumpUnexpectedNotifications(1);
 
   notifications = [];
   var TIP2 = createTIP();
   if (aForTests) {
     TIP2.beginInputTransactionForTests(window, callback);
   } else {
     TIP2.beginInputTransaction(window, callback);
   }
   is(notifications.length, 1,
      description + "Initializing another TIP should cause a notification");
   is(notifications[0].type, "notify-end-input-transaction",
      description + "Initializing another TIP should cause \"notify-detached\"");
   dumpUnexpectedNotifications(1);
 }
 
-function runTests()
+var gTestContinuation = null;
+
+function continueTest()
 {
-  textareaInFrame = iframe.contentDocument.getElementById("textarea");
-
+  if (!gTestContinuation) {
+    gTestContinuation = testBody();
+  }
+  var ret = gTestContinuation.next();
+  if (ret.done) {
+    finish();
+  }
+}
+
+function* testBody()
+{
   runBeginInputTransactionMethodTests();
   runReleaseTests();
   runCompositionTests();
   runCompositionWithKeyEventTests();
   runConsumingKeydownBeforeCompositionTests();
   runKeyTests();
   runErrorTests();
   runCommitCompositionTests();
-  runCallbackTests(false);
-  runCallbackTests(true);
-  runUnloadTests1(function () {
-    runUnloadTests2(function () {
-      finish();
-    });
-  });
+  yield* runCallbackTests(false);
+  yield* runCallbackTests(true);
+  yield runUnloadTests1();
+  yield runUnloadTests2();
+}
+
+function runTests()
+{
+  textareaInFrame = iframe.contentDocument.getElementById("textarea");
+  continueTest();
 }
 
 ]]>
 </script>
 
 </window>
--- a/dom/interfaces/base/nsITextInputProcessorCallback.idl
+++ b/dom/interfaces/base/nsITextInputProcessorCallback.idl
@@ -39,18 +39,153 @@ interface nsITextInputProcessorNotificat
    * "notify-focus" (optional)
    *   This is notified when an editable editor gets focus and Gecko starts
    *   to observe changes in the content. E.g., selection changes.
    *   IME shouldn't change DOM tree, focus nor something when this is notified.
    *
    * "notify-blur" (optional)
    *   This is notified when an editable editor loses focus and Gecko stops
    *   observing the changes in the content.
+   *
+   * "notify-text-change" (optional)
+   *   This is notified when text in the focused editor is modified.
+   *   Some attributes below are available to retrieve the detail.
+   *   IME shouldn't change DOM tree, focus nor something when this is notified.
+   *   Note that when there is no chance to notify you of some text changes
+   *   safely, this represents all changes as a change.
+   *
+   * "notify-selection-change" (optional)
+   *   This is notified when selection in the focused editor is changed.
+   *   Some attributes below are available to retrieve the detail.
+   *   IME shouldn't change DOM tree, focus nor something when this is notified.
+   *   Note that when there was no chance to notify you of this safely, this
+   *   represents the latest selection change.
+   *
+   * "notify-position-change" (optional)
+   *   This is notified when layout is changed in the editor or the window
+   *   is moved.
+   *   IME shouldn't change DOM tree, focus nor something when this is notified.
+   *   Note that when there was no chance to notify you of this safely, this
+   *   represents the latest layout change.
    */
   readonly attribute ACString type;
+
+  /**
+   * Be careful, line breakers in the editor are treated as native line
+   * breakers.  I.e., on Windows, a line breaker is "\r\n" (CRLF).  On the
+   * others, it is "\n" (LF).  If you want TextInputProcessor to treat line
+   * breakers on Windows as XP line breakers (LF), please file a bug with
+   * the reason why you need the behavior.
+   */
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change" or
+   * "notify-selection-change".
+   * This is offset of the start of modified text range if type is
+   * "notify-text-change".  Or offset of start of selection if type is
+   * "notify-selection-change".
+   */
+  readonly attribute unsigned long offset;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * This is selected text.  I.e., the length is selected length and
+   * it's empty if the selection is collapsed.
+   */
+  readonly attribute AString text;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * This is set to true when the selection is collapsed.  Otherwise, false.
+   */
+  readonly attribute boolean collapsed;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * This is selected length.  I.e., if this is 0, collapsed is set to true.
+   */
+  readonly attribute uint32_t length;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * When selection is created from latter point to former point, this is
+   * set to true.  Otherwise, false.
+   * I.e., if this is true, offset + length is the anchor of selection.
+   */
+  readonly attribute boolean reversed;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * This indicates the start of the selection's writing mode.
+   * The value can be "horizontal-tb", "vertical-rl" or "vertical-lr".
+   */
+  readonly attribute ACString writingMode;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * If the selection change was caused by composition, this is set to true.
+   * Otherwise, false.
+   */
+  readonly attribute boolean causedByComposition;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * If the selection change was caused by selection event, this is set to true.
+   * Otherwise, false.
+   */
+  readonly attribute boolean causedBySelectionEvent;
+
+  /**
+   * This attribute has a valid value when type is "notify-selection-change".
+   * If the selection change occurred during composition, this is set to true.
+   * Otherwise, false.
+   */
+  readonly attribute boolean occurredDuringComposition;
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change".
+   * This is removed text length by the change(s).  If this is empty, new text
+   * was just inserted.  Otherwise, the text is replaced with new text.
+   */
+  readonly attribute unsigned long removedLength;
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change".
+   * This is added text length by the change(s).  If this is empty, old text
+   * was just deleted.  Otherwise, the text replaces the old text.
+   */
+  readonly attribute unsigned long addedLength;
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change".
+   * If the text change(s) was caused only by composition, this is set to true.
+   * Otherwise, false.  I.e., if one of text changes are caused by JS or
+   * modifying without composition, this is set to false.
+   */
+  readonly attribute boolean causedOnlyByComposition;
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change".
+   * If at least one text change not caused by composition occurred during
+   * composition, this is set to true.  Otherwise, false.
+   * Note that this is set to false when new change is caused by neither
+   * composition nor occurred during composition because it's outdated for
+   * new composition.
+   * In other words, when text changes not caused by composition occurred
+   * during composition and it may cause committing composition, this is
+   * set to true.
+   */
+  readonly attribute boolean includingChangesDuringComposition;
+
+  /**
+   * This attribute has a valid value when type is "notify-text-change".
+   * If at least one text change occurred when there was no composition, this
+   * is set to true.  Otherwise, false.
+   */
+  readonly attribute boolean includingChangesWithoutComposition;
 };
 
 /**
  * nsITextInputProcessorCallback is a callback interface for JS to implement
  * IME.  IME implemented by JS can implement onNotify() function and must send
  * it to nsITextInputProcessor at initializing.  Then, onNotify() will be
  * called with nsITextInputProcessorNotification instance.
  * The reason why onNotify() uses string simply is that if we will support