1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, Paragraph, Widget},
7};
8
9use crate::engine;
10use crate::input::editor::Editor;
11use crate::ui::theme;
12
13pub struct TestInput<'a> {
14 pub editor: &'a Editor,
15 pub focused: bool,
16 pub matches: &'a [engine::Match],
17 pub show_whitespace: bool,
18}
19
20impl<'a> Widget for TestInput<'a> {
21 fn render(self, area: Rect, buf: &mut Buffer) {
22 let border_style = if self.focused {
23 Style::default().fg(theme::BLUE)
24 } else {
25 Style::default().fg(theme::OVERLAY)
26 };
27
28 let block = Block::default()
29 .borders(Borders::ALL)
30 .border_style(border_style)
31 .title(Span::styled(
32 " Test String ",
33 Style::default().fg(theme::TEXT),
34 ));
35
36 let content = self.editor.content();
37 let flat_spans = build_highlighted_spans(content, self.matches);
38 let flat_spans = if self.show_whitespace {
39 visualize_whitespace(flat_spans)
40 } else {
41 flat_spans
42 };
43 let lines = split_spans_into_lines(flat_spans);
44
45 let v_scroll = self.editor.vertical_scroll();
47 let inner_height = (area.height as usize).saturating_sub(2); let visible_lines: Vec<Line> = lines
49 .into_iter()
50 .skip(v_scroll)
51 .take(inner_height)
52 .collect();
53
54 let paragraph = Paragraph::new(visible_lines)
55 .block(block)
56 .style(Style::default().bg(theme::BASE));
57
58 paragraph.render(area, buf);
59
60 if self.focused {
62 let (cursor_line, cursor_col) = self.editor.cursor_line_col();
63 let visual_col = cursor_col.saturating_sub(self.editor.scroll_offset());
64 let visual_row = cursor_line.saturating_sub(v_scroll);
65 let cursor_x = area.x + 1 + visual_col as u16;
66 let cursor_y = area.y + 1 + visual_row as u16;
67 if cursor_x < area.x + area.width - 1 && cursor_y < area.y + area.height - 1 {
68 if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
69 cell.set_style(
70 Style::default()
71 .fg(theme::BASE)
72 .bg(theme::TEXT)
73 .add_modifier(Modifier::BOLD),
74 );
75 }
76 }
77 }
78 }
79}
80
81pub fn split_spans_into_lines<'a>(spans: Vec<Span<'a>>) -> Vec<Line<'a>> {
83 let mut lines: Vec<Line<'a>> = Vec::new();
84 let mut current_spans: Vec<Span<'a>> = Vec::new();
85
86 for span in spans {
87 let style = span.style;
88 let text: &str = span.content.as_ref();
89
90 let mut remaining = text;
91 while let Some(nl_pos) = remaining.find('\n') {
92 let before = &remaining[..nl_pos];
93 if !before.is_empty() {
94 current_spans.push(Span::styled(before.to_string(), style));
95 }
96 lines.push(Line::from(std::mem::take(&mut current_spans)));
97 remaining = &remaining[nl_pos + 1..];
98 }
99 if !remaining.is_empty() {
100 current_spans.push(Span::styled(remaining.to_string(), style));
101 }
102 }
103
104 lines.push(Line::from(current_spans));
106 lines
107}
108
109fn visualize_whitespace<'a>(spans: Vec<Span<'a>>) -> Vec<Span<'a>> {
111 let mut result = Vec::new();
112 for span in spans {
113 let style = span.style;
114 let text: &str = span.content.as_ref();
115 let ws_style = Style::default().fg(theme::OVERLAY);
116 let mut segment = String::new();
117
118 for c in text.chars() {
119 match c {
120 ' ' => {
121 if !segment.is_empty() {
122 result.push(Span::styled(std::mem::take(&mut segment), style));
123 }
124 result.push(Span::styled("\u{00b7}", ws_style)); }
126 '\n' => {
127 if !segment.is_empty() {
128 result.push(Span::styled(std::mem::take(&mut segment), style));
129 }
130 result.push(Span::styled("\u{21b5}\n", ws_style)); }
132 '\t' => {
133 if !segment.is_empty() {
134 result.push(Span::styled(std::mem::take(&mut segment), style));
135 }
136 result.push(Span::styled("\u{2192}", ws_style)); }
138 _ => {
139 segment.push(c);
140 }
141 }
142 }
143 if !segment.is_empty() {
144 result.push(Span::styled(segment, style));
145 }
146 }
147 result
148}
149
150fn build_highlighted_spans<'a>(text: &'a str, matches: &[engine::Match]) -> Vec<Span<'a>> {
151 if matches.is_empty() || text.is_empty() {
152 return vec![Span::styled(text, Style::default().fg(theme::TEXT))];
153 }
154
155 let mut spans = Vec::new();
156 let mut pos = 0;
157
158 for m in matches {
159 if m.start > pos {
160 spans.push(Span::styled(
161 &text[pos..m.start],
162 Style::default().fg(theme::TEXT),
163 ));
164 }
165
166 if m.captures.is_empty() {
167 spans.push(Span::styled(
168 &text[m.start..m.end],
169 Style::default()
170 .fg(theme::TEXT)
171 .bg(theme::MATCH_BG)
172 .add_modifier(Modifier::BOLD),
173 ));
174 } else {
175 let mut inner_pos = m.start;
177 for cap in &m.captures {
178 if cap.start > inner_pos {
179 spans.push(Span::styled(
180 &text[inner_pos..cap.start],
181 Style::default()
182 .fg(theme::TEXT)
183 .bg(theme::MATCH_BG)
184 .add_modifier(Modifier::BOLD),
185 ));
186 }
187 let color = theme::capture_color(cap.index.saturating_sub(1));
188 spans.push(Span::styled(
189 &text[cap.start..cap.end],
190 Style::default()
191 .fg(theme::BASE)
192 .bg(color)
193 .add_modifier(Modifier::BOLD),
194 ));
195 inner_pos = cap.end;
196 }
197 if inner_pos < m.end {
198 spans.push(Span::styled(
199 &text[inner_pos..m.end],
200 Style::default()
201 .fg(theme::TEXT)
202 .bg(theme::MATCH_BG)
203 .add_modifier(Modifier::BOLD),
204 ));
205 }
206 }
207
208 pos = m.end;
209 }
210
211 if pos < text.len() {
212 spans.push(Span::styled(&text[pos..], Style::default().fg(theme::TEXT)));
213 }
214
215 spans
216}