Clip UTF-16 offsets in text for range (cherry-pick #20968) (#20969)

gcp-cherry-pick-bot[bot] and Conrad Irwin created

Cherry-picked Clip UTF-16 offsets in text for range (#20968)

When launching the Pinyin keyboard, macOS will sometimes try to peek one
character back in the string.

This caused a panic if the preceding character was an emoji. The docs
say
"don't assume the range is valid", so now we don't.

Release Notes:

- (macOS) Fixed a panic when using the Pinyin keyboard with emojis

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/editor/src/editor.rs                  | 15 +++++----
crates/gpui/examples/input.rs                | 35 +++++++++++++++++++++
crates/gpui/src/input.rs                     | 14 ++++++--
crates/gpui/src/platform.rs                  | 10 +++++-
crates/gpui/src/platform/mac/window.rs       | 11 +++++-
crates/terminal_view/src/terminal_element.rs |  1 
6 files changed, 70 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -14434,15 +14434,16 @@ impl ViewInputHandler for Editor {
     fn text_for_range(
         &mut self,
         range_utf16: Range<usize>,
+        adjusted_range: &mut Option<Range<usize>>,
         cx: &mut ViewContext<Self>,
     ) -> Option<String> {
-        Some(
-            self.buffer
-                .read(cx)
-                .read(cx)
-                .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
-                .collect(),
-        )
+        let snapshot = self.buffer.read(cx).read(cx);
+        let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left);
+        let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right);
+        if (start.0..end.0) != range_utf16 {
+            adjusted_range.replace(start.0..end.0);
+        }
+        Some(snapshot.text_for_range(start..end).collect())
     }
 
     fn selected_text_range(

crates/gpui/examples/input.rs 🔗

@@ -15,7 +15,10 @@ actions!(
         SelectAll,
         Home,
         End,
-        ShowCharacterPalette
+        ShowCharacterPalette,
+        Paste,
+        Cut,
+        Copy,
     ]
 );
 
@@ -107,6 +110,28 @@ impl TextInput {
         cx.show_character_palette();
     }
 
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
+            self.replace_text_in_range(None, &text.replace("\n", " "), cx);
+        }
+    }
+
+    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        if !self.selected_range.is_empty() {
+            cx.write_to_clipboard(ClipboardItem::new_string(
+                (&self.content[self.selected_range.clone()]).to_string(),
+            ));
+        }
+    }
+    fn cut(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        if !self.selected_range.is_empty() {
+            cx.write_to_clipboard(ClipboardItem::new_string(
+                (&self.content[self.selected_range.clone()]).to_string(),
+            ));
+            self.replace_text_in_range(None, "", cx)
+        }
+    }
+
     fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
         self.selected_range = offset..offset;
         cx.notify()
@@ -219,9 +244,11 @@ impl ViewInputHandler for TextInput {
     fn text_for_range(
         &mut self,
         range_utf16: Range<usize>,
+        actual_range: &mut Option<Range<usize>>,
         _cx: &mut ViewContext<Self>,
     ) -> Option<String> {
         let range = self.range_from_utf16(&range_utf16);
+        actual_range.replace(self.range_to_utf16(&range));
         Some(self.content[range].to_string())
     }
 
@@ -497,6 +524,9 @@ impl Render for TextInput {
             .on_action(cx.listener(Self::home))
             .on_action(cx.listener(Self::end))
             .on_action(cx.listener(Self::show_character_palette))
+            .on_action(cx.listener(Self::paste))
+            .on_action(cx.listener(Self::cut))
+            .on_action(cx.listener(Self::copy))
             .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
             .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
             .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
@@ -602,6 +632,9 @@ fn main() {
             KeyBinding::new("shift-left", SelectLeft, None),
             KeyBinding::new("shift-right", SelectRight, None),
             KeyBinding::new("cmd-a", SelectAll, None),
+            KeyBinding::new("cmd-v", Paste, None),
+            KeyBinding::new("cmd-c", Copy, None),
+            KeyBinding::new("cmd-x", Cut, None),
             KeyBinding::new("home", Home, None),
             KeyBinding::new("end", End, None),
             KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),

crates/gpui/src/input.rs 🔗

@@ -9,8 +9,12 @@ use std::ops::Range;
 /// See [`InputHandler`] for details on how to implement each method.
 pub trait ViewInputHandler: 'static + Sized {
     /// See [`InputHandler::text_for_range`] for details
-    fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
-        -> Option<String>;
+    fn text_for_range(
+        &mut self,
+        range: Range<usize>,
+        adjusted_range: &mut Option<Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<String>;
 
     /// See [`InputHandler::selected_text_range`] for details
     fn selected_text_range(
@@ -89,10 +93,12 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
     fn text_for_range(
         &mut self,
         range_utf16: Range<usize>,
+        adjusted_range: &mut Option<Range<usize>>,
         cx: &mut WindowContext,
     ) -> Option<String> {
-        self.view
-            .update(cx, |view, cx| view.text_for_range(range_utf16, cx))
+        self.view.update(cx, |view, cx| {
+            view.text_for_range(range_utf16, adjusted_range, cx)
+        })
     }
 
     fn replace_text_in_range(

crates/gpui/src/platform.rs 🔗

@@ -643,9 +643,13 @@ impl PlatformInputHandler {
     }
 
     #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
-    fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
+    fn text_for_range(
+        &mut self,
+        range_utf16: Range<usize>,
+        adjusted: &mut Option<Range<usize>>,
+    ) -> Option<String> {
         self.cx
-            .update(|cx| self.handler.text_for_range(range_utf16, cx))
+            .update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx))
             .ok()
             .flatten()
     }
@@ -712,6 +716,7 @@ impl PlatformInputHandler {
 
 /// A struct representing a selection in a text buffer, in UTF16 characters.
 /// This is different from a range because the head may be before the tail.
+#[derive(Debug)]
 pub struct UTF16Selection {
     /// The range of text in the document this selection corresponds to
     /// in UTF16 characters.
@@ -749,6 +754,7 @@ pub trait InputHandler: 'static {
     fn text_for_range(
         &mut self,
         range_utf16: Range<usize>,
+        adjusted_range: &mut Option<Range<usize>>,
         cx: &mut WindowContext,
     ) -> Option<String>;
 

crates/gpui/src/platform/mac/window.rs 🔗

@@ -38,6 +38,7 @@ use std::{
     cell::Cell,
     ffi::{c_void, CStr},
     mem,
+    ops::Range,
     path::PathBuf,
     ptr::{self, NonNull},
     rc::Rc,
@@ -1754,15 +1755,21 @@ extern "C" fn attributed_substring_for_proposed_range(
     this: &Object,
     _: Sel,
     range: NSRange,
-    _actual_range: *mut c_void,
+    actual_range: *mut c_void,
 ) -> id {
     with_input_handler(this, |input_handler| {
         let range = range.to_range()?;
         if range.is_empty() {
             return None;
         }
+        let mut adjusted: Option<Range<usize>> = None;
 
-        let selected_text = input_handler.text_for_range(range.clone())?;
+        let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
+        if let Some(adjusted) = adjusted {
+            if adjusted != range {
+                unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
+            }
+        }
         unsafe {
             let string: id = msg_send![class!(NSAttributedString), alloc];
             let string: id = msg_send![string, initWithString: ns_string(&selected_text)];

crates/terminal_view/src/terminal_element.rs 🔗

@@ -1001,6 +1001,7 @@ impl InputHandler for TerminalInputHandler {
     fn text_for_range(
         &mut self,
         _: std::ops::Range<usize>,
+        _: &mut Option<std::ops::Range<usize>>,
         _: &mut WindowContext,
     ) -> Option<String> {
         None