diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index d7ac9734..455f9efe 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -62,7 +62,7 @@ impl InputStory { let mask_input = cx.new(|cx| { InputState::new(window, cx) .masked(true) - .default_value("this-is-password") + .default_value("this-is-password-中文🚀🎉") }); let prefix_input1 = diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index e11fa512..b5abf57e 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -76,12 +76,20 @@ impl TextElement { let line_number_width = last_layout.line_number_width; let mut selected_range = state.selected_range; + if let Some(ime_marked_range) = &state.ime_marked_range { selected_range = (ime_marked_range.end..ime_marked_range.end).into(); } let is_selected_all = selected_range.len() == state.text.len(); - let cursor = state.cursor(); + let mut cursor = state.cursor(); + if state.masked { + // Because masked use `*`, 1 char with 1 byte. + selected_range.start = state.text.offset_to_char_index(selected_range.start); + selected_range.end = state.text.offset_to_char_index(selected_range.end); + cursor = state.text.offset_to_char_index(cursor); + } + let mut current_row = None; let mut scroll_offset = state.scroll_handle.offset(); let mut cursor_bounds = None; @@ -442,6 +450,12 @@ impl TextElement { return None; } + if state.masked { + // Because masked use `*`, 1 char with 1 byte. + selected_range.start = state.text.offset_to_char_index(selected_range.start); + selected_range.end = state.text.offset_to_char_index(selected_range.end); + } + let (start_ix, end_ix) = if selected_range.start < selected_range.end { (selected_range.start, selected_range.end) } else { diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index 1fa9052b..e8d67a90 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -61,7 +61,7 @@ pub trait RopeExt { /// # Example /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.line_start_offset(0), 0); @@ -74,7 +74,7 @@ pub trait RopeExt { /// Return the end of the rope if the row is out of bounds. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n" /// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n" @@ -84,7 +84,7 @@ pub trait RopeExt { /// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.slice_line(0).to_string(), "Hello"); /// assert_eq!(rope.slice_line(1).to_string(), "World\r"); @@ -98,7 +98,7 @@ pub trait RopeExt { /// If the range is out of bounds, it will be clamped to the valid range. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r"); /// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文"); @@ -113,7 +113,7 @@ pub trait RopeExt { /// Each line slice includes `\r` if present, but not `\n`. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect(); /// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]); @@ -123,7 +123,7 @@ pub trait RopeExt { /// Return the number of lines in the rope. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.lines_len(), 4); /// ``` @@ -134,7 +134,7 @@ pub trait RopeExt { /// If the row is out of bounds, return 0. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.line_len(0), 5); // "Hello" /// assert_eq!(rope.line_len(1), 6); // "World\r" @@ -151,7 +151,7 @@ pub trait RopeExt { /// - If the range is out of bounds. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// rope.replace(6..11, "Universe"); /// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope"); @@ -207,7 +207,7 @@ pub trait RopeExt { /// - Otherwise return the ix. /// /// ``` - /// use gpui_component::input::{Rope, RopeExt}; + /// use gpui_component::{Rope, RopeExt}; /// use sum_tree::Bias; /// /// let rope = Rope::from("Hello 中文🎉 test\nRope"); @@ -217,6 +217,38 @@ pub trait RopeExt { /// assert_eq!(rope.clip_offset(7, Bias::Right), 9); /// ``` fn clip_offset(&self, offset: usize, bias: Bias) -> usize; + + /// Convert offset in characters to byte offset (0-based). + /// + /// Run in O(n) time. + /// + /// # Example + /// + /// ``` + /// use gpui_component::{Rope, RopeExt}; + /// let rope = Rope::from("a 中文🎉 test\nRope"); + /// assert_eq!(rope.char_index_to_offset(0), 0); + /// assert_eq!(rope.char_index_to_offset(1), 1); + /// assert_eq!(rope.char_index_to_offset(3), "a 中".len()); + /// assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len()); + /// ``` + fn char_index_to_offset(&self, char_index: usize) -> usize; + + /// Convert byte offset (0-based) to offset in characters. + /// + /// Run in O(n) time. + /// + /// # Example + /// + /// ``` + /// use gpui_component::{Rope, RopeExt}; + /// let rope = Rope::from("a 中文🎉 test\nRope"); + /// assert_eq!(rope.offset_to_char_index(0), 0); + /// assert_eq!(rope.offset_to_char_index(1), 1); + /// assert_eq!(rope.offset_to_char_index(3), 3); + /// assert_eq!(rope.offset_to_char_index(4), 3); + /// ``` + fn offset_to_char_index(&self, offset: usize) -> usize; } impl RopeExt for Rope { @@ -388,6 +420,15 @@ impl RopeExt for Rope { self.ceil_char_boundary(offset) } } + + fn char_index_to_offset(&self, char_offset: usize) -> usize { + self.chars().take(char_offset).map(|c| c.len_utf8()).sum() + } + + fn offset_to_char_index(&self, offset: usize) -> usize { + let offset = self.clip_offset(offset, Bias::Right); + self.slice(..offset).chars().count() + } } #[cfg(test)] @@ -396,7 +437,7 @@ mod tests { use sum_tree::Bias; use tree_sitter::Point; - use crate::input::{Position, RopeExt}; + use crate::{input::Position, RopeExt}; #[test] fn test_slice_line() { @@ -637,4 +678,22 @@ mod tests { assert_eq!(rope.clip_offset(26, Bias::Left), 26); assert_eq!(rope.clip_offset(100, Bias::Left), 26); } + + #[test] + fn test_char_index_to_offset() { + let rope = Rope::from("a 中文🎉 test\nRope"); + assert_eq!(rope.char_index_to_offset(0), 0); + assert_eq!(rope.char_index_to_offset(1), 1); + assert_eq!(rope.char_index_to_offset(3), "a 中".len()); + assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len()); + assert_eq!(rope.char_index_to_offset(6), "a 中文🎉 ".len()); + + assert_eq!(rope.offset_to_char_index(0), 0); + assert_eq!(rope.offset_to_char_index(1), 1); + assert_eq!(rope.offset_to_char_index(3), 3); + assert_eq!(rope.offset_to_char_index(4), 3); + assert_eq!(rope.offset_to_char_index(5), 3); + assert_eq!(rope.offset_to_char_index(6), 4); + assert_eq!(rope.offset_to_char_index(10), 5); + } } diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index fd3ba5ce..456bdbee 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -1671,7 +1671,8 @@ impl InputState { // Return offset by use closest_index_for_x if is single line mode. if self.mode.is_single_line() { - return line_layout.closest_index_for_x(pos.x); + index = line_layout.closest_index_for_x(pos.x); + break; } if let Some(v) = line_layout.closest_index_for_position(pos, line_height) { @@ -1685,10 +1686,17 @@ impl InputState { index += line_layout.len() + 1; } - if index > self.text.len() { + let index = if index > self.text.len() { self.text.len() } else { index + }; + + if self.masked { + // When is masked, the index is char index, need convert to byte index. + self.text.char_index_to_offset(index) + } else { + index } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 533c057b..0452990c 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -70,6 +70,7 @@ pub use wry; pub use crate::Disableable; pub use event::InteractiveElementExt; pub use index_path::IndexPath; +pub use input::{Rope, RopeExt, RopeLines}; #[cfg(any(feature = "inspector", debug_assertions))] pub use inspector::*; pub use menu::{context_menu, popup_menu};