diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index ee5c46e76..bd1d38b52 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -12,7 +12,9 @@ use tui::text::Span; use tui::widgets::{Block, Widget}; use helix_core::{ - unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, + unicode::segmentation::{GraphemeCursor, UnicodeSegmentation}, + unicode::width::UnicodeWidthStr, + Position, }; use helix_view::{ graphics::{CursorKind, Margin, Rect}, @@ -535,21 +537,31 @@ impl Prompt { .into(); text.render(self.line_area, surface, cx); } else { - if self.line.len() < self.line_area.width as usize { + if self.line.width() < self.line_area.width as usize { self.anchor = 0; } else if self.cursor < self.anchor { self.anchor = self.cursor; - } else if self.cursor - self.anchor > self.line_area.width as usize { - self.anchor = self.cursor - self.line_area.width as usize; + } else if self.line[self.anchor..self.cursor].width() > self.line_area.width as usize { + self.anchor = self + .line + .grapheme_indices(true) + .find(|i| self.line[i.0..self.cursor].width() <= self.line_area.width as usize) + .map_or(self.cursor, |(p, _)| p); } self.truncate_start = self.anchor > 0; - self.truncate_end = self.line.len() - self.anchor > self.line_area.width as usize; + self.truncate_end = self.line[self.anchor..].width() > self.line_area.width as usize; // if we keep inserting characters just before the end elipsis, we move the anchor // so that those new characters are displayed - if self.truncate_end && self.cursor - self.anchor >= self.line_area.width as usize { - self.anchor += 1; + if self.truncate_end + && self.line[self.anchor..self.cursor].width() >= self.line_area.width as usize + { + self.anchor = self + .line + .grapheme_indices(true) + .find(|i| self.anchor < i.0) + .map_or(self.anchor, |(p, _)| p); } surface.set_string_anchored( @@ -734,7 +746,11 @@ impl Component for Prompt { .clip_left(self.prompt.len() as u16) .clip_right(if self.prompt.is_empty() { 2 } else { 0 }); - let anchor = self.anchor.min(self.line.len().saturating_sub(1)); + let anchor = self + .line + .grapheme_indices(true) + .rfind(|i| i.0 < self.line.len()) + .map_or(self.anchor, |p| self.anchor.min(p.0)); let mut col = area.left() as usize + UnicodeWidthStr::width(&self.line[anchor..self.cursor.max(anchor)]); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 29f76cfb8..20e8ac9a7 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -820,3 +820,25 @@ async fn macro_play_within_macro_record() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn global_search_with_multibyte_chars() -> anyhow::Result<()> { + // Assert that `helix_term::commands::global_search` handles multibyte characters correctly. + test(( + indoc! {"\ + // Hello world! + // #[| + ]# + "}, + // start global search + " /«十分に長い マルチバイトキャラクター列» で検索", + indoc! {"\ + // Hello world! + // #[| + ]# + "}, + )) + .await?; + + Ok(()) +} diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index bfcf35ac5..c04d75a2d 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -338,14 +338,21 @@ impl Buffer { end_index -= 1; } + let mut graphemes = string.grapheme_indices(true); + if truncate_start { self.content[start_index].set_symbol("…"); - start_index += 1; + if let Some((_, c)) = graphemes.next() { + let width = c.width(); + // Reset following cells if multi-width + for i in start_index + 1..start_index + width { + self.content[i].reset(); + } + start_index += width; + } } - let graphemes = string.grapheme_indices(true); - - for (byte_offset, s) in graphemes.skip(truncate_start as usize) { + for (byte_offset, s) in graphemes { if start_index > end_index { break; }