mirror of https://github.com/helix-editor/helix
fix: global search crashes with multibyte characters (resolves #13681)
- use `str::width` from UnicodeWidthStr to determine the display range - `set_string_anchored` assumed that the string starts with a single-byte char - add an integration test `global_search_with_multibyte_chars`pull/13681/head
parent
205e7ece70
commit
22f3936eb8
|
@ -12,7 +12,9 @@ use tui::text::Span;
|
||||||
use tui::widgets::{Block, Widget};
|
use tui::widgets::{Block, Widget};
|
||||||
|
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
|
unicode::segmentation::{GraphemeCursor, UnicodeSegmentation},
|
||||||
|
unicode::width::UnicodeWidthStr,
|
||||||
|
Position,
|
||||||
};
|
};
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
graphics::{CursorKind, Margin, Rect},
|
graphics::{CursorKind, Margin, Rect},
|
||||||
|
@ -535,21 +537,31 @@ impl Prompt {
|
||||||
.into();
|
.into();
|
||||||
text.render(self.line_area, surface, cx);
|
text.render(self.line_area, surface, cx);
|
||||||
} else {
|
} else {
|
||||||
if self.line.len() < self.line_area.width as usize {
|
if self.line.width() < self.line_area.width as usize {
|
||||||
self.anchor = 0;
|
self.anchor = 0;
|
||||||
} else if self.cursor < self.anchor {
|
} else if self.cursor < self.anchor {
|
||||||
self.anchor = self.cursor;
|
self.anchor = self.cursor;
|
||||||
} else if self.cursor - self.anchor > self.line_area.width as usize {
|
} else if self.line[self.anchor..self.cursor].width() > self.line_area.width as usize {
|
||||||
self.anchor = self.cursor - 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_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
|
// if we keep inserting characters just before the end elipsis, we move the anchor
|
||||||
// so that those new characters are displayed
|
// so that those new characters are displayed
|
||||||
if self.truncate_end && self.cursor - self.anchor >= self.line_area.width as usize {
|
if self.truncate_end
|
||||||
self.anchor += 1;
|
&& 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(
|
surface.set_string_anchored(
|
||||||
|
@ -734,7 +746,11 @@ impl Component for Prompt {
|
||||||
.clip_left(self.prompt.len() as u16)
|
.clip_left(self.prompt.len() as u16)
|
||||||
.clip_right(if self.prompt.is_empty() { 2 } else { 0 });
|
.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
|
let mut col = area.left() as usize
|
||||||
+ UnicodeWidthStr::width(&self.line[anchor..self.cursor.max(anchor)]);
|
+ UnicodeWidthStr::width(&self.line[anchor..self.cursor.max(anchor)]);
|
||||||
|
|
||||||
|
|
|
@ -820,3 +820,25 @@ async fn macro_play_within_macro_record() -> anyhow::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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
|
||||||
|
" /«十分に長い マルチバイトキャラクター列» で検索<ret><esc>",
|
||||||
|
indoc! {"\
|
||||||
|
// Hello world!
|
||||||
|
// #[|
|
||||||
|
]#
|
||||||
|
"},
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -338,14 +338,21 @@ impl Buffer {
|
||||||
end_index -= 1;
|
end_index -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut graphemes = string.grapheme_indices(true);
|
||||||
|
|
||||||
if truncate_start {
|
if truncate_start {
|
||||||
self.content[start_index].set_symbol("…");
|
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 {
|
||||||
|
|
||||||
for (byte_offset, s) in graphemes.skip(truncate_start as usize) {
|
|
||||||
if start_index > end_index {
|
if start_index > end_index {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue