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| {
InputState::new(window, cx)
.masked(true)
.default_value("this-is-password")
.default_value("this-is-password-中文🚀🎉")
});
let prefix_input1 =

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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};