Merge m-i to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 12 Dec 2015 16:53:04 -0800
changeset 310399 e85484f3cd5115af9743973a49cb69641b497b84
parent 310386 cdcd33fd6e39cd12feb5bb11951e1c981a04bd86 (current diff)
parent 310398 d68d7fa71614637287ff71c3ba0f98b5d550a30f (diff)
child 310400 f07e71078bc8991f74c2101944c8f869c77f442a
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone45.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
Merge m-i to m-c, a=merge
dom/interfaces/push/nsIPushClient.idl
dom/interfaces/push/nsIPushNotificationService.idl
dom/push/PushClient.js
dom/push/PushNotificationService.js
--- a/gfx/tests/crashtests/crashtests.list
+++ b/gfx/tests/crashtests/crashtests.list
@@ -7,17 +7,17 @@ load 248518-1.html
 load 306649-1.xml
 load 306902-1.xml
 load 333861-1.html
 load 334735-1.html
 load 345576-1.html
 load 345629-1.html
 load 348462-1.html
 load 348462-2.html
-load 358732-1.xhtml
+skip load 358732-1.xhtml # Bug 1226751
 load 358732-2.svg
 load 358732-3.html
 load 366643.html
 load 369688-1.html
 load 369947-1.html
 load 372094-1.xhtml
 load 376627-1.html
 load 377231-1.html
@@ -49,17 +49,17 @@ load 398042-1.xhtml
 load 398042-2.xhtml
 load 402307-1.html
 load 403464-1.html
 load 404112-1.html
 load 404112-2.html
 load 405268-1.xhtml
 load 407761-1.html
 load 407842.html
-asserts-if(Android,1) load 408754-1.html
+load 408754-1.html
 load 410728-1.xml
 load 416637-1.html
 load 419095-1.html
 load 419255-1.html
 load 420945-1.html
 load 420962-1.html
 load 421393-1.html
 load 421813-1.html
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
@@ -963,20 +963,24 @@ final class GeckoEditable extends JNIObj
             @Override
             public void run() {
                 mListener.onSelectionChange(newStart, newEnd);
             }
         });
     }
 
     private void geckoReplaceText(int start, int oldEnd, CharSequence newText) {
-        // Don't use replace() because Gingerbread has a bug where if the replaced text
-        // has the same spans as the original text, the spans will end up being deleted
-        mText.delete(start, oldEnd);
-        mText.insert(start, newText);
+        if (AppConstants.Versions.preHC) {
+            // Don't use replace() because Gingerbread has a bug where if the replaced text
+            // has the same spans as the original text, the spans will end up being deleted
+            mText.delete(start, oldEnd);
+            mText.insert(start, newText);
+        } else {
+            mText.replace(start, oldEnd, newText);
+        }
     }
 
     private boolean isSameText(int start, int oldEnd, CharSequence newText) {
         return oldEnd - start == newText.length() &&
                TextUtils.regionMatches(mText, start, newText, 0, oldEnd - start);
     }
 
     @WrapForJNI @Override
@@ -996,84 +1000,65 @@ final class GeckoEditable extends JNIObj
             Log.e(LOGTAG, "invalid text notification range: " +
                   start + " to " + unboundedOldEnd);
             throw new IllegalArgumentException("invalid text notification range");
         }
         /* For the "end" parameters, Gecko can pass in a large
            number to denote "end of the text". Fix that here */
         final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd;
         // new end should always match text
-        if (start != 0 && unboundedNewEnd != (start + text.length())) {
+        if (unboundedOldEnd <= mText.length() &&
+                unboundedNewEnd != (start + text.length())) {
             Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
                   (start + text.length()));
             throw new IllegalArgumentException("newEnd does not match text");
         }
         final int newEnd = start + text.length();
         final Action action = mActionQueue.peek();
 
         if (action != null && action.mType == Action.TYPE_ACKNOWLEDGE_FOCUS) {
             // Simply replace the text for newly-focused editors.
             mText.replace(0, mText.length(), text);
 
-        } else {
-            mChangedText.clearSpans();
-            mChangedText.replace(0, mChangedText.length(), text);
-            // Preserve as many spans as possible
-            TextUtils.copySpansFrom(mText, start, Math.min(oldEnd, newEnd),
-                                    Object.class, mChangedText, 0);
+        } else if (action != null &&
+                action.mType == Action.TYPE_REPLACE_TEXT &&
+                start <= action.mStart &&
+                oldEnd >= action.mEnd &&
+                newEnd >= action.mStart + action.mSequence.length()) {
 
-            if (action != null &&
-                    action.mType == Action.TYPE_REPLACE_TEXT &&
-                    start <= action.mStart &&
-                    action.mStart + action.mSequence.length() <= newEnd) {
-
-                // actionNewEnd is the new end of the original replacement action
-                final int actionNewEnd = action.mStart + action.mSequence.length();
-                int selStart = Selection.getSelectionStart(mText);
-                int selEnd = Selection.getSelectionEnd(mText);
-
-                // Replace old spans with new spans
-                mChangedText.replace(action.mStart - start, actionNewEnd - start,
-                                     action.mSequence);
-                geckoReplaceText(start, oldEnd, mChangedText);
+            // actionNewEnd is the new end of the original replacement action
+            final int actionNewEnd = action.mStart + action.mSequence.length();
 
-                // delete/insert above might have moved our selection to somewhere else
-                // this happens when the Gecko text change covers a larger range than
-                // the original replacement action. Fix selection here
-                if (selStart >= start && selStart <= oldEnd) {
-                    selStart = selStart < action.mStart ? selStart :
-                               selStart < action.mEnd   ? actionNewEnd :
-                                                          selStart + actionNewEnd - action.mEnd;
-                    mText.setSpan(Selection.SELECTION_START, selStart, selStart,
-                                  Spanned.SPAN_POINT_POINT);
-                }
-                if (selEnd >= start && selEnd <= oldEnd) {
-                    selEnd = selEnd < action.mStart ? selEnd :
-                             selEnd < action.mEnd   ? actionNewEnd :
-                                                      selEnd + actionNewEnd - action.mEnd;
-                    mText.setSpan(Selection.SELECTION_END, selEnd, selEnd,
-                                  Spanned.SPAN_POINT_POINT);
-                }
-
-                // Ignore the next selection change because the selection change is a
-                // side-effect of the replace-text event we sent.
-                mIgnoreSelectionChange = true;
-
+            // Replace old spans with new spans
+            if (start == action.mStart && oldEnd == action.mEnd && newEnd == actionNewEnd) {
+                // The new text exactly matches our sequence, so do a direct replace.
+                geckoReplaceText(start, oldEnd, action.mSequence);
             } else {
-                // Gecko side initiated the text change.
-                if (isSameText(start, oldEnd, mChangedText)) {
-                    // Nothing to do because the text is the same. This could happen when
-                    // the composition is updated for example. Ignore the next selection
-                    // change because the selection change is a side-effect of the
-                    // update-composition event we sent.
-                    mIgnoreSelectionChange = true;
-                    return;
-                }
+                mChangedText.clearSpans();
+                mChangedText.replace(0, mChangedText.length(), mText, start, oldEnd);
+                mChangedText.replace(action.mStart - start, action.mEnd - start, action.mSequence);
                 geckoReplaceText(start, oldEnd, mChangedText);
             }
+
+            // Ignore the next selection change because the selection change is a
+            // side-effect of the replace-text event we sent.
+            mIgnoreSelectionChange = true;
+
+        } else {
+            // Gecko side initiated the text change.
+            if (isSameText(start, oldEnd, text)) {
+                // Nothing to do because the text is the same. This could happen when
+                // the composition is updated for example. Ignore the next selection
+                // change because the selection change is a side-effect of the
+                // update-composition event we sent.
+                mIgnoreSelectionChange = true;
+                return;
+            }
+
+            geckoReplaceText(start, oldEnd, text);
         }
 
         geckoPostToIc(new Runnable() {
             @Override
             public void run() {
                 mListener.onTextChange(text, start, oldEnd, newEnd);
             }
         });
--- a/mobile/android/tests/browser/robocop/robocop_head.js
+++ b/mobile/android/tests/browser/robocop/robocop_head.js
@@ -1,14 +1,28 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// The test js is shared between sandboxed (which has no SpecialPowers object)
+// and content mochitests (where the |Components| object is accessible only as
+// SpecialPowers.Components). Expose Components if necessary here to make things
+// work everywhere.
+//
+// Even if the real |Components| doesn't exist, we might shim in a simple JS
+// placebo for compat. An easy way to differentiate this from the real thing
+// is whether the property is read-only or not.
+{
+  let c = Object.getOwnPropertyDescriptor(this, 'Components');
+  if ((!c.value || c.writable) && typeof SpecialPowers === 'object')
+    Components = SpecialPowers.wrap(SpecialPowers.Components);
+}
+
 /*
  * This file contains common code that is loaded before each test file(s).
  * See http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests
  * for more information.
  */
 
 var _quit = false;
 var _tests_pending = 0;
--- a/mobile/android/tests/browser/robocop/robocop_input.html
+++ b/mobile/android/tests/browser/robocop/robocop_input.html
@@ -1,25 +1,49 @@
 <!DOCTYPE html>
 <html>
   <head>
     <title>Robocop Input</title>
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
   </head>
-  <body onload="fillInput()">
+  <body>
     <p><input id="input" type="text"></p>
     <p><input id="resetting-input" type="text"></p>
-    <script>
-      function fillInput() {
-        // fill the input with the URL hash if provided
-        var input = document.getElementById("input");
-        input.value = window.location.hash.slice(1); // remove leading #
-        input.focus();
+    <script type="application/javascript;version=1.8" src="robocop_head.js"></script>
+    <script type="application/javascript;version=1.8">
+      let input = document.getElementById("input");
+
+      // An input that resets the editor on every input by resetting the value property.
+      let resetting_input = document.getElementById("resetting-input");
+      resetting_input.addEventListener('input', function () {
+          this.value = this.value;
+      });
+
+      let test = {
+        focus_input: function(val) {
+          input.value = val;
+          input.focus();
+        },
 
-        // An input that resets the editor on every input by resetting the value property.
-        var resetting_input = document.getElementById("resetting-input");
-        resetting_input.addEventListener('input', function () {
-            this.value = this.value;
-        });
-      }
+        test_reflush_changes: function() {
+          let inputEditable = SpecialPowers.wrap(input).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
+          let inputIme = inputEditable.editor.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
+          do_check_true(inputIme.composing);
+
+          // Ending the composition then setting the input value triggers the bug.
+          inputIme.forceCompositionEnd();
+          input.value = "good";
+        },
+
+        focus_resetting_input: function(val) {
+          resetting_input.value = val;
+          resetting_input.focus();
+        },
+
+        finish_test: function() {
+          java.disconnect();
+        },
+      };
+
+      var java = new JavaBridge(test);
     </script>
   </body>
 </html>
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
@@ -10,45 +10,66 @@ import static org.mozilla.gecko.tests.he
 import static org.mozilla.gecko.tests.helpers.TextInputHelper.assertText;
 import static org.mozilla.gecko.tests.helpers.TextInputHelper.assertTextAndSelection;
 import static org.mozilla.gecko.tests.helpers.TextInputHelper.assertTextAndSelectionAt;
 import static org.mozilla.gecko.tests.helpers.TextInputHelper.getText;
 import static org.mozilla.gecko.tests.helpers.WaitHelper.waitFor;
 
 import org.mozilla.gecko.tests.components.GeckoViewComponent.InputConnectionTest;
 import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.JavascriptBridge;
 import org.mozilla.gecko.tests.helpers.NavigationHelper;
 
 import com.jayway.android.robotium.solo.Condition;
 
 import android.view.KeyEvent;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 
 /**
  * Tests the proper operation of GeckoInputConnection
  */
 public class testInputConnection extends UITest {
 
     private static final String INITIAL_TEXT = "foo";
 
+    private JavascriptBridge js;
+
+    @Override // UITest
+    public void setUp() throws Exception {
+        super.setUp();
+        js = new JavascriptBridge(this);
+    }
+
+    @Override // UITest
+    public void tearDown() throws Exception {
+        js.disconnect();
+        super.tearDown();
+    }
+
     public void testInputConnection() throws InterruptedException {
         GeckoHelper.blockForReady();
 
-        final String url = mStringHelper.ROBOCOP_INPUT_URL + "#" + INITIAL_TEXT;
+        final String url = mStringHelper.ROBOCOP_INPUT_URL;
         NavigationHelper.enterAndLoadUrl(url);
         mToolbar.assertTitle(url);
 
+        // First run tests inside the normal input field.
+        js.syncCall("focus_input", INITIAL_TEXT);
         mGeckoView.mTextInput
             .waitForInputConnection()
-            // First run tests inside the normal input field.
-            .testInputConnection(new BasicInputConnectionTest())
-            // Then switch focus to the resetting input field, and run tests there.
-            .testInputConnection(new FocusNextInputFieldTest())
+            .testInputConnection(new BasicInputConnectionTest());
+
+        // Then switch focus to the resetting input field, and run tests there.
+        js.syncCall("focus_resetting_input", "");
+        mGeckoView.mTextInput
+            .waitForInputConnection()
             .testInputConnection(new ResettingInputConnectionTest());
+
+        js.syncCall("finish_test");
     }
 
     private class BasicInputConnectionTest extends InputConnectionTest {
         @Override
         public void test(InputConnection ic, EditorInfo info) {
             // Test initial text provided by the hash in the test page URL
             assertText("Initial text matches URL hash", ic, INITIAL_TEXT);
 
@@ -152,45 +173,35 @@ public class testInputConnection extends
             assertTextAndSelectionAt("Can clear text", ic, "", 0);
 
             // Bug 1209465, cannot enter ideographic space character by itself (U+3000).
             ic.commitText("\u3000", 1);
             assertTextAndSelectionAt("Can commit ideographic space", ic, "\u3000", 1);
 
             ic.deleteSurroundingText(1, 0);
             assertTextAndSelectionAt("Can clear text", ic, "", 0);
-        }
-    }
 
-    /**
-     * FocusNextInputFieldTest is used to switch focus from one input field to
-     * another on the test page by sending a tab key.
-     */
-    private class FocusNextInputFieldTest extends InputConnectionTest {
-        @Override
-        public void test(final InputConnection ic, EditorInfo info) {
-            // First clear all text.
-            ic.setSelection(0, 0);
-            assertSelectionAt("Can set selection to start", ic, 0);
+            // Bug 1051556, exception due to committing text changes during flushing.
+            ic.setComposingText("bad", 1);
+            assertTextAndSelectionAt("Can set the composing text", ic, "bad", 3);
+            js.asyncCall("test_reflush_changes");
+            // Wait for text change notifications to come in.
+            processGeckoEvents(ic);
+            assertTextAndSelectionAt("Can re-flush text changes", ic, "good", 4);
+            ic.setComposingText("done", 1);
+            assertTextAndSelectionAt("Can update composition after re-flushing", ic, "done", 4);
+            ic.finishComposingText();
+            assertTextAndSelectionAt("Can finish composing text", ic, "done", 4);
 
-            ic.deleteSurroundingText(0, Integer.MAX_VALUE);
-            assertTextAndSelectionAt("Can clear all text", ic, "", 0);
+            ic.deleteSurroundingText(4, 0);
+            assertTextAndSelectionAt("Can clear text", ic, "", 0);
 
-            // Set dummy text in currently focused input so we can check when we have switched focus.
-            final String dummyText = "dummy switch input text";
-            ic.commitText(dummyText, 1);
-            assertTextAndSelectionAt("Can commit text", ic, dummyText, dummyText.length());
-
-            // Finish processing events from the old input field.
+            // Make sure we don't leave behind stale events for the following test.
             processGeckoEvents(ic);
             processInputConnectionEvents();
-
-            final KeyEvent tabKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB);
-            ic.sendKeyEvent(tabKey);
-            ic.sendKeyEvent(KeyEvent.changeAction(tabKey, KeyEvent.ACTION_UP));
         }
     }
 
     /**
      * ResettingInputConnectionTest performs tests on the resetting input in
      * robocop_input.html. Any test that uses the normal input should be put in
      * BasicInputConnectionTest.
      */
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -236,16 +236,17 @@ public:
     }
 
     Natives(nsWindow* aWindow)
         : window(*aWindow)
         , mIMERanges(new TextRangeArray())
         , mIMEMaskEventsCount(1) // Mask IME events since there's no focus yet
         , mIMEUpdatingContext(false)
         , mIMESelectionChanged(false)
+        , mIMETextChangedDuringFlush(false)
     {}
 
     ~Natives();
 
     using Base::DisposeNative;
     using EditableBase::DisposeNative;
 
     /**
@@ -311,21 +312,27 @@ private:
     mozilla::widget::GeckoEditable::GlobalRef mEditable;
     nsAutoTArray<mozilla::UniquePtr<mozilla::WidgetEvent>, 8> mIMEKeyEvents;
     nsAutoTArray<IMETextChange, 4> mIMETextChanges;
     InputContext mInputContext;
     RefPtr<mozilla::TextRangeArray> mIMERanges;
     int32_t mIMEMaskEventsCount; // Mask events when > 0
     bool mIMEUpdatingContext;
     bool mIMESelectionChanged;
+    bool mIMETextChangedDuringFlush;
 
     void SendIMEDummyKeyEvents();
     void AddIMETextChange(const IMETextChange& aChange);
-    void PostFlushIMEChanges();
-    void FlushIMEChanges();
+
+    enum FlushChangesFlag {
+        FLUSH_FLAG_NONE,
+        FLUSH_FLAG_RETRY
+    };
+    void PostFlushIMEChanges(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
+    void FlushIMEChanges(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
 
 public:
     bool NotifyIME(const IMENotification& aIMENotification);
     void SetInputContext(const InputContext& aContext,
                          const InputContextAction& aAction);
     InputContext GetInputContext();
 
     // Handle an Android KeyEvent.
@@ -1976,16 +1983,20 @@ nsWindow::Natives::SendIMEDummyKeyEvents
     window.DispatchEvent(&upEvent);
 }
 
 void
 nsWindow::Natives::AddIMETextChange(const IMETextChange& aChange)
 {
     mIMETextChanges.AppendElement(aChange);
 
+    // We may not be in the middle of flushing,
+    // in which case this flag is meaningless.
+    mIMETextChangedDuringFlush = true;
+
     // Now that we added a new range we need to go back and
     // update all the ranges before that.
     // Ranges that have offsets which follow this new range
     // need to be updated to reflect new offsets
     const int32_t delta = aChange.mNewEnd - aChange.mOldEnd;
     for (int32_t i = mIMETextChanges.Length() - 2; i >= 0; i--) {
         IMETextChange& previousChange = mIMETextChanges[i];
         if (previousChange.mStart > aChange.mOldEnd) {
@@ -2034,85 +2045,116 @@ nsWindow::Natives::AddIMETextChange(cons
         mIMETextChanges.RemoveElementAt(srcIndex);
         // Any ranges that we skip over between src and dst are not mergeable
         // so we can safely continue the merge starting at dst
         srcIndex = dstIndex;
     }
 }
 
 void
-nsWindow::Natives::PostFlushIMEChanges()
+nsWindow::Natives::PostFlushIMEChanges(FlushChangesFlag aFlags)
 {
-    if (!mIMETextChanges.IsEmpty() || mIMESelectionChanged) {
+    if (aFlags != FLUSH_FLAG_RETRY &&
+            (!mIMETextChanges.IsEmpty() || mIMESelectionChanged)) {
         // Already posted
         return;
     }
 
     // Keep a strong reference to the window to keep 'this' alive.
     RefPtr<nsWindow> window(&this->window);
 
-    nsAppShell::gAppShell->PostEvent([this, window] {
+    nsAppShell::gAppShell->PostEvent([this, window, aFlags] {
         if (!window->Destroyed()) {
-            FlushIMEChanges();
+            FlushIMEChanges(aFlags);
         }
     });
 }
 
 void
-nsWindow::Natives::FlushIMEChanges()
+nsWindow::Natives::FlushIMEChanges(FlushChangesFlag aFlags)
 {
     // Only send change notifications if we are *not* masking events,
     // i.e. if we have a focused editor,
     NS_ENSURE_TRUE_VOID(!mIMEMaskEventsCount);
 
     nsCOMPtr<nsISelection> imeSelection;
     nsCOMPtr<nsIContent> imeRoot;
 
     // If we are receiving notifications, we must have selection/root content.
     MOZ_ALWAYS_TRUE(NS_SUCCEEDED(IMEStateManager::GetFocusSelectionAndRoot(
             getter_AddRefs(imeSelection), getter_AddRefs(imeRoot))));
 
     RefPtr<nsWindow> kungFuDeathGrip(&window);
     window.UserActivity();
 
-    for (uint32_t i = 0; i < mIMETextChanges.Length(); i++) {
-        IMETextChange &change = mIMETextChanges[i];
-
+    struct TextRecord {
+        nsString text;
+        int32_t start;
+        int32_t oldEnd;
+        int32_t newEnd;
+    };
+    nsAutoTArray<TextRecord, 4> textTransaction;
+    if (mIMETextChanges.Length() > textTransaction.Capacity()) {
+        textTransaction.SetCapacity(mIMETextChanges.Length());
+    }
+
+    mIMETextChangedDuringFlush = false;
+
+    for (const IMETextChange &change : mIMETextChanges) {
         if (change.mStart == change.mOldEnd &&
                 change.mStart == change.mNewEnd) {
             continue;
         }
 
         WidgetQueryContentEvent event(true, eQueryTextContent, &window);
 
         if (change.mNewEnd != change.mStart) {
             window.InitEvent(event, nullptr);
             event.InitForQueryTextContent(change.mStart,
                                           change.mNewEnd - change.mStart);
             window.DispatchEvent(&event);
             NS_ENSURE_TRUE_VOID(event.mSucceeded);
             NS_ENSURE_TRUE_VOID(event.mReply.mContentsRoot == imeRoot.get());
         }
 
-        mEditable->OnTextChange(event.mReply.mString, change.mStart,
-                                change.mOldEnd, change.mNewEnd);
+        if (mIMETextChangedDuringFlush) {
+            // The query event above could have triggered more text changes to
+            // come in, as indicated by our flag. If that happens, try flushing
+            // IME changes again later.
+            if (!NS_WARN_IF(aFlags == FLUSH_FLAG_RETRY)) {
+                // Don't retry if already retrying, to avoid infinite loops.
+                PostFlushIMEChanges(FLUSH_FLAG_RETRY);
+            }
+            return;
+        }
+
+        textTransaction.AppendElement(
+                TextRecord{event.mReply.mString, change.mStart,
+                           change.mOldEnd, change.mNewEnd});
     }
+
     mIMETextChanges.Clear();
 
+    for (const TextRecord& record : textTransaction) {
+        mEditable->OnTextChange(record.text, record.start,
+                                record.oldEnd, record.newEnd);
+    }
+
     if (mIMESelectionChanged) {
+        mIMESelectionChanged = false;
+
         WidgetQueryContentEvent event(true, eQuerySelectedText, &window);
         window.InitEvent(event, nullptr);
         window.DispatchEvent(&event);
 
         NS_ENSURE_TRUE_VOID(event.mSucceeded);
         NS_ENSURE_TRUE_VOID(event.mReply.mContentsRoot == imeRoot.get());
 
         mEditable->OnSelectionChange(int32_t(event.GetSelectionStart()),
                                      int32_t(event.GetSelectionEnd()));
-        mIMESelectionChanged = false;
     }
 }
 
 bool
 nsWindow::Natives::NotifyIME(const IMENotification& aIMENotification)
 {
     MOZ_ASSERT(mEditable);