editor: Fix APCA contrast split text runs offset (#38751)
Closes #38576 In case of inline element rendering, we can have multiple text runs on the same display row. There was a bug in https://github.com/zed-industries/zed/pull/37165 which doesn't consider this multiple text runs case. This PR fixes that and adds a test for it. Before: <img width="600" alt="image" src="https://github.com/user-attachments/assets/3bdf5f14-988b-45dc-bc8e-c5d61ab35a93" /> After: <img width="600" alt="image" src="https://github.com/user-attachments/assets/0e1a45ff-c521-4994-b259-3a054d89c4df" /> Release Notes: - Fixed an issue where text could be incorrectly highlighted during search when a line contained an inline color preview.
This commit is contained in:
+119
-77
@@ -7616,7 +7616,7 @@ impl LineWithInvisibles {
|
||||
let text_runs: &[TextRun] = if segments.is_empty() {
|
||||
&styles
|
||||
} else {
|
||||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast)
|
||||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
|
||||
};
|
||||
let shaped_line = window.text_system().shape_line(
|
||||
line.clone().into(),
|
||||
@@ -7703,7 +7703,7 @@ impl LineWithInvisibles {
|
||||
let text_runs = if segments.is_empty() {
|
||||
&styles
|
||||
} else {
|
||||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast)
|
||||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
|
||||
};
|
||||
let shaped_line = window.text_system().shape_line(
|
||||
line.clone().into(),
|
||||
@@ -7802,9 +7802,10 @@ impl LineWithInvisibles {
|
||||
text_runs: &[TextRun],
|
||||
bg_segments: &[(Range<DisplayPoint>, Hsla)],
|
||||
min_contrast: f32,
|
||||
start_col_offset: usize,
|
||||
) -> Vec<TextRun> {
|
||||
let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
|
||||
let mut line_col = 0usize;
|
||||
let mut line_col = start_col_offset;
|
||||
let mut segment_ix = 0usize;
|
||||
|
||||
for text_run in text_runs.iter() {
|
||||
@@ -11254,102 +11255,143 @@ mod tests {
|
||||
fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let dx = |start: u32, end: u32| {
|
||||
DisplayPoint::new(DisplayRow(0), start)..DisplayPoint::new(DisplayRow(0), end)
|
||||
};
|
||||
|
||||
let text_color = Hsla {
|
||||
h: 210.0,
|
||||
s: 0.1,
|
||||
l: 0.4,
|
||||
a: 1.0,
|
||||
};
|
||||
let bg1 = Hsla {
|
||||
let bg_1 = Hsla {
|
||||
h: 30.0,
|
||||
s: 0.6,
|
||||
l: 0.8,
|
||||
a: 1.0,
|
||||
};
|
||||
let bg2 = Hsla {
|
||||
let bg_2 = Hsla {
|
||||
h: 200.0,
|
||||
s: 0.6,
|
||||
l: 0.2,
|
||||
a: 1.0,
|
||||
};
|
||||
let min_contrast = 45.0;
|
||||
let adjusted_bg1 = ensure_minimum_contrast(text_color, bg_1, min_contrast);
|
||||
let adjusted_bg2 = ensure_minimum_contrast(text_color, bg_2, min_contrast);
|
||||
|
||||
// Case A: single run; disjoint segments inside the run
|
||||
let runs = vec![generate_test_run(20, text_color)];
|
||||
let segs = vec![
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10),
|
||||
bg1,
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 16),
|
||||
bg2,
|
||||
),
|
||||
];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
|
||||
// Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![5, 5, 2, 4, 4]
|
||||
);
|
||||
assert_eq!(out[0].color, text_color);
|
||||
assert_eq!(
|
||||
out[1].color,
|
||||
ensure_minimum_contrast(text_color, bg1, min_contrast)
|
||||
);
|
||||
assert_eq!(out[2].color, text_color);
|
||||
assert_eq!(
|
||||
out[3].color,
|
||||
ensure_minimum_contrast(text_color, bg2, min_contrast)
|
||||
);
|
||||
assert_eq!(out[4].color, text_color);
|
||||
{
|
||||
let runs = vec![generate_test_run(20, text_color)];
|
||||
let segs = vec![(dx(5, 10), bg_1), (dx(12, 16), bg_2)];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||||
// Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![5, 5, 2, 4, 4]
|
||||
);
|
||||
assert_eq!(out[0].color, text_color);
|
||||
assert_eq!(out[1].color, adjusted_bg1);
|
||||
assert_eq!(out[2].color, text_color);
|
||||
assert_eq!(out[3].color, adjusted_bg2);
|
||||
assert_eq!(out[4].color, text_color);
|
||||
}
|
||||
|
||||
// Case B: multiple runs; segment extends to end of line (u32::MAX)
|
||||
let runs = vec![
|
||||
generate_test_run(8, text_color),
|
||||
generate_test_run(7, text_color),
|
||||
];
|
||||
let segs = vec![(
|
||||
DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), u32::MAX),
|
||||
bg1,
|
||||
)];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
|
||||
// Expected slices across runs: [0,6) [6,8) | [0,7)
|
||||
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
|
||||
let adjusted = ensure_minimum_contrast(text_color, bg1, min_contrast);
|
||||
assert_eq!(out[0].color, text_color);
|
||||
assert_eq!(out[1].color, adjusted);
|
||||
assert_eq!(out[2].color, adjusted);
|
||||
{
|
||||
let runs = vec![
|
||||
generate_test_run(8, text_color),
|
||||
generate_test_run(7, text_color),
|
||||
];
|
||||
let segs = vec![(dx(6, u32::MAX), bg_1)];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||||
// Expected slices across runs: [0,6) [6,8) | [0,7)
|
||||
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
|
||||
assert_eq!(out[0].color, text_color);
|
||||
assert_eq!(out[1].color, adjusted_bg1);
|
||||
assert_eq!(out[2].color, adjusted_bg1);
|
||||
}
|
||||
|
||||
// Case C: multi-byte characters
|
||||
// for text: "Hello 🌍 世界!"
|
||||
let runs = vec![
|
||||
generate_test_run(5, text_color), // "Hello"
|
||||
generate_test_run(6, text_color), // " 🌍 "
|
||||
generate_test_run(6, text_color), // "世界"
|
||||
generate_test_run(1, text_color), // "!"
|
||||
];
|
||||
// selecting "🌍 世"
|
||||
let segs = vec![(
|
||||
DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), 14),
|
||||
bg1,
|
||||
)];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast);
|
||||
// "Hello" | " " | "🌍 " | "世" | "界" | "!"
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![5, 1, 5, 3, 3, 1]
|
||||
);
|
||||
assert_eq!(out[0].color, text_color); // "Hello"
|
||||
assert_eq!(
|
||||
out[2].color,
|
||||
ensure_minimum_contrast(text_color, bg1, min_contrast)
|
||||
); // "🌍 "
|
||||
assert_eq!(
|
||||
out[3].color,
|
||||
ensure_minimum_contrast(text_color, bg1, min_contrast)
|
||||
); // "世"
|
||||
assert_eq!(out[4].color, text_color); // "界"
|
||||
assert_eq!(out[5].color, text_color); // "!"
|
||||
{
|
||||
// for text: "Hello 🌍 世界!"
|
||||
let runs = vec![
|
||||
generate_test_run(5, text_color), // "Hello"
|
||||
generate_test_run(6, text_color), // " 🌍 "
|
||||
generate_test_run(6, text_color), // "世界"
|
||||
generate_test_run(1, text_color), // "!"
|
||||
];
|
||||
// selecting "🌍 世"
|
||||
let segs = vec![(dx(6, 14), bg_1)];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||||
// "Hello" | " " | "🌍 " | "世" | "界" | "!"
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![5, 1, 5, 3, 3, 1]
|
||||
);
|
||||
assert_eq!(out[0].color, text_color); // "Hello"
|
||||
assert_eq!(out[2].color, adjusted_bg1); // "🌍 "
|
||||
assert_eq!(out[3].color, adjusted_bg1); // "世"
|
||||
assert_eq!(out[4].color, text_color); // "界"
|
||||
assert_eq!(out[5].color, text_color); // "!"
|
||||
}
|
||||
|
||||
// Case D: split multiple consecutive text runs with segments
|
||||
{
|
||||
let segs = vec![
|
||||
(dx(2, 4), bg_1), // selecting "cd"
|
||||
(dx(4, 8), bg_2), // selecting "efgh"
|
||||
(dx(9, 11), bg_1), // selecting "jk"
|
||||
(dx(12, 16), bg_2), // selecting "mnop"
|
||||
(dx(18, 19), bg_1), // selecting "s"
|
||||
];
|
||||
|
||||
// for text: "abcdef"
|
||||
let runs = vec![
|
||||
generate_test_run(2, text_color), // ab
|
||||
generate_test_run(4, text_color), // cdef
|
||||
];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||||
// new splits "ab", "cd", "ef"
|
||||
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![2, 2, 2]);
|
||||
assert_eq!(out[0].color, text_color);
|
||||
assert_eq!(out[1].color, adjusted_bg1);
|
||||
assert_eq!(out[2].color, adjusted_bg2);
|
||||
|
||||
// for text: "ghijklmn"
|
||||
let runs = vec![
|
||||
generate_test_run(3, text_color), // ghi
|
||||
generate_test_run(2, text_color), // jk
|
||||
generate_test_run(3, text_color), // lmn
|
||||
];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 6); // 2 + 4 from first run
|
||||
// new splits "gh", "i", "jk", "l", "mn"
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![2, 1, 2, 1, 2]
|
||||
);
|
||||
assert_eq!(out[0].color, adjusted_bg2);
|
||||
assert_eq!(out[1].color, text_color);
|
||||
assert_eq!(out[2].color, adjusted_bg1);
|
||||
assert_eq!(out[3].color, text_color);
|
||||
assert_eq!(out[4].color, adjusted_bg2);
|
||||
|
||||
// for text: "opqrs"
|
||||
let runs = vec![
|
||||
generate_test_run(1, text_color), // o
|
||||
generate_test_run(4, text_color), // pqrs
|
||||
];
|
||||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 14); // 6 + 3 + 2 + 3 from first two runs
|
||||
// new splits "o", "p", "qr", "s"
|
||||
assert_eq!(
|
||||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||||
vec![1, 1, 2, 1]
|
||||
);
|
||||
assert_eq!(out[0].color, adjusted_bg2);
|
||||
assert_eq!(out[1].color, adjusted_bg2);
|
||||
assert_eq!(out[2].color, text_color);
|
||||
assert_eq!(out[3].color, adjusted_bg1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user