mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 21:32:40 +00:00
Fix line truncate crash on Windows (#17271)
Closes #17267 We should update the `len` of `runs` when truncating. cc @huacnlee Release Notes: - N/A
This commit is contained in:
parent
33e84da657
commit
49ed932c1f
2 changed files with 174 additions and 27 deletions
|
@ -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())]
|
||||
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -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.);
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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::*;
|
||||
|
@ -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();
|
||||
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue