From 5419600fcd5686a86939c6ca7c6b28cb3d0703fa Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 26 May 2025 22:42:31 +0800 Subject: [PATCH] input: Fix Input may out of char boundary (#901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` byte index 3 is not a char boundary; it is inside '吧' (bytes 1..4) of `L吧2`: 0: ``` --- crates/ui/src/input/state.rs | 84 +++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index c582659a..a661fdbe 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -640,8 +640,8 @@ impl InputState { cx: &mut Context, ) { let text: SharedString = text.into(); - let range = self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset())); - self.replace_text_in_range(Some(range), &text, window, cx); + let range_utf16 = self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset())); + self.replace_text_in_range(Some(range_utf16), &text, window, cx); self.selected_range = self.selected_range.end..self.selected_range.end; } @@ -783,7 +783,11 @@ impl InputState { } if !self.selected_range.is_empty() { - self.move_to(self.selected_range.start.saturating_sub(1), window, cx); + self.move_to( + self.previous_boundary(self.selected_range.start.saturating_sub(1)), + window, + cx, + ); } self.pause_blink_cursor(cx); self.move_vertical(-1, window, cx); @@ -795,7 +799,11 @@ impl InputState { } if !self.selected_range.is_empty() { - self.move_to(self.selected_range.end.saturating_sub(1), window, cx); + self.move_to( + self.next_boundary(self.selected_range.end.saturating_sub(1)), + window, + cx, + ); } self.pause_blink_cursor(cx); @@ -825,7 +833,7 @@ impl InputState { return; } let offset = self.start_of_line(window, cx).saturating_sub(1); - self.select_to(offset, window, cx); + self.select_to(self.previous_boundary(offset), window, cx); } pub(super) fn select_down( @@ -964,8 +972,8 @@ impl InputState { /// Return the start offset of the previous word. fn previous_start_of_word(&mut self) -> usize { let offset = self.selected_range.start; - let prev_str = &self.text[..offset].to_string(); - UnicodeSegmentation::split_word_bound_indices(prev_str as &str) + let prev_str = self.text_for_range_utf8(0..offset); + UnicodeSegmentation::split_word_bound_indices(prev_str) .filter(|(_, s)| !s.trim_start().is_empty()) .next_back() .map(|(i, _)| i) @@ -975,8 +983,8 @@ impl InputState { /// Return the next end offset of the next word. fn next_end_of_word(&mut self) -> usize { let offset = self.cursor_offset(); - let next_str = &self.text[offset..].to_string(); - UnicodeSegmentation::split_word_bound_indices(next_str as &str) + let next_str = self.text_for_range_utf8(offset..self.text.len()); + UnicodeSegmentation::split_word_bound_indices(next_str) .find(|(_, s)| !s.trim_start().is_empty()) .map(|(i, s)| offset + i + s.len()) .unwrap_or(self.text.len()) @@ -1199,7 +1207,7 @@ impl InputState { if is_eof { new_offset += 1; } - self.move_to(new_offset, window, cx); + self.move_to(self.next_boundary(new_offset), window, cx); // Add indent self.replace_text_in_range( @@ -1297,6 +1305,7 @@ impl InputState { ); removed_len += tab_indent.len(); } + // +1 for "\n" offset += line.len().saturating_sub(tab_indent.len()) + 1; } @@ -1306,7 +1315,10 @@ impl InputState { // Selected none let start_offset = self.selected_range.start; let offset = self.start_of_line_of_selection(window, cx); - if self.text[offset..].starts_with(tab_indent.as_ref()) { + if self + .text_for_range_utf8(offset..self.text.len()) + .starts_with(tab_indent.as_ref()) + { self.replace_text_in_range( Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))), "", @@ -1416,7 +1428,9 @@ impl InputState { return; } - let selected_text = self.text[self.selected_range.clone()].to_string(); + let selected_text = self + .text_for_range_utf8(self.selected_range.clone()) + .to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); } @@ -1425,7 +1439,9 @@ impl InputState { return; } - let selected_text = self.text[self.selected_range.clone()].to_string(); + let selected_text = self + .text_for_range_utf8(self.selected_range.clone()) + .to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); self.replace_text_in_range(None, "", window, cx); } @@ -1658,7 +1674,7 @@ impl InputState { let mut start = offset; let mut end = start; let prev_text = self - .text_for_range(self.range_to_utf16(&(0..start + 1)), &mut None, window, cx) + .text_for_range(self.range_to_utf16(&(0..start)), &mut None, window, cx) .unwrap_or_default(); let next_text = self .text_for_range( @@ -1672,16 +1688,16 @@ impl InputState { let prev_chars = prev_text.chars().rev(); let next_chars = next_text.chars(); - let mut last_char_len = 0; - for (_, c) in prev_chars.enumerate() { + let pre_chars_count = prev_chars.clone().count(); + for (ix, c) in prev_chars.enumerate() { if !is_word(c) { break; } - last_char_len = c.len_utf8(); - start = start.saturating_sub(last_char_len); + if ix < pre_chars_count { + start = start.saturating_sub(c.len_utf8()); + } } - start += last_char_len; for (_, c) in next_chars.enumerate() { if !is_word(c) { @@ -1691,19 +1707,8 @@ impl InputState { end += c.len_utf8(); } - // Ensure at least one character is selected if start == end { - end = end + 1; - - // Avoid select empty range - match self.text.get(start..end) { - None => return, - Some(part) => { - if part.trim().len() == 0 { - return; - } - } - } + return; } self.selected_range = start..end; @@ -1891,6 +1896,11 @@ impl InputState { self.mode.update_auto_grow(&self.text_wrapper); } } + + fn text_for_range_utf8(&mut self, range: impl Into>) -> &str { + let range = self.range_from_utf16(&self.range_to_utf16(&range.into())); + &self.text[range] + } } impl EntityInputHandler for InputState { @@ -1953,8 +1963,10 @@ impl EntityInputHandler for InputState { .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); - let pending_text: SharedString = - (self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into(); + let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned() + + new_text + + self.text_for_range_utf8(range.end..self.text.len())) + .into(); // Check if the new text is valid if !self.is_valid_input(&pending_text) { return; @@ -1994,8 +2006,10 @@ impl EntityInputHandler for InputState { .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); - let pending_text: SharedString = - (self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into(); + let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned() + + new_text + + self.text_for_range_utf8(range.end..self.text.len())) + .into(); if !self.is_valid_input(&pending_text) { return; }