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:
Smit Barmase
2025-09-24 04:34:35 +05:30
committed by GitHub
parent b60f19f71e
commit 0f7dbf57f5
+119 -77
View File
@@ -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);
}
}
}