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