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