Skip to main content

rgx/ui/
match_display.rs

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        // Replace preview section
88        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}