1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, Paragraph, Widget, Wrap},
7};
8
9use crate::engine;
10use crate::ui::test_input::split_spans_into_lines;
11use crate::ui::theme;
12
13pub struct MatchDisplay<'a> {
14 pub matches: &'a [engine::Match],
15 pub replace_result: Option<&'a engine::ReplaceResult>,
16 pub scroll: u16,
17 pub focused: bool,
18 pub selected_match: usize,
19 pub selected_capture: Option<usize>,
20 pub clipboard_status: Option<&'a str>,
21}
22
23impl<'a> Widget for MatchDisplay<'a> {
24 fn render(self, area: Rect, buf: &mut Buffer) {
25 let title = if self.replace_result.is_some() {
26 format!(" Matches ({}) | Preview ", self.matches.len())
27 } else {
28 format!(" Matches ({}) ", self.matches.len())
29 };
30 let border_color = if self.focused {
31 theme::BLUE
32 } else {
33 theme::OVERLAY
34 };
35 let block = Block::default()
36 .borders(Borders::ALL)
37 .border_style(Style::default().fg(border_color))
38 .title(Span::styled(title, Style::default().fg(theme::TEXT)));
39
40 if self.matches.is_empty() {
41 let paragraph = Paragraph::new(Line::from(Span::styled(
42 "No matches",
43 Style::default().fg(theme::SUBTEXT),
44 )))
45 .block(block)
46 .style(Style::default().bg(theme::BASE));
47 paragraph.render(area, buf);
48 return;
49 }
50
51 let is_selected_panel = self.focused;
52 let mut lines = Vec::new();
53 for (i, m) in self.matches.iter().enumerate() {
54 let match_selected =
55 is_selected_panel && i == self.selected_match && self.selected_capture.is_none();
56 let prefix = if match_selected { ">> " } else { " " };
57 let bg = if match_selected {
58 theme::SURFACE1
59 } else {
60 theme::BASE
61 };
62 lines.push(Line::from(vec![
63 Span::styled(prefix, Style::default().fg(theme::BLUE).bg(bg)),
64 Span::styled(
65 format!("Match {} ", i + 1),
66 Style::default().fg(theme::BLUE).bg(bg),
67 ),
68 Span::styled(
69 format!("[{}-{}]: ", m.start, m.end),
70 Style::default().fg(theme::SUBTEXT).bg(bg),
71 ),
72 Span::styled(
73 format!("\"{}\"", &m.text),
74 Style::default().fg(theme::GREEN).bg(bg),
75 ),
76 ]));
77
78 for (ci, cap) in m.captures.iter().enumerate() {
79 let cap_selected = is_selected_panel
80 && i == self.selected_match
81 && self.selected_capture == Some(ci);
82 let prefix = if cap_selected { ">> " } else { " " };
83 let bg = if cap_selected {
84 theme::SURFACE1
85 } else {
86 theme::BASE
87 };
88 let color = theme::capture_color(cap.index.saturating_sub(1));
89 let name_str = cap
90 .name
91 .as_ref()
92 .map(|n| format!(" '{n}'"))
93 .unwrap_or_default();
94 lines.push(Line::from(vec![
95 Span::styled(prefix, Style::default().fg(color).bg(bg)),
96 Span::styled(
97 format!("Group #{}{name_str} ", cap.index),
98 Style::default().fg(color).bg(bg),
99 ),
100 Span::styled(
101 format!("[{}-{}]: ", cap.start, cap.end),
102 Style::default().fg(theme::SUBTEXT).bg(bg),
103 ),
104 Span::styled(
105 format!("\"{}\"", &cap.text),
106 Style::default().fg(color).bg(bg),
107 ),
108 ]));
109 }
110 }
111
112 if let Some(result) = self.replace_result {
114 lines.push(Line::from(Span::styled(
115 "─── Replace Preview ───",
116 Style::default().fg(theme::OVERLAY),
117 )));
118
119 let preview_spans = build_replace_preview_spans(result);
120 let preview_lines = split_spans_into_lines(preview_spans);
121 lines.extend(preview_lines);
122 }
123
124 if let Some(status) = self.clipboard_status {
126 lines.push(Line::from(Span::styled(
127 status.to_string(),
128 Style::default().fg(theme::YELLOW),
129 )));
130 }
131
132 let paragraph = Paragraph::new(lines)
133 .block(block)
134 .style(Style::default().bg(theme::BASE))
135 .wrap(Wrap { trim: false })
136 .scroll((self.scroll, 0));
137
138 paragraph.render(area, buf);
139 }
140}
141
142fn build_replace_preview_spans(result: &engine::ReplaceResult) -> Vec<Span<'_>> {
143 let mut spans = Vec::new();
144 for seg in &result.segments {
145 let text = &result.output[seg.start..seg.end];
146 let style = if seg.is_replacement {
147 Style::default()
148 .fg(theme::BASE)
149 .bg(theme::GREEN)
150 .add_modifier(Modifier::BOLD)
151 } else {
152 Style::default().fg(theme::TEXT)
153 };
154 spans.push(Span::styled(text.to_string(), style));
155 }
156 if spans.is_empty() {
157 spans.push(Span::styled("(empty)", Style::default().fg(theme::SUBTEXT)));
158 }
159 spans
160}