2424
2525use rio_backend:: config:: colors:: term:: TermColors ;
2626use rio_backend:: crosswords:: grid:: row:: Row ;
27- use rio_backend:: crosswords:: pos:: { Column , Line } ;
27+ use rio_backend:: crosswords:: pos:: { Column , Line , Pos } ;
28+ use rio_backend:: crosswords:: search:: Match ;
2829use rio_backend:: crosswords:: square:: { ContentTag , Square } ;
2930use rio_backend:: crosswords:: style:: { StyleFlags , StyleSet } ;
3031use rio_backend:: selection:: SelectionRange ;
@@ -92,6 +93,127 @@ fn cell_in_row_sel(row_sel: Option<RowSelection>, col: u16) -> bool {
9293 }
9394}
9495
96+ /// Search-hint category at a cell. Matches Ghostty's `HighlightTag`
97+ /// (`ghostty/src/renderer/generic.zig:240`) — we use the same two-way
98+ /// split so `search_focused_match_background` can override the regular
99+ /// match color on the currently-focused hit.
100+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
101+ pub enum HintTag {
102+ Match ,
103+ Focused ,
104+ }
105+
106+ /// Per-row hint interval, closed on both ends. Several `RowHint`s may
107+ /// exist on one row (when the row contains multiple matches).
108+ #[ derive( Clone , Copy , Debug ) ]
109+ pub struct RowHint {
110+ pub lo : u16 ,
111+ pub hi : u16 ,
112+ pub tag : HintTag ,
113+ }
114+
115+ /// Compute the hint-match intervals (if any) for visible row `y`.
116+ /// Linear-selection semantics: a match can span multiple rows; first
117+ /// / last rows clip to the match's column bounds; interior rows cover
118+ /// the full width. Mirrors `row_selection_for`.
119+ ///
120+ /// `focused_match` is pushed first so it wins `cell_in_row_hints`
121+ /// iteration order when it overlaps another match — same precedence
122+ /// as Ghostty (`generic.zig:1330-1353`: "The order below matters.
123+ /// Highlights added earlier will take priority").
124+ pub fn row_hints_for (
125+ hint_matches : Option < & [ Match ] > ,
126+ focused_match : Option < & Match > ,
127+ y : usize ,
128+ cols : usize ,
129+ display_offset : i32 ,
130+ out : & mut Vec < RowHint > ,
131+ ) {
132+ out. clear ( ) ;
133+ if cols == 0 {
134+ return ;
135+ }
136+ let Some ( matches) = hint_matches else {
137+ return ;
138+ } ;
139+ let line = Line ( ( y as i32 ) - display_offset) ;
140+ let cols_max = cols. saturating_sub ( 1 ) as u16 ;
141+
142+ let to_row_hint = |m : & Match , tag : HintTag | -> Option < RowHint > {
143+ let start = * m. start ( ) ;
144+ let end = * m. end ( ) ;
145+ if line < start. row || line > end. row {
146+ return None ;
147+ }
148+ let lo = if line == start. row {
149+ start. col . 0 as u16
150+ } else {
151+ 0
152+ } ;
153+ let hi = if line == end. row {
154+ end. col . 0 as u16
155+ } else {
156+ cols_max
157+ } ;
158+ Some ( RowHint {
159+ lo : lo. min ( cols_max) ,
160+ hi : hi. min ( cols_max) ,
161+ tag,
162+ } )
163+ } ;
164+
165+ let is_same_match = |a : & Match , b : & Match | -> bool {
166+ let ( a_start, a_end) = ( * a. start ( ) , * a. end ( ) ) ;
167+ let ( b_start, b_end) = ( * b. start ( ) , * b. end ( ) ) ;
168+ pos_eq ( a_start, b_start) && pos_eq ( a_end, b_end)
169+ } ;
170+
171+ if let Some ( fm) = focused_match {
172+ if let Some ( rh) = to_row_hint ( fm, HintTag :: Focused ) {
173+ out. push ( rh) ;
174+ }
175+ }
176+ for m in matches {
177+ if let Some ( fm) = focused_match {
178+ if is_same_match ( m, fm) {
179+ continue ;
180+ }
181+ }
182+ if let Some ( rh) = to_row_hint ( m, HintTag :: Match ) {
183+ out. push ( rh) ;
184+ }
185+ }
186+ }
187+
188+ #[ inline]
189+ fn pos_eq ( a : Pos , b : Pos ) -> bool {
190+ a. row == b. row && a. col == b. col
191+ }
192+
193+ #[ inline]
194+ fn cell_in_row_hints ( row_hints : & [ RowHint ] , col : u16 ) -> Option < HintTag > {
195+ for rh in row_hints {
196+ if col >= rh. lo && col <= rh. hi {
197+ return Some ( rh. tag ) ;
198+ }
199+ }
200+ None
201+ }
202+
203+ /// Foreground for a hint-matched cell. Mirrors `cell_fg_selected` but
204+ /// uses the configured `search_match_foreground` /
205+ /// `search_focused_match_foreground` from
206+ /// `colors::Colors` (`rio-backend/src/config/colors/mod.rs:287,299`).
207+ #[ inline]
208+ fn cell_fg_hinted ( tag : HintTag , renderer : & Renderer ) -> [ u8 ; 4 ] {
209+ match tag {
210+ HintTag :: Focused => {
211+ normalized_to_u8 ( renderer. named_colors . search_focused_match_foreground )
212+ }
213+ HintTag :: Match => normalized_to_u8 ( renderer. named_colors . search_match_foreground ) ,
214+ }
215+ }
216+
95217use rio_backend:: sugarloaf:: font:: FontLibrary ;
96218use rio_backend:: sugarloaf:: grid:: {
97219 AtlasSlot , CellBg , CellText , GlyphKey , GridRenderer , RasterizedGlyph ,
@@ -425,6 +547,7 @@ pub fn build_row_bg(
425547 renderer : & Renderer ,
426548 term_colors : & TermColors ,
427549 row_sel : Option < RowSelection > ,
550+ row_hints : & [ RowHint ] ,
428551 bg_scratch : & mut Vec < CellBg > ,
429552) {
430553 bg_scratch. clear ( ) ;
@@ -434,12 +557,33 @@ pub fn build_row_bg(
434557 } else {
435558 None
436559 } ;
560+ // Precompute hint bg colors if the row has any hints. Both variants
561+ // are cheap enough to compute unconditionally when the row is hit.
562+ let ( match_bg, focused_bg) = if !row_hints. is_empty ( ) {
563+ (
564+ Some ( normalized_to_u8 ( renderer. named_colors . search_match_background ) ) ,
565+ Some ( normalized_to_u8 (
566+ renderer. named_colors . search_focused_match_background ,
567+ ) ) ,
568+ )
569+ } else {
570+ ( None , None )
571+ } ;
437572 for x in 0 ..cols {
438573 let sq = row[ Column ( x) ] ;
439- let rgba = if cell_in_row_sel ( row_sel, x as u16 ) {
440- // Selection bg wins over the cell's own bg, matching Ghostty
441- // `generic.zig:2817` (`.selection` branch).
574+ let col = x as u16 ;
575+ let rgba = if cell_in_row_sel ( row_sel, col) {
576+ // Selection bg wins over hint bg and the cell's own bg,
577+ // matching Ghostty `generic.zig:2775-2800` (selection check
578+ // runs before highlight check).
442579 sel_bg. unwrap_or_else ( || cell_bg ( sq, style_set, renderer, term_colors) )
580+ } else if let Some ( tag) = cell_in_row_hints ( row_hints, col) {
581+ match tag {
582+ HintTag :: Focused => focused_bg
583+ . unwrap_or_else ( || cell_bg ( sq, style_set, renderer, term_colors) ) ,
584+ HintTag :: Match => match_bg
585+ . unwrap_or_else ( || cell_bg ( sq, style_set, renderer, term_colors) ) ,
586+ }
443587 } else {
444588 cell_bg ( sq, style_set, renderer, term_colors)
445589 } ;
@@ -802,6 +946,7 @@ pub fn build_row_fg(
802946 cell_w : f32 ,
803947 cell_h : f32 ,
804948 row_sel : Option < RowSelection > ,
949+ row_hints : & [ RowHint ] ,
805950 font_library : & FontLibrary ,
806951 fg_scratch : & mut Vec < CellText > ,
807952) {
@@ -828,6 +973,7 @@ pub fn build_row_fg(
828973 cell_h_u32,
829974 thickness,
830975 row_sel,
976+ row_hints,
831977 fg_scratch,
832978 ) ;
833979
@@ -1025,15 +1171,26 @@ pub fn build_row_fg(
10251171 ( run_start + cell_idx_in_run as usize ) . min ( cols. saturating_sub ( 1 ) ) ;
10261172 let src_sq = row[ Column ( src_col) ] ;
10271173 let is_sel = cell_in_row_sel ( row_sel, src_col as u16 ) ;
1174+ let hint_tag = if is_sel {
1175+ None
1176+ } else {
1177+ cell_in_row_hints ( row_hints, src_col as u16 )
1178+ } ;
10281179 let ( atlas, color) = if is_color {
1029- // Colour glyphs (emoji) don't take the selection-fg swap —
1030- // matches Ghostty's behaviour for bitmap/COLR atlas entries.
1180+ // Colour glyphs (emoji) don't take the selection-fg /
1181+ // hint-fg swap — matches Ghostty's behaviour for
1182+ // bitmap/COLR atlas entries.
10311183 ( CellText :: ATLAS_COLOR , [ 255 , 255 , 255 , 255 ] )
10321184 } else if is_sel {
10331185 (
10341186 CellText :: ATLAS_GRAYSCALE ,
10351187 cell_fg_selected ( src_sq, style_set, renderer, term_colors) ,
10361188 )
1189+ } else if let Some ( tag) = hint_tag {
1190+ // Hint-fg wins over the cell's own fg, matching
1191+ // Ghostty's `.search` / `.search_selected` branches at
1192+ // `generic.zig:2829-2833` (the fg picker mirrors bg).
1193+ ( CellText :: ATLAS_GRAYSCALE , cell_fg_hinted ( tag, renderer) )
10371194 } else {
10381195 (
10391196 CellText :: ATLAS_GRAYSCALE ,
@@ -1070,6 +1227,7 @@ pub fn build_row_fg(
10701227 cell_h_u32,
10711228 thickness,
10721229 row_sel,
1230+ row_hints,
10731231 fg_scratch,
10741232 ) ;
10751233}
@@ -1087,6 +1245,7 @@ fn emit_underlines(
10871245 cell_h : u32 ,
10881246 thickness : u32 ,
10891247 row_sel : Option < RowSelection > ,
1248+ row_hints : & [ RowHint ] ,
10901249 fg_scratch : & mut Vec < CellText > ,
10911250) {
10921251 for x in 0 ..cols {
@@ -1102,12 +1261,17 @@ fn emit_underlines(
11021261 if slot. w == 0 || slot. h == 0 {
11031262 continue ;
11041263 }
1105- let color = if cell_in_row_sel ( row_sel, x as u16 ) {
1264+ let col = x as u16 ;
1265+ let color = if cell_in_row_sel ( row_sel, col) {
11061266 // Inside selection: underline follows the selection fg so
11071267 // it stays visible against the selection bg. SGR 58 is
11081268 // suppressed here — a theme's selection_foreground
11091269 // overrides per-cell decoration color.
11101270 cell_fg_selected ( sq, style_set, renderer, term_colors)
1271+ } else if let Some ( tag) = cell_in_row_hints ( row_hints, col) {
1272+ // Same reasoning as selection: underline inside a hint
1273+ // should stay legible on the hint bg.
1274+ cell_fg_hinted ( tag, renderer)
11111275 } else {
11121276 decoration_color ( sq, & style, style_set, renderer, term_colors)
11131277 } ;
@@ -1137,6 +1301,7 @@ fn emit_strikethroughs(
11371301 cell_h : u32 ,
11381302 thickness : u32 ,
11391303 row_sel : Option < RowSelection > ,
1304+ row_hints : & [ RowHint ] ,
11401305 fg_scratch : & mut Vec < CellText > ,
11411306) {
11421307 for x in 0 ..cols {
@@ -1157,10 +1322,13 @@ fn emit_strikethroughs(
11571322 if slot. w == 0 || slot. h == 0 {
11581323 continue ;
11591324 }
1325+ let col = x as u16 ;
11601326 // Strikethrough always uses the cell fg (there's no SGR for
11611327 // a separate strike color, matching Ghostty).
1162- let color = if cell_in_row_sel ( row_sel, x as u16 ) {
1328+ let color = if cell_in_row_sel ( row_sel, col ) {
11631329 cell_fg_selected ( sq, style_set, renderer, term_colors)
1330+ } else if let Some ( tag) = cell_in_row_hints ( row_hints, col) {
1331+ cell_fg_hinted ( tag, renderer)
11641332 } else {
11651333 cell_fg ( sq, style_set, renderer, term_colors)
11661334 } ;
0 commit comments