Clean cherry-pick: Handle deleting non-single-word characters at end of input.

BUG:17587083

Original description:

Handle deleting non-single-word characters at end of input.  This includes both multi-word code-points and compositions of multiple single-word code-points.

A "backspace" from Android IME will send 1,0 as parameters to extendSelectionAndDelete(...) but it's possible that the last character of the input is a multi-word code and take multiple positions.

The setSelectedRange() call made by setSelectionOffsets() will _reduce_ the range to include only full characters and can end up reducing the range all the way to zero and causing the deleteSelection() call to do nothing.

BUG=355995
Committed: https://src.chromium.org/viewvc/blink?view=rev&revision=176961

Change-Id: Iedb4b749551fcd037269eac989e3c97d242b3eb9
diff --git a/Source/core/core.gypi b/Source/core/core.gypi
index c181832..1066c6d 100644
--- a/Source/core/core.gypi
+++ b/Source/core/core.gypi
@@ -3414,6 +3414,7 @@
             'dom/RangeTest.cpp',
             'dom/TreeScopeTest.cpp',
             'editing/CompositionUnderlineRangeFilterTest.cpp',
+            'editing/InputMethodControllerTest.cpp',
             'editing/SurroundingTextTest.cpp',
             'editing/TextIteratorTest.cpp',
             'editing/VisibleSelectionTest.cpp',
diff --git a/Source/core/editing/InputMethodController.cpp b/Source/core/editing/InputMethodController.cpp
index 3e6b85e..793b04b 100644
--- a/Source/core/editing/InputMethodController.cpp
+++ b/Source/core/editing/InputMethodController.cpp
@@ -401,7 +401,24 @@
     PlainTextRange selectionOffsets(getSelectionOffsets());
     if (selectionOffsets.isNull())
         return;
-    setSelectionOffsets(PlainTextRange(std::max(static_cast<int>(selectionOffsets.start()) - before, 0), selectionOffsets.end() + after));
+
+    // A common call of before=1 and after=0 will fail if the last character
+    // is multi-code-word UTF-16, including both multi-16bit code-points and
+    // Unicode combining character sequences of multiple single-16bit code-
+    // points (officially called "compositions"). Try more until success.
+    // http://crbug.com/355995
+    //
+    // FIXME: Note that this is not an ideal solution when this function is
+    // called to implement "backspace". In that case, there should be some call
+    // that will not delete a full multi-code-point composition but rather
+    // only the last code-point so that it's possible for a user to correct
+    // a composition without starting it from the beginning.
+    // http://crbug.com/37993
+    do {
+        if (!setSelectionOffsets(PlainTextRange(std::max(static_cast<int>(selectionOffsets.start()) - before, 0), selectionOffsets.end() + after)))
+            return;
+        ++before;
+    } while (m_frame.selection().start() == m_frame.selection().end() && before <= static_cast<int>(selectionOffsets.start()));
     TypingCommand::deleteSelection(*m_frame.document());
 }
 
diff --git a/Source/core/editing/InputMethodControllerTest.cpp b/Source/core/editing/InputMethodControllerTest.cpp
new file mode 100644
index 0000000..73cfd37
--- /dev/null
+++ b/Source/core/editing/InputMethodControllerTest.cpp
@@ -0,0 +1,71 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "config.h"
+#include "core/editing/InputMethodController.h"
+
+#include "core/frame/LocalFrame.h"
+#include "core/html/HTMLDocument.h"
+#include "core/html/HTMLInputElement.h"
+#include "core/testing/DummyPageHolder.h"
+#include <gtest/gtest.h>
+
+using namespace blink;
+using namespace WebCore;
+
+namespace {
+
+class InputMethodControllerTest : public ::testing::Test {
+protected:
+    InputMethodController& controller() { return frame().inputMethodController(); }
+    HTMLDocument& document() const { return *m_document; }
+    LocalFrame& frame() const { return m_dummyPageHolder->frame(); }
+
+private:
+    virtual void SetUp() OVERRIDE;
+
+    OwnPtr<DummyPageHolder> m_dummyPageHolder;
+    HTMLDocument* m_document;
+};
+
+void InputMethodControllerTest::SetUp()
+{
+    m_dummyPageHolder = DummyPageHolder::create(IntSize(800, 600));
+    m_document = toHTMLDocument(&m_dummyPageHolder->document());
+    ASSERT(m_document);
+}
+
+TEST_F(InputMethodControllerTest, BackspaceFromEndOfInput)
+{
+    document().write("<input id='sample'>");
+    HTMLInputElement* input = toHTMLInputElement(document().getElementById("sample"));
+    document().updateLayout();
+    input->focus();
+
+    input->setValue("fooX");
+    controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
+    EXPECT_STREQ("fooX", input->value().utf8().data());
+    controller().extendSelectionAndDelete(1, 0);
+    EXPECT_STREQ("foo", input->value().utf8().data());
+
+    input->setValue(String::fromUTF8("foo\xE2\x98\x85")); // U+2605 == "black star"
+    controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
+    EXPECT_STREQ("foo\xE2\x98\x85", input->value().utf8().data());
+    controller().extendSelectionAndDelete(1, 0);
+    EXPECT_STREQ("foo", input->value().utf8().data());
+
+    input->setValue(String::fromUTF8("foo\xF0\x9F\x8F\x86")); // U+1F3C6 == "trophy"
+    controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
+    EXPECT_STREQ("foo\xF0\x9F\x8F\x86", input->value().utf8().data());
+    controller().extendSelectionAndDelete(1, 0);
+    EXPECT_STREQ("foo", input->value().utf8().data());
+
+    input->setValue(String::fromUTF8("foo\xE0\xB8\x81\xE0\xB9\x89")); // composed U+0E01 "ka kai" + U+0E49 "mai tho"
+    controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
+    EXPECT_STREQ("foo\xE0\xB8\x81\xE0\xB9\x89", input->value().utf8().data());
+    controller().extendSelectionAndDelete(1, 0);
+    EXPECT_STREQ("foo", input->value().utf8().data());
+}
+
+} // namespace