Implement surrounding text API for exo::TextInput

Bug: 826614
Test: exo_unittests
Change-Id: Ia2fe9a39c38b0ea93d1f0a0f5aa820536a656a08
Reviewed-on: https://chromium-review.googlesource.com/c/1399722
Commit-Queue: Jun Mukai <mukai@chromium.org>
Reviewed-by: Mitsuru Oshima <oshima@chromium.org>
Reviewed-by: Yuichiro Hanada <yhanada@chromium.org>
Cr-Commit-Position: refs/heads/master@{#629807}
diff --git a/components/exo/text_input.cc b/components/exo/text_input.cc
index f2534c4c..02fb7dcd 100644
--- a/components/exo/text_input.cc
+++ b/components/exo/text_input.cc
@@ -4,6 +4,9 @@
 
 #include "components/exo/text_input.h"
 
+#include <algorithm>
+
+#include "base/strings/utf_string_conversions.h"
 #include "components/exo/surface.h"
 #include "components/exo/wm_helper.h"
 #include "third_party/icu/source/common/unicode/uchar.h"
@@ -25,6 +28,14 @@
 
 }  // namespace
 
+size_t OffsetFromUTF8Offset(const base::StringPiece& text, uint32_t offset) {
+  return base::UTF8ToUTF16(text.substr(0, offset)).size();
+}
+
+size_t OffsetFromUTF16Offset(const base::StringPiece16& text, uint32_t offset) {
+  return base::UTF16ToUTF8(text.substr(0, offset)).size();
+}
+
 TextInput::TextInput(std::unique_ptr<Delegate> delegate)
     : delegate_(std::move(delegate)) {}
 
@@ -69,8 +80,14 @@
 }
 
 void TextInput::SetSurroundingText(const base::string16& text,
-                                   uint32_t cursor_pos) {
-  NOTIMPLEMENTED();
+                                   uint32_t cursor_pos,
+                                   uint32_t anchor) {
+  surrounding_text_ = text;
+  cursor_pos_ = gfx::Range(cursor_pos);
+  if (anchor < cursor_pos)
+    cursor_pos_->set_start(anchor);
+  else
+    cursor_pos_->set_end(anchor);
 }
 
 void TextInput::SetTypeModeFlags(ui::TextInputType type,
@@ -163,37 +180,87 @@
 }
 
 bool TextInput::GetTextRange(gfx::Range* range) const {
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  if (!cursor_pos_)
+    return false;
+  range->set_start(0);
+  if (composition_.text.empty()) {
+    range->set_end(surrounding_text_.size());
+  } else {
+    range->set_end(surrounding_text_.size() - cursor_pos_->length() +
+                   composition_.text.size());
+  }
+  return true;
 }
 
 bool TextInput::GetCompositionTextRange(gfx::Range* range) const {
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  if (!cursor_pos_ || composition_.text.empty())
+    return false;
+
+  range->set_start(cursor_pos_->start());
+  range->set_end(cursor_pos_->start() + composition_.text.size());
+  return true;
 }
 
 bool TextInput::GetEditableSelectionRange(gfx::Range* range) const {
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  if (!cursor_pos_)
+    return false;
+  range->set_start(cursor_pos_->start());
+  range->set_end(cursor_pos_->end());
+  return true;
 }
 
 bool TextInput::SetEditableSelectionRange(const gfx::Range& range) {
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  if (surrounding_text_.size() < range.GetMax())
+    return false;
+  delegate_->SetCursor(
+      gfx::Range(OffsetFromUTF16Offset(surrounding_text_, range.start()),
+                 OffsetFromUTF16Offset(surrounding_text_, range.end())));
+  return true;
 }
 
 bool TextInput::DeleteRange(const gfx::Range& range) {
-  // TODO(mukai): call delegate_->DeleteSurroundingText(range) once it's
-  // supported.
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  if (surrounding_text_.size() < range.GetMax())
+    return false;
+  delegate_->DeleteSurroundingText(
+      gfx::Range(OffsetFromUTF16Offset(surrounding_text_, range.start()),
+                 OffsetFromUTF16Offset(surrounding_text_, range.end())));
+  return true;
 }
 
 bool TextInput::GetTextFromRange(const gfx::Range& range,
                                  base::string16* text) const {
-  // TODO(mukai): support of surrounding text.
-  NOTIMPLEMENTED_LOG_ONCE();
-  return false;
+  gfx::Range text_range;
+  if (!GetTextRange(&text_range) || !text_range.Contains(range))
+    return false;
+  if (composition_.text.empty() || range.GetMax() <= cursor_pos_->GetMin()) {
+    text->assign(surrounding_text_, range.GetMin(), range.length());
+    return true;
+  }
+  size_t composition_end = cursor_pos_->GetMin() + composition_.text.size();
+  if (range.GetMin() >= composition_end) {
+    size_t start =
+        range.GetMin() - composition_.text.size() + cursor_pos_->length();
+    text->assign(surrounding_text_, start, range.length());
+    return true;
+  }
+
+  size_t start_in_composition = 0;
+  if (range.GetMin() <= cursor_pos_->GetMin()) {
+    text->assign(surrounding_text_, range.GetMin(),
+                 cursor_pos_->GetMin() - range.GetMin());
+  } else {
+    start_in_composition = range.GetMin() - cursor_pos_->GetMin();
+  }
+  if (range.GetMax() <= composition_end) {
+    text->append(composition_.text, start_in_composition,
+                 range.GetMax() - cursor_pos_->GetMin() - start_in_composition);
+  } else {
+    text->append(composition_.text, start_in_composition,
+                 composition_.text.size() - start_in_composition);
+    text->append(surrounding_text_, cursor_pos_->GetMax(),
+                 range.GetMax() - composition_end);
+  }
+  return true;
 }
 
 void TextInput::OnInputMethodChanged() {
@@ -214,7 +281,17 @@
   return true;
 }
 
-void TextInput::ExtendSelectionAndDelete(size_t before, size_t after) {}
+void TextInput::ExtendSelectionAndDelete(size_t before, size_t after) {
+  if (!cursor_pos_)
+    return;
+  uint32_t start =
+      (cursor_pos_->GetMin() < before) ? 0 : (cursor_pos_->GetMin() - before);
+  uint32_t end =
+      std::min(cursor_pos_->GetMax() + after, surrounding_text_.size());
+  delegate_->DeleteSurroundingText(
+      gfx::Range(OffsetFromUTF16Offset(surrounding_text_, start),
+                 OffsetFromUTF16Offset(surrounding_text_, end)));
+}
 
 void TextInput::EnsureCaretNotInRect(const gfx::Rect& rect) {}
 
diff --git a/components/exo/text_input.h b/components/exo/text_input.h
index 4a47440..b00873a 100644
--- a/components/exo/text_input.h
+++ b/components/exo/text_input.h
@@ -23,6 +23,9 @@
 namespace exo {
 class Surface;
 
+size_t OffsetFromUTF8Offset(const base::StringPiece& text, uint32_t offset);
+size_t OffsetFromUTF16Offset(const base::StringPiece16& text, uint32_t offset);
+
 // This class bridges the ChromeOS input method and a text-input context.
 class TextInput : public ui::TextInputClient,
                   public keyboard::KeyboardControllerObserver {
@@ -47,10 +50,11 @@
     // Commit |text| to the current text input session.
     virtual void Commit(const base::string16& text) = 0;
 
-    // Set the cursor position.
+    // Set the cursor position. The range should be in bytes offset.
     virtual void SetCursor(const gfx::Range& selection) = 0;
 
-    // Delete the surrounding text of the current text input.
+    // Delete the surrounding text of the current text input. The range should
+    // be in the bytes offset.
     virtual void DeleteSurroundingText(const gfx::Range& range) = 0;
 
     // Sends a key event.
@@ -83,7 +87,9 @@
   void Resync();
 
   // Sets the surrounding text in the app.
-  void SetSurroundingText(const base::string16& text, uint32_t cursor_pos);
+  void SetSurroundingText(const base::string16& text,
+                          uint32_t cursor_pos,
+                          uint32_t anchor);
 
   // Sets the text input type, mode, flags, and |should_do_learning|.
   void SetTypeModeFlags(ui::TextInputType type,
@@ -148,6 +154,8 @@
   int flags_ = ui::TEXT_INPUT_FLAG_NONE;
   bool should_do_learning_ = true;
   ui::CompositionText composition_;
+  base::string16 surrounding_text_;
+  base::Optional<gfx::Range> cursor_pos_;
   base::i18n::TextDirection direction_ = base::i18n::UNKNOWN_DIRECTION;
 
   DISALLOW_COPY_AND_ASSIGN(TextInput);
diff --git a/components/exo/text_input_unittest.cc b/components/exo/text_input_unittest.cc
index 2aa5e1a6..3dc0ed5 100644
--- a/components/exo/text_input_unittest.cc
+++ b/components/exo/text_input_unittest.cc
@@ -4,6 +4,8 @@
 
 #include "components/exo/text_input.h"
 
+#include <string>
+
 #include "base/strings/utf_string_conversions.h"
 #include "components/exo/buffer.h"
 #include "components/exo/shell_surface.h"
@@ -117,6 +119,16 @@
     return surface_->window()->GetHost()->GetInputMethod();
   }
 
+  void SetCompositionText(const std::string& utf8) {
+    ui::CompositionText t;
+    t.text = base::UTF8ToUTF16(utf8);
+    t.selection = gfx::Range(1u);
+    t.ime_text_spans.push_back(
+        ui::ImeTextSpan(0, t.text.size(), ui::ImeTextSpan::Thickness::kThick));
+    EXPECT_CALL(*delegate(), SetCompositionText(t)).Times(1);
+    text_input()->SetCompositionText(t);
+  }
+
  private:
   std::unique_ptr<TextInput> text_input_;
 
@@ -224,31 +236,17 @@
 }
 
 TEST_F(TextInputTest, CompositionText) {
-  ui::CompositionText t;
-  t.text = base::ASCIIToUTF16("composition");
-  t.selection = gfx::Range(1u);
-  t.ime_text_spans.push_back(
-      ui::ImeTextSpan(0, t.text.size(), ui::ImeTextSpan::Thickness::kThick));
+  SetCompositionText("composition");
 
   ui::CompositionText empty;
-  EXPECT_CALL(*delegate(), SetCompositionText(t)).Times(1);
   EXPECT_CALL(*delegate(), SetCompositionText(empty)).Times(1);
-
-  text_input()->SetCompositionText(t);
   text_input()->ClearCompositionText();
 }
 
 TEST_F(TextInputTest, CommitCompositionText) {
-  ui::CompositionText t;
-  t.text = base::ASCIIToUTF16("composition");
-  t.selection = gfx::Range(1u);
-  t.ime_text_spans.push_back(
-      ui::ImeTextSpan(0, t.text.size(), ui::ImeTextSpan::Thickness::kThick));
+  SetCompositionText("composition");
 
-  EXPECT_CALL(*delegate(), SetCompositionText(t)).Times(1);
-  EXPECT_CALL(*delegate(), Commit(t.text)).Times(1);
-
-  text_input()->SetCompositionText(t);
+  EXPECT_CALL(*delegate(), Commit(base::UTF8ToUTF16("composition"))).Times(1);
   text_input()->ConfirmCompositionText();
 }
 
@@ -275,5 +273,66 @@
   text_input()->InsertChar(ev);
 }
 
+TEST_F(TextInputTest, SurroundingText) {
+  gfx::Range range;
+  EXPECT_FALSE(text_input()->GetTextRange(&range));
+  EXPECT_FALSE(text_input()->GetCompositionTextRange(&range));
+  EXPECT_FALSE(text_input()->GetEditableSelectionRange(&range));
+  base::string16 got_text;
+  EXPECT_FALSE(text_input()->GetTextFromRange(gfx::Range(0, 1), &got_text));
+
+  base::string16 text = base::UTF8ToUTF16("surrounding\xE3\x80\x80text");
+  text_input()->SetSurroundingText(text, 11, 12);
+
+  EXPECT_TRUE(text_input()->GetTextRange(&range));
+  EXPECT_EQ(gfx::Range(0, text.size()).ToString(), range.ToString());
+
+  EXPECT_FALSE(text_input()->GetCompositionTextRange(&range));
+  EXPECT_TRUE(text_input()->GetEditableSelectionRange(&range));
+  EXPECT_EQ(gfx::Range(11, 12).ToString(), range.ToString());
+  EXPECT_TRUE(text_input()->GetTextFromRange(gfx::Range(11, 12), &got_text));
+  EXPECT_EQ(text.substr(11, 1), got_text);
+
+  // DeleteSurroundingText receives the range in UTF8 -- so (11, 14) range is
+  // expected.
+  EXPECT_CALL(*delegate(), DeleteSurroundingText(gfx::Range(11, 14))).Times(1);
+  text_input()->ExtendSelectionAndDelete(0, 0);
+
+  size_t composition_size = std::string("composition").size();
+  SetCompositionText("composition");
+  EXPECT_TRUE(text_input()->GetCompositionTextRange(&range));
+  EXPECT_EQ(gfx::Range(11, 11 + composition_size).ToString(), range.ToString());
+  EXPECT_TRUE(text_input()->GetTextRange(&range));
+  EXPECT_EQ(gfx::Range(0, text.size() - 1 + composition_size).ToString(),
+            range.ToString());
+  EXPECT_TRUE(text_input()->GetEditableSelectionRange(&range));
+  EXPECT_EQ(gfx::Range(11, 12).ToString(), range.ToString());
+}
+
+TEST_F(TextInputTest, GetTextRange) {
+  base::string16 text = base::UTF8ToUTF16("surrounding text");
+  text_input()->SetSurroundingText(text, 11, 12);
+
+  SetCompositionText("composition");
+
+  const struct {
+    gfx::Range range;
+    std::string expected;
+  } kTestCases[] = {
+      {gfx::Range(0, 3), "sur"},
+      {gfx::Range(10, 13), "gco"},
+      {gfx::Range(10, 23), "gcompositiont"},
+      {gfx::Range(12, 15), "omp"},
+      {gfx::Range(12, 23), "ompositiont"},
+      {gfx::Range(22, 25), "tex"},
+  };
+  for (auto& c : kTestCases) {
+    base::string16 result;
+    EXPECT_TRUE(text_input()->GetTextFromRange(c.range, &result))
+        << c.range.ToString();
+    EXPECT_EQ(base::UTF8ToUTF16(c.expected), result) << c.range.ToString();
+  }
+}
+
 }  // anonymous namespace
 }  // namespace exo
diff --git a/components/exo/wayland/zwp_text_input_manager.cc b/components/exo/wayland/zwp_text_input_manager.cc
index 15ebfff..73ae15f 100644
--- a/components/exo/wayland/zwp_text_input_manager.cc
+++ b/components/exo/wayland/zwp_text_input_manager.cc
@@ -25,14 +25,6 @@
 ////////////////////////////////////////////////////////////////////////////////
 // text_input_v1 interface:
 
-size_t OffsetFromUTF8Offset(const base::StringPiece& text, uint32_t offset) {
-  return base::UTF8ToUTF16(text.substr(0, offset)).size();
-}
-
-size_t OffsetFromUTF16Offset(const base::StringPiece16& text, uint32_t offset) {
-  return base::UTF16ToUTF8(text.substr(0, offset)).size();
-}
-
 class WaylandTextInputDelegate : public TextInput::Delegate {
  public:
   WaylandTextInputDelegate(wl_resource* text_input) : text_input_(text_input) {}
@@ -107,15 +99,13 @@
   }
 
   void SetCursor(const gfx::Range& selection) override {
-    // TODO(mukai): compute the utf8 offset for |selection| and call
-    // zwp_text_input_v1_send_cursor_position.
-    NOTIMPLEMENTED();
+    zwp_text_input_v1_send_cursor_position(text_input_, selection.end(),
+                                           selection.start());
   }
 
   void DeleteSurroundingText(const gfx::Range& range) override {
-    // TODO(mukai): compute the utf8 offset for |range| and call
-    // zwp_text_input_send_delete_surrounding_text.
-    NOTIMPLEMENTED();
+    zwp_text_input_v1_send_delete_surrounding_text(text_input_, range.start(),
+                                                   range.length());
   }
 
   void SendKey(const ui::KeyEvent& event) override {
@@ -216,7 +206,8 @@
                                      uint32_t anchor) {
   TextInput* text_input = GetUserDataAs<TextInput>(resource);
   text_input->SetSurroundingText(base::UTF8ToUTF16(text),
-                                 OffsetFromUTF8Offset(text, cursor));
+                                 OffsetFromUTF8Offset(text, cursor),
+                                 OffsetFromUTF8Offset(text, anchor));
 }
 
 void text_input_set_content_type(wl_client* client,