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
8#[derive(Debug, Clone)]
9pub struct BenchmarkResult {
10 pub engine: EngineKind,
11 pub compile_time: Duration,
12 pub match_time: Duration,
13 pub match_count: usize,
14 pub error: Option<String>,
15}
16
17fn truncate(s: &str, max_chars: usize) -> String {
18 let char_count = s.chars().count();
19 if char_count <= max_chars {
20 s.to_string()
21 } else {
22 let end = s
23 .char_indices()
24 .nth(max_chars)
25 .map(|(i, _)| i)
26 .unwrap_or(s.len());
27 format!("{}...", &s[..end])
28 }
29}
30
31pub struct App {
32 pub regex_editor: Editor,
33 pub test_editor: Editor,
34 pub replace_editor: Editor,
35 pub focused_panel: u8,
36 pub engine_kind: EngineKind,
37 pub flags: EngineFlags,
38 pub matches: Vec<engine::Match>,
39 pub replace_result: Option<engine::ReplaceResult>,
40 pub explanation: Vec<ExplainNode>,
41 pub error: Option<String>,
42 pub show_help: bool,
43 pub help_page: usize,
44 pub should_quit: bool,
45 pub match_scroll: u16,
46 pub replace_scroll: u16,
47 pub explain_scroll: u16,
48 pub pattern_history: VecDeque<String>,
50 pub history_index: Option<usize>,
51 history_temp: Option<String>,
52 pub selected_match: usize,
54 pub selected_capture: Option<usize>,
55 pub clipboard_status: Option<String>,
56 clipboard_status_ticks: u32,
57 pub show_whitespace: bool,
58 pub rounded_borders: bool,
59 pub vim_mode: bool,
60 pub vim_state: crate::input::vim::VimState,
61 pub compile_time: Option<Duration>,
62 pub match_time: Option<Duration>,
63 pub error_offset: Option<usize>,
64 pub output_on_quit: bool,
65 pub workspace_path: Option<String>,
66 pub show_recipes: bool,
67 pub recipe_index: usize,
68 pub show_benchmark: bool,
69 pub benchmark_results: Vec<BenchmarkResult>,
70 engine: Box<dyn RegexEngine>,
71 compiled: Option<Box<dyn CompiledRegex>>,
72}
73
74impl App {
75 pub const PANEL_REGEX: u8 = 0;
76 pub const PANEL_TEST: u8 = 1;
77 pub const PANEL_REPLACE: u8 = 2;
78 pub const PANEL_MATCHES: u8 = 3;
79 pub const PANEL_EXPLAIN: u8 = 4;
80 pub const PANEL_COUNT: u8 = 5;
81}
82
83impl App {
84 pub fn new(engine_kind: EngineKind, flags: EngineFlags) -> Self {
85 let engine = engine::create_engine(engine_kind);
86 Self {
87 regex_editor: Editor::new(),
88 test_editor: Editor::new(),
89 replace_editor: Editor::new(),
90 focused_panel: 0,
91 engine_kind,
92 flags,
93 matches: Vec::new(),
94 replace_result: None,
95 explanation: Vec::new(),
96 error: None,
97 show_help: false,
98 help_page: 0,
99 should_quit: false,
100 match_scroll: 0,
101 replace_scroll: 0,
102 explain_scroll: 0,
103 pattern_history: VecDeque::new(),
104 history_index: None,
105 history_temp: None,
106 selected_match: 0,
107 selected_capture: None,
108 clipboard_status: None,
109 clipboard_status_ticks: 0,
110 show_whitespace: false,
111 rounded_borders: false,
112 vim_mode: false,
113 vim_state: crate::input::vim::VimState::new(),
114 compile_time: None,
115 match_time: None,
116 error_offset: None,
117 output_on_quit: false,
118 workspace_path: None,
119 show_recipes: false,
120 recipe_index: 0,
121 show_benchmark: false,
122 benchmark_results: Vec::new(),
123 engine,
124 compiled: None,
125 }
126 }
127
128 pub fn set_replacement(&mut self, text: &str) {
129 self.replace_editor = Editor::with_content(text.to_string());
130 self.rereplace();
131 }
132
133 pub fn scroll_replace_up(&mut self) {
134 self.replace_scroll = self.replace_scroll.saturating_sub(1);
135 }
136
137 pub fn scroll_replace_down(&mut self) {
138 self.replace_scroll = self.replace_scroll.saturating_add(1);
139 }
140
141 pub fn rereplace(&mut self) {
142 let template = self.replace_editor.content().to_string();
143 if template.is_empty() || self.matches.is_empty() {
144 self.replace_result = None;
145 return;
146 }
147 let text = self.test_editor.content().to_string();
148 self.replace_result = Some(engine::replace_all(&text, &self.matches, &template));
149 }
150
151 pub fn set_pattern(&mut self, pattern: &str) {
152 self.regex_editor = Editor::with_content(pattern.to_string());
153 self.recompute();
154 }
155
156 pub fn set_test_string(&mut self, text: &str) {
157 self.test_editor = Editor::with_content(text.to_string());
158 self.rematch();
159 }
160
161 pub fn switch_engine(&mut self) {
162 self.engine_kind = self.engine_kind.next();
163 self.engine = engine::create_engine(self.engine_kind);
164 self.recompute();
165 }
166
167 pub fn switch_engine_to(&mut self, kind: EngineKind) {
168 self.engine_kind = kind;
169 self.engine = engine::create_engine(kind);
170 }
171
172 pub fn scroll_match_up(&mut self) {
173 self.match_scroll = self.match_scroll.saturating_sub(1);
174 }
175
176 pub fn scroll_match_down(&mut self) {
177 self.match_scroll = self.match_scroll.saturating_add(1);
178 }
179
180 pub fn scroll_explain_up(&mut self) {
181 self.explain_scroll = self.explain_scroll.saturating_sub(1);
182 }
183
184 pub fn scroll_explain_down(&mut self) {
185 self.explain_scroll = self.explain_scroll.saturating_add(1);
186 }
187
188 pub fn recompute(&mut self) {
189 let pattern = self.regex_editor.content().to_string();
190 self.match_scroll = 0;
191 self.explain_scroll = 0;
192 self.error_offset = None;
193
194 if pattern.is_empty() {
195 self.compiled = None;
196 self.matches.clear();
197 self.explanation.clear();
198 self.error = None;
199 self.compile_time = None;
200 self.match_time = None;
201 return;
202 }
203
204 let compile_start = Instant::now();
206 match self.engine.compile(&pattern, &self.flags) {
207 Ok(compiled) => {
208 self.compile_time = Some(compile_start.elapsed());
209 self.compiled = Some(compiled);
210 self.error = None;
211 }
212 Err(e) => {
213 self.compile_time = Some(compile_start.elapsed());
214 self.compiled = None;
215 self.matches.clear();
216 self.error = Some(e.to_string());
217 }
218 }
219
220 match explain::explain(&pattern) {
222 Ok(nodes) => self.explanation = nodes,
223 Err((msg, offset)) => {
224 self.explanation.clear();
225 if self.error_offset.is_none() {
226 self.error_offset = offset;
227 }
228 if self.error.is_none() {
229 self.error = Some(msg);
230 }
231 }
232 }
233
234 self.rematch();
236 }
237
238 pub fn rematch(&mut self) {
239 self.match_scroll = 0;
240 self.selected_match = 0;
241 self.selected_capture = None;
242 if let Some(compiled) = &self.compiled {
243 let text = self.test_editor.content().to_string();
244 if text.is_empty() {
245 self.matches.clear();
246 self.replace_result = None;
247 self.match_time = None;
248 return;
249 }
250 let match_start = Instant::now();
251 match compiled.find_matches(&text) {
252 Ok(m) => {
253 self.match_time = Some(match_start.elapsed());
254 self.matches = m;
255 }
256 Err(e) => {
257 self.match_time = Some(match_start.elapsed());
258 self.matches.clear();
259 self.error = Some(e.to_string());
260 }
261 }
262 } else {
263 self.matches.clear();
264 self.match_time = None;
265 }
266 self.rereplace();
267 }
268
269 pub fn commit_pattern_to_history(&mut self) {
272 let pattern = self.regex_editor.content().to_string();
273 if pattern.is_empty() {
274 return;
275 }
276 if self.pattern_history.back().map(|s| s.as_str()) == Some(&pattern) {
277 return;
278 }
279 self.pattern_history.push_back(pattern);
280 if self.pattern_history.len() > 100 {
281 self.pattern_history.pop_front();
282 }
283 self.history_index = None;
284 self.history_temp = None;
285 }
286
287 pub fn history_prev(&mut self) {
288 if self.pattern_history.is_empty() {
289 return;
290 }
291 let new_index = match self.history_index {
292 Some(0) => return,
293 Some(idx) => idx - 1,
294 None => {
295 self.history_temp = Some(self.regex_editor.content().to_string());
296 self.pattern_history.len() - 1
297 }
298 };
299 self.history_index = Some(new_index);
300 let pattern = self.pattern_history[new_index].clone();
301 self.regex_editor = Editor::with_content(pattern);
302 self.recompute();
303 }
304
305 pub fn history_next(&mut self) {
306 let idx = match self.history_index {
307 Some(idx) => idx,
308 None => return,
309 };
310 if idx + 1 < self.pattern_history.len() {
311 let new_index = idx + 1;
312 self.history_index = Some(new_index);
313 let pattern = self.pattern_history[new_index].clone();
314 self.regex_editor = Editor::with_content(pattern);
315 self.recompute();
316 } else {
317 self.history_index = None;
319 let content = self.history_temp.take().unwrap_or_default();
320 self.regex_editor = Editor::with_content(content);
321 self.recompute();
322 }
323 }
324
325 pub fn select_match_next(&mut self) {
328 if self.matches.is_empty() {
329 return;
330 }
331 match self.selected_capture {
332 None => {
333 let m = &self.matches[self.selected_match];
334 if !m.captures.is_empty() {
335 self.selected_capture = Some(0);
336 } else if self.selected_match + 1 < self.matches.len() {
337 self.selected_match += 1;
338 }
339 }
340 Some(ci) => {
341 let m = &self.matches[self.selected_match];
342 if ci + 1 < m.captures.len() {
343 self.selected_capture = Some(ci + 1);
344 } else if self.selected_match + 1 < self.matches.len() {
345 self.selected_match += 1;
346 self.selected_capture = None;
347 }
348 }
349 }
350 self.scroll_to_selected();
351 }
352
353 pub fn select_match_prev(&mut self) {
354 if self.matches.is_empty() {
355 return;
356 }
357 match self.selected_capture {
358 Some(0) => {
359 self.selected_capture = None;
360 }
361 Some(ci) => {
362 self.selected_capture = Some(ci - 1);
363 }
364 None => {
365 if self.selected_match > 0 {
366 self.selected_match -= 1;
367 let m = &self.matches[self.selected_match];
368 if !m.captures.is_empty() {
369 self.selected_capture = Some(m.captures.len() - 1);
370 }
371 }
372 }
373 }
374 self.scroll_to_selected();
375 }
376
377 fn scroll_to_selected(&mut self) {
378 if self.matches.is_empty() || self.selected_match >= self.matches.len() {
379 return;
380 }
381 let mut line = 0usize;
382 for i in 0..self.selected_match {
383 line += 1 + self.matches[i].captures.len();
384 }
385 if let Some(ci) = self.selected_capture {
386 line += 1 + ci;
387 }
388 self.match_scroll = u16::try_from(line).unwrap_or(u16::MAX);
389 }
390
391 pub fn copy_selected_match(&mut self) {
392 let text = self.selected_text();
393 let Some(text) = text else { return };
394 match arboard::Clipboard::new() {
395 Ok(mut cb) => match cb.set_text(&text) {
396 Ok(()) => {
397 self.clipboard_status = Some(format!("Copied: \"{}\"", truncate(&text, 40)));
398 self.clipboard_status_ticks = 40; }
400 Err(e) => {
401 self.clipboard_status = Some(format!("Clipboard error: {e}"));
402 self.clipboard_status_ticks = 40;
403 }
404 },
405 Err(e) => {
406 self.clipboard_status = Some(format!("Clipboard error: {e}"));
407 self.clipboard_status_ticks = 40;
408 }
409 }
410 }
411
412 pub fn set_status_message(&mut self, message: String) {
413 self.clipboard_status = Some(message);
414 self.clipboard_status_ticks = 40; }
416
417 pub fn tick_clipboard_status(&mut self) -> bool {
419 if self.clipboard_status.is_some() {
420 if self.clipboard_status_ticks > 0 {
421 self.clipboard_status_ticks -= 1;
422 } else {
423 self.clipboard_status = None;
424 return true;
425 }
426 }
427 false
428 }
429
430 pub fn print_output(&self, group: Option<&str>, count: bool) {
432 if count {
433 println!("{}", self.matches.len());
434 return;
435 }
436 if let Some(ref result) = self.replace_result {
437 print!("{}", result.output);
438 } else if let Some(group_spec) = group {
439 for m in &self.matches {
440 if let Some(text) = engine::lookup_capture(m, group_spec) {
441 println!("{text}");
442 } else {
443 eprintln!("rgx: group '{group_spec}' not found in match");
444 }
445 }
446 } else {
447 for m in &self.matches {
448 println!("{}", m.text);
449 }
450 }
451 }
452
453 fn selected_text(&self) -> Option<String> {
454 let m = self.matches.get(self.selected_match)?;
455 match self.selected_capture {
456 None => Some(m.text.clone()),
457 Some(ci) => m.captures.get(ci).map(|c| c.text.clone()),
458 }
459 }
460
461 pub fn edit_focused(&mut self, f: impl FnOnce(&mut Editor)) {
464 match self.focused_panel {
465 Self::PANEL_REGEX => {
466 f(&mut self.regex_editor);
467 self.recompute();
468 }
469 Self::PANEL_TEST => {
470 f(&mut self.test_editor);
471 self.rematch();
472 }
473 Self::PANEL_REPLACE => {
474 f(&mut self.replace_editor);
475 self.rereplace();
476 }
477 _ => {}
478 }
479 }
480
481 pub fn move_focused(&mut self, f: impl FnOnce(&mut Editor)) {
483 match self.focused_panel {
484 Self::PANEL_REGEX => f(&mut self.regex_editor),
485 Self::PANEL_TEST => f(&mut self.test_editor),
486 Self::PANEL_REPLACE => f(&mut self.replace_editor),
487 _ => {}
488 }
489 }
490
491 pub fn run_benchmark(&mut self) {
492 let pattern = self.regex_editor.content().to_string();
493 let text = self.test_editor.content().to_string();
494 if pattern.is_empty() || text.is_empty() {
495 return;
496 }
497
498 let mut results = Vec::new();
499 for kind in EngineKind::all() {
500 let eng = engine::create_engine(kind);
501 let compile_start = Instant::now();
502 let compiled = match eng.compile(&pattern, &self.flags) {
503 Ok(c) => c,
504 Err(e) => {
505 results.push(BenchmarkResult {
506 engine: kind,
507 compile_time: compile_start.elapsed(),
508 match_time: Duration::ZERO,
509 match_count: 0,
510 error: Some(e.to_string()),
511 });
512 continue;
513 }
514 };
515 let compile_time = compile_start.elapsed();
516 let match_start = Instant::now();
517 let (match_count, error) = match compiled.find_matches(&text) {
518 Ok(matches) => (matches.len(), None),
519 Err(e) => (0, Some(e.to_string())),
520 };
521 results.push(BenchmarkResult {
522 engine: kind,
523 compile_time,
524 match_time: match_start.elapsed(),
525 match_count,
526 error,
527 });
528 }
529 self.benchmark_results = results;
530 self.show_benchmark = true;
531 }
532}