Skip to content

Commit

Permalink
Fix line truncate crash on Windows (zed-industries#17271)
Browse files Browse the repository at this point in the history
Closes zed-industries#17267

We should update the `len` of `runs` when truncating. cc @huacnlee 

Release Notes:

- N/A
  • Loading branch information
JunkuiZhang authored and Anthony-Eid committed Nov 22, 2024
1 parent ec17527 commit 2a8ba4f
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 27 deletions.
4 changes: 2 additions & 2 deletions crates/gpui/src/elements/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ impl TextLayout {
.line_height
.to_pixels(font_size.into(), cx.rem_size());

let runs = if let Some(runs) = runs {
let mut runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
Expand Down Expand Up @@ -306,7 +306,7 @@ impl TextLayout {

let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
} else {
text.clone()
};
Expand Down
197 changes: 172 additions & 25 deletions crates/gpui/src/text_system/line_wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString};
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun};
use collections::HashMap;
use std::{iter, sync::Arc};

Expand Down Expand Up @@ -104,6 +104,7 @@ impl LineWrapper {
line: SharedString,
truncate_width: Pixels,
ellipsis: Option<&str>,
runs: &mut Vec<TextRun>,
) -> SharedString {
let mut width = px(0.);
let mut ellipsis_width = px(0.);
Expand All @@ -124,15 +125,15 @@ impl LineWrapper {
width += char_width;

if width.floor() > truncate_width {
return SharedString::from(format!(
"{}{}",
&line[..truncate_ix],
ellipsis.unwrap_or("")
));
let ellipsis = ellipsis.unwrap_or("");
let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis));
update_runs_after_truncation(&result, ellipsis, runs);

return result;
}
}

line.clone()
line
}

pub(crate) fn is_word_char(c: char) -> bool {
Expand Down Expand Up @@ -195,6 +196,23 @@ impl LineWrapper {
}
}

fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
let mut truncate_at = result.len() - ellipsis.len();
let mut run_end = None;
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
run_end = Some(run_index + 1);
break;
}
}
if let Some(run_end) = run_end {
runs.truncate(run_end);
}
}

/// A boundary between two lines of text.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Boundary {
Expand All @@ -213,7 +231,9 @@ impl Boundary {
#[cfg(test)]
mod tests {
use super::*;
use crate::{font, TestAppContext, TestDispatcher};
use crate::{
font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher,
};
#[cfg(target_os = "macos")]
use crate::{TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
Expand All @@ -232,6 +252,26 @@ mod tests {
LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
}

fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
input_run_len
.iter()
.map(|run_len| TextRun {
len: *run_len,
font: Font {
family: "Dummy".into(),
features: FontFeatures::default(),
fallbacks: None,
weight: FontWeight::default(),
style: FontStyle::Normal,
},
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
})
.collect()
}

#[test]
fn test_wrap_line() {
let mut wrapper = build_wrapper();
Expand Down Expand Up @@ -293,28 +333,135 @@ mod tests {
fn test_truncate_line() {
let mut wrapper = build_wrapper();

assert_eq!(
wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None),
"aa bbb cccc ddddd eeee"
fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
result: &'static str,
ellipsis: Option<&str>,
) {
let dummy_run_lens = vec![text.len()];
let mut dummy_runs = generate_test_runs(&dummy_run_lens);
assert_eq!(
wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs),
result
);
assert_eq!(dummy_runs.first().unwrap().len, result.len());
}

perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc ddddd eeee",
None,
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("…")
),
"aa bbb cccc ddddd eee…"
perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc ddddd eee…",
Some("…"),
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("......")
),
"aa bbb cccc dddd......"
perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc dddd......",
Some("......"),
);
}

#[test]
fn test_truncate_multiple_runs() {
let mut wrapper = build_wrapper();

fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
result: &str,
run_lens: &[usize],
result_run_len: &[usize],
line_width: Pixels,
) {
let mut dummy_runs = generate_test_runs(run_lens);
assert_eq!(
wrapper.truncate_line(text.into(), line_width, Some("…"), &mut dummy_runs),
result
);
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
assert_eq!(run.len, *result_len);
}
}
// Case 0: Normal
// Text: abcdefghijkl
// Runs: Run0 { len: 12, ... }
//
// Truncate res: abcd… (truncate_at = 4)
// Run res: Run0 { string: abcd…, len: 7, ... }
perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
// Case 1: Drop some runs
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdef… (truncate_at = 6)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
// 5, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"abcdef…",
&[4, 4, 4],
&[4, 5],
px(70.),
);
// Case 2: Truncate at start of some run
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdefgh… (truncate_at = 8)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
// 4, ... }, Run2 { string: …, len: 3, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"abcdefgh…",
&[4, 4, 4],
&[4, 4, 3],
px(90.),
);
}

#[test]
fn test_update_run_after_truncation() {
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
let mut dummy_runs = generate_test_runs(run_lens);
update_runs_after_truncation(result, "…", &mut dummy_runs);
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
assert_eq!(run.len, *result_len);
}
}
// Case 0: Normal
// Text: abcdefghijkl
// Runs: Run0 { len: 12, ... }
//
// Truncate res: abcd… (truncate_at = 4)
// Run res: Run0 { string: abcd…, len: 7, ... }
perform_test("abcd…", &[12], &[7]);
// Case 1: Drop some runs
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdef… (truncate_at = 6)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
// 5, ... }
perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
// Case 2: Truncate at start of some run
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdefgh… (truncate_at = 8)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
// 4, ... }, Run2 { string: …, len: 3, ... }
perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
}

#[test]
fn test_is_word_char() {
#[track_caller]
Expand Down

0 comments on commit 2a8ba4f

Please sign in to comment.