Treat NSTextInputClient ranges as UTF-16

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs              | 76 ++++++++++++++++---------
crates/editor/src/multi_buffer/anchor.rs | 10 ++
crates/gpui/src/platform/mac/window.rs   | 49 +++-------------
crates/text/src/offset_utf16.rs          | 50 +++++++++++++++++
4 files changed, 115 insertions(+), 70 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -39,15 +39,15 @@ pub use items::MAX_TAB_TITLE_LEN;
 pub use language::{char_kind, CharKind};
 use language::{
     BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
-    IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal,
+    IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal,
     TransactionId,
 };
 use link_go_to_definition::LinkGoToDefinitionState;
-use multi_buffer::MultiBufferChunks;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
+use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
 use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
@@ -5877,27 +5877,33 @@ impl View for Editor {
         context
     }
 
-    fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String> {
+    fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
         Some(
             self.buffer
                 .read(cx)
                 .read(cx)
-                .text_for_range(range)
+                .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
                 .collect(),
         )
     }
 
     fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
-        Some(self.selections.newest(cx).range())
+        let range = self.selections.newest::<OffsetUtf16>(cx).range();
+        Some(range.start.0..range.end.0)
     }
 
-    fn set_selected_text_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) {
-        self.change_selections(None, cx, |selections| selections.select_ranges([range]));
+    fn set_selected_text_range(&mut self, range_utf16: Range<usize>, cx: &mut ViewContext<Self>) {
+        self.change_selections(None, cx, |selections| {
+            selections.select_ranges([OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)])
+        });
     }
 
     fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
         let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
-        Some(range.to_offset(&*self.buffer.read(cx).read(cx)))
+        let snapshot = self.buffer.read(cx).read(cx);
+        let range_utf16 =
+            range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot);
+        Some(range_utf16.start.0..range_utf16.end.0)
     }
 
     fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
@@ -5906,34 +5912,36 @@ impl View for Editor {
 
     fn replace_text_in_range(
         &mut self,
-        range: Option<Range<usize>>,
+        range_utf16: Option<Range<usize>>,
         text: &str,
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            if let Some(range) = range {
-                this.set_selected_text_range(range, cx);
+            if let Some(range_utf16) = range_utf16.or_else(|| this.marked_text_range(cx)) {
+                this.set_selected_text_range(range_utf16, cx);
             }
             this.handle_input(text, cx);
+            this.unmark_text(cx);
         });
     }
 
     fn replace_and_mark_text_in_range(
         &mut self,
-        range: Option<Range<usize>>,
+        range_utf16: Option<Range<usize>>,
         text: &str,
-        new_selected_range: Option<Range<usize>>,
+        new_selected_range_utf16: Option<Range<usize>>,
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            let range = range.or_else(|| {
-                let ranges = this.text_highlights::<InputComposition>(cx)?.1;
-                let range = ranges.first()?;
-                let snapshot = this.buffer.read(cx).read(cx);
-                Some(range.to_offset(&*snapshot))
-            });
-            if let Some(range) = range {
-                this.set_selected_text_range(range, cx);
+            if let Some(mut marked_range) = this.marked_text_range(cx) {
+                if let Some(relative_range_utf16) = range_utf16.as_ref() {
+                    marked_range.end = marked_range.start + relative_range_utf16.end;
+                    marked_range.start += relative_range_utf16.start;
+                }
+
+                this.set_selected_text_range(marked_range, cx);
+            } else if let Some(range_utf16) = range_utf16 {
+                this.set_selected_text_range(range_utf16, cx);
             }
 
             let selection = this.selections.newest_anchor();
@@ -5941,16 +5949,28 @@ impl View for Editor {
                 let snapshot = this.buffer.read(cx).read(cx);
                 selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
             };
-            this.highlight_text::<InputComposition>(
-                vec![marked_range],
-                this.style(cx).composition_mark,
-                cx,
-            );
+
+            if text.is_empty() {
+                this.unmark_text(cx);
+            } else {
+                this.highlight_text::<InputComposition>(
+                    vec![marked_range.clone()],
+                    this.style(cx).composition_mark,
+                    cx,
+                );
+            }
 
             this.handle_input(text, cx);
 
-            if let Some(new_selected_range) = new_selected_range {
-                this.set_selected_text_range(new_selected_range, cx);
+            if let Some(new_selected_range) = new_selected_range_utf16 {
+                let snapshot = this.buffer.read(cx).read(cx);
+                let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+                drop(snapshot);
+                this.set_selected_text_range(
+                    insertion_start + new_selected_range.start
+                        ..insertion_start + new_selected_range.end,
+                    cx,
+                );
             }
         });
     }

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -1,10 +1,10 @@
-use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
+use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
 use std::{
     cmp::Ordering,
     ops::{Range, Sub},
 };
 use sum_tree::Bias;
-use text::{rope::TextDimension, Point};
+use text::{rope::TextDimension, OffsetUtf16, Point};
 
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
@@ -89,6 +89,12 @@ impl ToOffset for Anchor {
     }
 }
 
+impl ToOffsetUtf16 for Anchor {
+    fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
+        self.summary(snapshot)
+    }
+}
+
 impl ToPoint for Anchor {
     fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
         self.summary(snapshot)

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

@@ -1139,62 +1139,31 @@ extern "C" fn set_marked_text(
             .to_str()
             .unwrap();
 
-        let window_state = get_window_state(this);
-        let mut window_state = window_state.borrow_mut();
-        if let Some(pending) = window_state.pending_key_event.as_mut() {
-            pending.set_marked_text = Some((text.to_string(), selected_range, replacement_range));
-        } else {
-            drop(window_state);
-            with_input_handler(this, |input_handler| {
-                input_handler.replace_and_mark_text_in_range(
-                    replacement_range,
-                    text,
-                    selected_range,
-                );
-            });
-        }
+        with_input_handler(this, |input_handler| {
+            input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range);
+        });
     }
 }
 
 extern "C" fn unmark_text(this: &Object, _: Sel) {
-    println!("unmark_text");
-    let window_state = unsafe { get_window_state(this) };
-    let mut window_state = window_state.borrow_mut();
-    if let Some(pending) = window_state.pending_key_event.as_mut() {
-        pending.unmark_text = true;
-        pending.set_marked_text.take();
-    } else {
-        drop(window_state);
-        with_input_handler(this, |input_handler| input_handler.finish_composition());
-    }
+    with_input_handler(this, |input_handler| input_handler.finish_composition());
 }
 
 extern "C" fn attributed_substring_for_proposed_range(
     this: &Object,
     _: Sel,
     range: NSRange,
-    actual_range: *mut c_void,
+    _actual_range: *mut c_void,
 ) -> id {
+    println!("attributed_substring_for_proposed_range({:?})", range);
     with_input_handler(this, |input_handler| {
-        let actual_range = actual_range as *mut NSRange;
-        if !actual_range.is_null() {
-            unsafe { *actual_range = NSRange::invalid() };
-        }
-
-        let requested_range = range.to_range()?;
-        if requested_range.is_empty() {
-            return None;
-        }
-
-        let selected_range = input_handler.selected_text_range()?;
-        let intersection = cmp::max(requested_range.start, selected_range.start)
-            ..cmp::min(requested_range.end, selected_range.end);
-        if intersection.start >= intersection.end {
+        let range = range.to_range()?;
+        if range.is_empty() {
             return None;
         }
 
+        let selected_text = input_handler.text_for_range(range)?;
         unsafe {
-            let selected_text = input_handler.text_for_range(intersection)?;
             let string: id = msg_send![class!(NSAttributedString), alloc];
             let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
             Some(string)

crates/text/src/offset_utf16.rs 🔗

@@ -0,0 +1,50 @@
+use std::ops::{Add, AddAssign, Sub};
+
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
+pub struct OffsetUtf16(pub usize);
+
+impl<'a> Add<&'a Self> for OffsetUtf16 {
+    type Output = Self;
+
+    fn add(self, other: &'a Self) -> Self::Output {
+        Self(self.0 + other.0)
+    }
+}
+
+impl Add for OffsetUtf16 {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self::Output {
+        Self(self.0 + other.0)
+    }
+}
+
+impl<'a> Sub<&'a Self> for OffsetUtf16 {
+    type Output = Self;
+
+    fn sub(self, other: &'a Self) -> Self::Output {
+        debug_assert!(*other <= self);
+        Self(self.0 - other.0)
+    }
+}
+
+impl Sub for OffsetUtf16 {
+    type Output = OffsetUtf16;
+
+    fn sub(self, other: Self) -> Self::Output {
+        debug_assert!(other <= self);
+        Self(self.0 - other.0)
+    }
+}
+
+impl<'a> AddAssign<&'a Self> for OffsetUtf16 {
+    fn add_assign(&mut self, other: &'a Self) {
+        self.0 += other.0;
+    }
+}
+
+impl AddAssign<Self> for OffsetUtf16 {
+    fn add_assign(&mut self, other: Self) {
+        self.0 += other.0;
+    }
+}