Bug 1217700 part.3 Expose text change, selection change and position change notifications to nsITextInputProcessorCallback with nsITextInputProcessorNotification r?smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 20 Apr 2017 20:17:03 +0900
changeset 566205 2a41d150ac17e28906d6396f099e92f4be5c7985
parent 566204 0d37197143e56a240893e9f7421841f5b1097d75
child 566206 b16dc026e2da2d68fed7db366e49ee26f4d2d64f
push id55137
push usermasayuki@d-toybox.com
push dateFri, 21 Apr 2017 05:00:08 +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