From 0f7dbf57f5e690780cf7b388d19fa1378e297a6e Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 24 Sep 2025 04:34:35 +0530 Subject: [PATCH] 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: image After: image Release Notes: - Fixed an issue where text could be incorrectly highlighted during search when a line contained an inline color preview. --- crates/editor/src/element.rs | 196 +++++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 77 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 28fe68e71cb4fac36f84d1161020e16ba2d0605f..72b84b4599b681088b6cc4aa5afd705d8af229f8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, Hsla)], min_contrast: f32, + start_col_offset: usize, ) -> Vec { let mut output_runs: Vec = 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![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![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![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![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![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![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![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![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![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); + } } }