input: Fix Input panics when masked have non-ASCII characters. (#1382)

Close #1377
This commit is contained in:
Jason Lee 2025-10-15 15:15:50 +08:00 committed by GitHub
parent 491bacfd0e
commit 59f4e26c7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 96 additions and 14 deletions

View file

@ -62,7 +62,7 @@ impl InputStory {
let mask_input = cx.new(|cx| { let mask_input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.masked(true) .masked(true)
.default_value("this-is-password") .default_value("this-is-password-中文🚀🎉")
}); });
let prefix_input1 = let prefix_input1 =

View file

@ -76,12 +76,20 @@ impl TextElement {
let line_number_width = last_layout.line_number_width; let line_number_width = last_layout.line_number_width;
let mut selected_range = state.selected_range; let mut selected_range = state.selected_range;
if let Some(ime_marked_range) = &state.ime_marked_range { if let Some(ime_marked_range) = &state.ime_marked_range {
selected_range = (ime_marked_range.end..ime_marked_range.end).into(); selected_range = (ime_marked_range.end..ime_marked_range.end).into();
} }
let is_selected_all = selected_range.len() == state.text.len(); 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 current_row = None;
let mut scroll_offset = state.scroll_handle.offset(); let mut scroll_offset = state.scroll_handle.offset();
let mut cursor_bounds = None; let mut cursor_bounds = None;
@ -442,6 +450,12 @@ impl TextElement {
return None; 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 { let (start_ix, end_ix) = if selected_range.start < selected_range.end {
(selected_range.start, selected_range.end) (selected_range.start, selected_range.end)
} else { } else {

View file

@ -61,7 +61,7 @@ pub trait RopeExt {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use gpui_component::input::{Rope, RopeExt}; /// use gpui_component::{Rope, RopeExt};
/// ///
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
/// assert_eq!(rope.line_start_offset(0), 0); /// 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. /// 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"); /// 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(0), 5); // "Hello\n"
/// assert_eq!(rope.line_end_offset(1), 12); // "World\r\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`. /// 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"); /// 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(0).to_string(), "Hello");
/// assert_eq!(rope.slice_line(1).to_string(), "World\r"); /// 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. /// 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"); /// 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(0..2).to_string(), "Hello\nWorld\r");
/// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文"); /// 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`. /// 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 rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
/// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect(); /// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
/// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]); /// 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. /// 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"); /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
/// assert_eq!(rope.lines_len(), 4); /// assert_eq!(rope.lines_len(), 4);
/// ``` /// ```
@ -134,7 +134,7 @@ pub trait RopeExt {
/// If the row is out of bounds, return 0. /// 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"); /// 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(0), 5); // "Hello"
/// assert_eq!(rope.line_len(1), 6); // "World\r" /// assert_eq!(rope.line_len(1), 6); // "World\r"
@ -151,7 +151,7 @@ pub trait RopeExt {
/// - If the range is out of bounds. /// - 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"); /// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
/// rope.replace(6..11, "Universe"); /// rope.replace(6..11, "Universe");
/// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope"); /// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope");
@ -207,7 +207,7 @@ pub trait RopeExt {
/// - Otherwise return the ix. /// - Otherwise return the ix.
/// ///
/// ``` /// ```
/// use gpui_component::input::{Rope, RopeExt}; /// use gpui_component::{Rope, RopeExt};
/// use sum_tree::Bias; /// use sum_tree::Bias;
/// ///
/// let rope = Rope::from("Hello 中文🎉 test\nRope"); /// let rope = Rope::from("Hello 中文🎉 test\nRope");
@ -217,6 +217,38 @@ pub trait RopeExt {
/// assert_eq!(rope.clip_offset(7, Bias::Right), 9); /// assert_eq!(rope.clip_offset(7, Bias::Right), 9);
/// ``` /// ```
fn clip_offset(&self, offset: usize, bias: Bias) -> usize; 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 { impl RopeExt for Rope {
@ -388,6 +420,15 @@ impl RopeExt for Rope {
self.ceil_char_boundary(offset) 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)] #[cfg(test)]
@ -396,7 +437,7 @@ mod tests {
use sum_tree::Bias; use sum_tree::Bias;
use tree_sitter::Point; use tree_sitter::Point;
use crate::input::{Position, RopeExt}; use crate::{input::Position, RopeExt};
#[test] #[test]
fn test_slice_line() { fn test_slice_line() {
@ -637,4 +678,22 @@ mod tests {
assert_eq!(rope.clip_offset(26, Bias::Left), 26); assert_eq!(rope.clip_offset(26, Bias::Left), 26);
assert_eq!(rope.clip_offset(100, 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);
}
} }

View file

@ -1671,7 +1671,8 @@ impl InputState {
// Return offset by use closest_index_for_x if is single line mode. // Return offset by use closest_index_for_x if is single line mode.
if self.mode.is_single_line() { 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) { if let Some(v) = line_layout.closest_index_for_position(pos, line_height) {
@ -1685,10 +1686,17 @@ impl InputState {
index += line_layout.len() + 1; index += line_layout.len() + 1;
} }
if index > self.text.len() { let index = if index > self.text.len() {
self.text.len() self.text.len()
} else { } else {
index 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
} }
} }

View file

@ -70,6 +70,7 @@ pub use wry;
pub use crate::Disableable; pub use crate::Disableable;
pub use event::InteractiveElementExt; pub use event::InteractiveElementExt;
pub use index_path::IndexPath; pub use index_path::IndexPath;
pub use input::{Rope, RopeExt, RopeLines};
#[cfg(any(feature = "inspector", debug_assertions))] #[cfg(any(feature = "inspector", debug_assertions))]
pub use inspector::*; pub use inspector::*;
pub use menu::{context_menu, popup_menu}; pub use menu::{context_menu, popup_menu};