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}
19
20impl<'a> Widget for MatchDisplay<'a> {
21 fn render(self, area: Rect, buf: &mut Buffer) {
22 let title = if self.replace_result.is_some() {
23 format!(" Matches ({}) | Preview ", self.matches.len())
24 } else {
25 format!(" Matches ({}) ", self.matches.len())
26 };
27 let border_color = if self.focused {
28 theme::BLUE
29 } else {
30 theme::OVERLAY
31 };
32 let block = Block::default()
33 .borders(Borders::ALL)
34 .border_style(Style::default().fg(border_color))
35 .title(Span::styled(title, Style::default().fg(theme::TEXT)));
36
37 if self.matches.is_empty() {
38 let paragraph = Paragraph::new(Line::from(Span::styled(
39 "No matches",
40 Style::default().fg(theme::SUBTEXT),
41 )))
42 .block(block)
43 .style(Style::default().bg(theme::BASE));
44 paragraph.render(area, buf);
45 return;
46 }
47
48 let mut lines = Vec::new();
49 for (i, m) in self.matches.iter().enumerate() {
50 lines.push(Line::from(vec![
51 Span::styled(
52 format!("Match {} ", i + 1),
53 Style::default().fg(theme::BLUE),
54 ),
55 Span::styled(
56 format!("[{}-{}]: ", m.start, m.end),
57 Style::default().fg(theme::SUBTEXT),
58 ),
59 Span::styled(
60 format!("\"{}\"", &m.text),
61 Style::default().fg(theme::GREEN),
62 ),
63 ]));
64
65 for cap in &m.captures {
66 let color = theme::capture_color(cap.index.saturating_sub(1));
67 let name_str = cap
68 .name
69 .as_ref()
70 .map(|n| format!(" '{n}'"))
71 .unwrap_or_default();
72 lines.push(Line::from(vec![
73 Span::styled(" ", Style::default()),
74 Span::styled(
75 format!("Group #{}{name_str} ", cap.index),
76 Style::default().fg(color),
77 ),
78 Span::styled(
79 format!("[{}-{}]: ", cap.start, cap.end),
80 Style::default().fg(theme::SUBTEXT),
81 ),
82 Span::styled(format!("\"{}\"", &cap.text), Style::default().fg(color)),
83 ]));
84 }
85 }
86
87 if let Some(result) = self.replace_result {
89 lines.push(Line::from(Span::styled(
90 "─── Replace Preview ───",
91 Style::default().fg(theme::OVERLAY),
92 )));
93
94 let preview_spans = build_replace_preview_spans(result);
95 let preview_lines = split_spans_into_lines(preview_spans);
96 lines.extend(preview_lines);
97 }
98
99 let paragraph = Paragraph::new(lines)
100 .block(block)
101 .style(Style::default().bg(theme::BASE))
102 .wrap(Wrap { trim: false })
103 .scroll((self.scroll, 0));
104
105 paragraph.render(area, buf);
106 }
107}
108
109fn build_replace_preview_spans(result: &engine::ReplaceResult) -> Vec<Span<'_>> {
110 let mut spans = Vec::new();
111 for seg in &result.segments {
112 let text = &result.output[seg.start..seg.end];
113 let style = if seg.is_replacement {
114 Style::default()
115 .fg(theme::BASE)
116 .bg(theme::GREEN)
117 .add_modifier(Modifier::BOLD)
118 } else {
119 Style::default().fg(theme::TEXT)
120 };
121 spans.push(Span::styled(text.to_string(), style));
122 }
123 if spans.is_empty() {
124 spans.push(Span::styled("(empty)", Style::default().fg(theme::SUBTEXT)));
125 }
126 spans
127}