1use std::collections::VecDeque;
2use std::time::{Duration, Instant};
3
4use crate::engine::{self, CompiledRegex, EngineFlags, EngineKind, RegexEngine};
5use crate::explain::{self, ExplainNode};
6use crate::input::editor::Editor;
7
8fn truncate(s: &str, max_chars: usize) -> String {
9 let char_count = s.chars().count();
10 if char_count <= max_chars {
11 s.to_string()
12 } else {
13 let end = s
14 .char_indices()
15 .nth(max_chars)
16 .map(|(i, _)| i)
17 .unwrap_or(s.len());
18 format!("{}...", &s[..end])
19 }
20}
21
22pub struct App {
23 pub regex_editor: Editor,
24 pub test_editor: Editor,
25 pub replace_editor: Editor,
26 pub focused_panel: u8,
27 pub engine_kind: EngineKind,
28 pub flags: EngineFlags,
29 pub matches: Vec<engine::Match>,
30 pub replace_result: Option<engine::ReplaceResult>,
31 pub explanation: Vec<ExplainNode>,
32 pub error: Option<String>,
33 pub show_help: bool,
34 pub help_page: usize,
35 pub should_quit: bool,
36 pub match_scroll: u16,
37 pub replace_scroll: u16,
38 pub explain_scroll: u16,
39 pub pattern_history: VecDeque<String>,
41 pub history_index: Option<usize>,
42 history_temp: Option<String>,
43 pub selected_match: usize,
45 pub selected_capture: Option<usize>,
46 pub clipboard_status: Option<String>,
47 clipboard_status_ticks: u32,
48 pub show_whitespace: bool,
49 pub compile_time: Option<Duration>,
50 pub match_time: Option<Duration>,
51 engine: Box<dyn RegexEngine>,
52 compiled: Option<Box<dyn CompiledRegex>>,
53}
54
55impl App {
56 pub const PANEL_REGEX: u8 = 0;
57 pub const PANEL_TEST: u8 = 1;
58 pub const PANEL_REPLACE: u8 = 2;
59 pub const PANEL_MATCHES: u8 = 3;
60 pub const PANEL_EXPLAIN: u8 = 4;
61}
62
63impl App {
64 pub fn new(engine_kind: EngineKind, flags: EngineFlags) -> Self {
65 let engine = engine::create_engine(engine_kind);
66 Self {
67 regex_editor: Editor::new(),
68 test_editor: Editor::new(),
69 replace_editor: Editor::new(),
70 focused_panel: 0,
71 engine_kind,
72 flags,
73 matches: Vec::new(),
74 replace_result: None,
75 explanation: Vec::new(),
76 error: None,
77 show_help: false,
78 help_page: 0,
79 should_quit: false,
80 match_scroll: 0,
81 replace_scroll: 0,
82 explain_scroll: 0,
83 pattern_history: VecDeque::new(),
84 history_index: None,
85 history_temp: None,
86 selected_match: 0,
87 selected_capture: None,
88 clipboard_status: None,
89 clipboard_status_ticks: 0,
90 show_whitespace: false,
91 compile_time: None,
92 match_time: None,
93 engine,
94 compiled: None,
95 }
96 }
97
98 pub fn set_replacement(&mut self, text: &str) {
99 self.replace_editor = Editor::with_content(text.to_string());
100 self.rereplace();
101 }
102
103 pub fn scroll_replace_up(&mut self) {
104 self.replace_scroll = self.replace_scroll.saturating_sub(1);
105 }
106
107 pub fn scroll_replace_down(&mut self) {
108 self.replace_scroll = self.replace_scroll.saturating_add(1);
109 }
110
111 pub fn rereplace(&mut self) {
112 let template = self.replace_editor.content().to_string();
113 if template.is_empty() || self.matches.is_empty() {
114 self.replace_result = None;
115 return;
116 }
117 let text = self.test_editor.content().to_string();
118 self.replace_result = Some(engine::replace_all(&text, &self.matches, &template));
119 }
120
121 pub fn set_pattern(&mut self, pattern: &str) {
122 self.regex_editor = Editor::with_content(pattern.to_string());
123 self.recompute();
124 }
125
126 pub fn set_test_string(&mut self, text: &str) {
127 self.test_editor = Editor::with_content(text.to_string());
128 self.rematch();
129 }
130
131 pub fn switch_engine(&mut self) {
132 self.engine_kind = self.engine_kind.next();
133 self.engine = engine::create_engine(self.engine_kind);
134 self.recompute();
135 }
136
137 pub fn scroll_match_up(&mut self) {
138 self.match_scroll = self.match_scroll.saturating_sub(1);
139 }
140
141 pub fn scroll_match_down(&mut self) {
142 self.match_scroll = self.match_scroll.saturating_add(1);
143 }
144
145 pub fn scroll_explain_up(&mut self) {
146 self.explain_scroll = self.explain_scroll.saturating_sub(1);
147 }
148
149 pub fn scroll_explain_down(&mut self) {
150 self.explain_scroll = self.explain_scroll.saturating_add(1);
151 }
152
153 pub fn recompute(&mut self) {
154 let pattern = self.regex_editor.content().to_string();
155 self.match_scroll = 0;
156 self.explain_scroll = 0;
157
158 if pattern.is_empty() {
159 self.compiled = None;
160 self.matches.clear();
161 self.explanation.clear();
162 self.error = None;
163 self.compile_time = None;
164 self.match_time = None;
165 return;
166 }
167
168 let compile_start = Instant::now();
170 match self.engine.compile(&pattern, &self.flags) {
171 Ok(compiled) => {
172 self.compile_time = Some(compile_start.elapsed());
173 self.compiled = Some(compiled);
174 self.error = None;
175 }
176 Err(e) => {
177 self.compile_time = Some(compile_start.elapsed());
178 self.compiled = None;
179 self.matches.clear();
180 self.error = Some(e.to_string());
181 }
182 }
183
184 match explain::explain(&pattern) {
186 Ok(nodes) => self.explanation = nodes,
187 Err(e) => {
188 self.explanation.clear();
189 if self.error.is_none() {
190 self.error = Some(e);
191 }
192 }
193 }
194
195 self.rematch();
197 }
198
199 pub fn rematch(&mut self) {
200 self.match_scroll = 0;
201 self.selected_match = 0;
202 self.selected_capture = None;
203 if let Some(compiled) = &self.compiled {
204 let text = self.test_editor.content().to_string();
205 if text.is_empty() {
206 self.matches.clear();
207 self.replace_result = None;
208 self.match_time = None;
209 return;
210 }
211 let match_start = Instant::now();
212 match compiled.find_matches(&text) {
213 Ok(m) => {
214 self.match_time = Some(match_start.elapsed());
215 self.matches = m;
216 }
217 Err(e) => {
218 self.match_time = Some(match_start.elapsed());
219 self.matches.clear();
220 self.error = Some(e.to_string());
221 }
222 }
223 } else {
224 self.matches.clear();
225 self.match_time = None;
226 }
227 self.rereplace();
228 }
229
230 pub fn commit_pattern_to_history(&mut self) {
233 let pattern = self.regex_editor.content().to_string();
234 if pattern.is_empty() {
235 return;
236 }
237 if self.pattern_history.back().map(|s| s.as_str()) == Some(&pattern) {
238 return;
239 }
240 self.pattern_history.push_back(pattern);
241 if self.pattern_history.len() > 100 {
242 self.pattern_history.pop_front();
243 }
244 self.history_index = None;
245 self.history_temp = None;
246 }
247
248 pub fn history_prev(&mut self) {
249 if self.pattern_history.is_empty() {
250 return;
251 }
252 let new_index = match self.history_index {
253 Some(0) => return,
254 Some(idx) => idx - 1,
255 None => {
256 self.history_temp = Some(self.regex_editor.content().to_string());
257 self.pattern_history.len() - 1
258 }
259 };
260 self.history_index = Some(new_index);
261 let pattern = self.pattern_history[new_index].clone();
262 self.regex_editor = Editor::with_content(pattern);
263 self.recompute();
264 }
265
266 pub fn history_next(&mut self) {
267 let idx = match self.history_index {
268 Some(idx) => idx,
269 None => return,
270 };
271 if idx + 1 < self.pattern_history.len() {
272 let new_index = idx + 1;
273 self.history_index = Some(new_index);
274 let pattern = self.pattern_history[new_index].clone();
275 self.regex_editor = Editor::with_content(pattern);
276 self.recompute();
277 } else {
278 self.history_index = None;
280 let content = self.history_temp.take().unwrap_or_default();
281 self.regex_editor = Editor::with_content(content);
282 self.recompute();
283 }
284 }
285
286 pub fn select_match_next(&mut self) {
289 if self.matches.is_empty() {
290 return;
291 }
292 match self.selected_capture {
293 None => {
294 let m = &self.matches[self.selected_match];
295 if !m.captures.is_empty() {
296 self.selected_capture = Some(0);
297 } else if self.selected_match + 1 < self.matches.len() {
298 self.selected_match += 1;
299 }
300 }
301 Some(ci) => {
302 let m = &self.matches[self.selected_match];
303 if ci + 1 < m.captures.len() {
304 self.selected_capture = Some(ci + 1);
305 } else if self.selected_match + 1 < self.matches.len() {
306 self.selected_match += 1;
307 self.selected_capture = None;
308 }
309 }
310 }
311 self.scroll_to_selected();
312 }
313
314 pub fn select_match_prev(&mut self) {
315 if self.matches.is_empty() {
316 return;
317 }
318 match self.selected_capture {
319 Some(0) => {
320 self.selected_capture = None;
321 }
322 Some(ci) => {
323 self.selected_capture = Some(ci - 1);
324 }
325 None => {
326 if self.selected_match > 0 {
327 self.selected_match -= 1;
328 let m = &self.matches[self.selected_match];
329 if !m.captures.is_empty() {
330 self.selected_capture = Some(m.captures.len() - 1);
331 }
332 }
333 }
334 }
335 self.scroll_to_selected();
336 }
337
338 fn scroll_to_selected(&mut self) {
339 if self.matches.is_empty() || self.selected_match >= self.matches.len() {
340 return;
341 }
342 let mut line = 0usize;
343 for i in 0..self.selected_match {
344 line += 1 + self.matches[i].captures.len();
345 }
346 if let Some(ci) = self.selected_capture {
347 line += 1 + ci;
348 }
349 self.match_scroll = u16::try_from(line).unwrap_or(u16::MAX);
350 }
351
352 pub fn copy_selected_match(&mut self) {
353 let text = self.selected_text();
354 let Some(text) = text else { return };
355 match arboard::Clipboard::new() {
356 Ok(mut cb) => match cb.set_text(&text) {
357 Ok(()) => {
358 self.clipboard_status = Some(format!("Copied: \"{}\"", truncate(&text, 40)));
359 self.clipboard_status_ticks = 40; }
361 Err(e) => {
362 self.clipboard_status = Some(format!("Clipboard error: {e}"));
363 self.clipboard_status_ticks = 40;
364 }
365 },
366 Err(e) => {
367 self.clipboard_status = Some(format!("Clipboard error: {e}"));
368 self.clipboard_status_ticks = 40;
369 }
370 }
371 }
372
373 pub fn tick_clipboard_status(&mut self) -> bool {
375 if self.clipboard_status.is_some() {
376 if self.clipboard_status_ticks > 0 {
377 self.clipboard_status_ticks -= 1;
378 } else {
379 self.clipboard_status = None;
380 return true;
381 }
382 }
383 false
384 }
385
386 fn selected_text(&self) -> Option<String> {
387 let m = self.matches.get(self.selected_match)?;
388 match self.selected_capture {
389 None => Some(m.text.clone()),
390 Some(ci) => m.captures.get(ci).map(|c| c.text.clone()),
391 }
392 }
393}