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, 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        // Replace preview section
115        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        // Clipboard status
127        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}