Fix soft-wrapping with fold creases (#28029)

Release Notes:

- Fixed a rendering bug that caused context in the agent to not wrap
properly.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Zed AI <ai+claude-3.7@zed.dev>
This commit is contained in:
Antonio Scandurra 2025-04-03 19:33:08 +02:00 committed by GitHub
parent ed3722023e
commit e123c4bced
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 456 additions and 128 deletions

View file

@ -32,7 +32,7 @@ impl LineWrapper {
/// Wrap a line of text to the given width with this wrapper's font and font size.
pub fn wrap_line<'a>(
&'a mut self,
line: &'a str,
fragments: &'a [LineFragment],
wrap_width: Pixels,
) -> impl Iterator<Item = Boundary> + 'a {
let mut width = px(0.);
@ -42,32 +42,61 @@ impl LineWrapper {
let mut last_candidate_width = px(0.);
let mut last_wrap_ix = 0;
let mut prev_c = '\0';
let mut char_indices = line.char_indices();
let mut index = 0;
let mut candidates = fragments
.into_iter()
.flat_map(move |fragment| fragment.wrap_boundary_candidates())
.peekable();
iter::from_fn(move || {
for (ix, c) in char_indices.by_ref() {
if c == '\n' {
continue;
}
for candidate in candidates.by_ref() {
let ix = index;
index += candidate.len_utf8();
let mut new_prev_c = prev_c;
let item_width = match candidate {
WrapBoundaryCandidate::Char { character: c } => {
if c == '\n' {
continue;
}
if Self::is_word_char(c) {
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
if Self::is_word_char(c) {
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
} else {
// CJK may not be space separated, e.g.: `Hello world你好世界`
if c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
}
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
new_prev_c = c;
self.width_for_char(c)
}
} else {
// CJK may not be space separated, e.g.: `Hello world你好世界`
if c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
WrapBoundaryCandidate::Element {
width: element_width,
..
} => {
if prev_c == ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
if first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
element_width
}
}
};
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
let char_width = self.width_for_char(c);
width += char_width;
width += item_width;
if width > wrap_width && ix > last_wrap_ix {
if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
{
@ -82,7 +111,7 @@ impl LineWrapper {
last_candidate_ix = 0;
} else {
last_wrap_ix = ix;
width = char_width;
width = item_width;
}
if let Some(indent) = indent {
@ -91,7 +120,8 @@ impl LineWrapper {
return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
}
prev_c = c;
prev_c = new_prev_c;
}
None
@ -213,6 +243,65 @@ fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<Tex
}
}
/// A fragment of a line that can be wrapped.
pub enum LineFragment<'a> {
/// A text fragment consisting of characters.
Text {
/// The text content of the fragment.
text: &'a str,
},
/// A non-text element with a fixed width.
Element {
/// The width of the element in pixels.
width: Pixels,
/// The UTF-8 encoded length of the element.
len_utf8: usize,
},
}
impl<'a> LineFragment<'a> {
/// Creates a new text fragment from the given text.
pub fn text(text: &'a str) -> Self {
LineFragment::Text { text }
}
/// Creates a new non-text element with the given width and UTF-8 encoded length.
pub fn element(width: Pixels, len_utf8: usize) -> Self {
LineFragment::Element { width, len_utf8 }
}
fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
let text = match self {
LineFragment::Text { text } => text,
LineFragment::Element { .. } => "\0",
};
text.chars().map(move |character| {
if let LineFragment::Element { width, len_utf8 } = self {
WrapBoundaryCandidate::Element {
width: *width,
len_utf8: *len_utf8,
}
} else {
WrapBoundaryCandidate::Char { character }
}
})
}
}
enum WrapBoundaryCandidate {
Char { character: char },
Element { width: Pixels, len_utf8: usize },
}
impl WrapBoundaryCandidate {
pub fn len_utf8(&self) -> usize {
match self {
WrapBoundaryCandidate::Char { character } => character.len_utf8(),
WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
}
}
}
/// A boundary between two lines of text.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Boundary {
@ -278,7 +367,7 @@ mod tests {
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", px(72.))
.wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@ -288,7 +377,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
.wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
.collect::<Vec<_>>(),
&[
Boundary::new(4, 0),
@ -298,7 +387,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaa", px(72.))
.wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
@ -308,7 +397,10 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" ", px(72.))
.wrap_line(
&[LineFragment::text(" ")],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@ -318,7 +410,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", px(72.))
.wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@ -327,6 +419,84 @@ mod tests {
Boundary::new(22, 3),
]
);
// Test wrapping multiple text fragments
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("aa bbb "),
LineFragment::text("cccc ddddd eeee")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
// Test wrapping with a mix of text and element fragments
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("aa "),
LineFragment::element(px(20.), 1),
LineFragment::text(" bbb "),
LineFragment::element(px(30.), 1),
LineFragment::text(" cccc")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(5, 0),
Boundary::new(9, 0),
Boundary::new(11, 0)
],
);
// Test with element at the beginning and text afterward
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::element(px(50.), 1),
LineFragment::text(" aaaa bbbb cccc dddd")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(2, 0),
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(17, 0)
],
);
// Test with a large element that forces wrapping by itself
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("short text "),
LineFragment::element(px(100.), 1),
LineFragment::text(" more text")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(6, 0),
Boundary::new(11, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
}
#[test]