1use std::collections::{HashMap, VecDeque};
2use std::fmt::Write as _;
3use std::time::{Duration, Instant};
4
5use crate::ansi::{GREEN_BOLD, RED_BOLD, RESET};
6use crate::engine::{self, CompiledRegex, EngineFlags, EngineKind, RegexEngine};
7use crate::explain::{self, ExplainNode};
8use crate::input::editor::Editor;
9use crate::input::Action;
10use crate::ui;
11
12const MAX_PATTERN_HISTORY: usize = 100;
13const STATUS_DISPLAY_TICKS: u32 = 40; #[derive(Debug, Clone)]
16pub struct BenchmarkResult {
17 pub engine: EngineKind,
18 pub compile_time: Duration,
19 pub match_time: Duration,
20 pub match_count: usize,
21 pub error: Option<String>,
22}
23
24fn truncate(s: &str, max_chars: usize) -> String {
25 match s.char_indices().nth(max_chars) {
29 Some((end, _)) => format!("{}...", &s[..end]),
30 None => s.to_string(),
31 }
32}
33
34#[derive(Default)]
35pub struct OverlayState {
36 pub help: bool,
37 pub help_page: usize,
38 pub recipes: bool,
39 pub recipe_index: usize,
40 pub benchmark: bool,
41 pub codegen: bool,
42 pub codegen_language_index: usize,
43 pub grex: Option<crate::ui::grex_overlay::GrexOverlayState>,
44}
45
46#[derive(Default)]
47pub struct ScrollState {
48 pub match_scroll: u16,
49 pub replace_scroll: u16,
50 pub explain_scroll: u16,
51}
52
53#[derive(Default)]
54pub struct PatternHistory {
55 pub entries: VecDeque<String>,
56 pub index: Option<usize>,
57 pub temp: Option<String>,
58}
59
60#[derive(Default)]
61pub struct MatchSelection {
62 pub match_index: usize,
63 pub capture_index: Option<usize>,
64}
65
66#[derive(Default)]
67pub struct StatusMessage {
68 pub text: Option<String>,
69 ticks: u32,
70}
71
72impl StatusMessage {
73 pub fn set(&mut self, message: String) {
74 self.text = Some(message);
75 self.ticks = STATUS_DISPLAY_TICKS;
76 }
77
78 pub fn tick(&mut self) -> bool {
79 if self.text.is_some() {
80 if self.ticks > 0 {
81 self.ticks -= 1;
82 } else {
83 self.text = None;
84 return true;
85 }
86 }
87 false
88 }
89}
90
91pub struct App {
92 pub regex_editor: Editor,
93 pub test_editor: Editor,
94 pub replace_editor: Editor,
95 pub focused_panel: u8,
96 pub engine_kind: EngineKind,
97 pub flags: EngineFlags,
98 pub matches: Vec<engine::Match>,
99 pub replace_result: Option<engine::ReplaceResult>,
100 pub explanation: Vec<ExplainNode>,
101 pub error: Option<String>,
102 pub overlay: OverlayState,
103 pub should_quit: bool,
104 pub scroll: ScrollState,
105 pub history: PatternHistory,
106 pub selection: MatchSelection,
107 pub status: StatusMessage,
108 pub show_whitespace: bool,
109 pub show_quickref: bool,
110 pub rounded_borders: bool,
111 pub vim_mode: bool,
112 pub vim_state: crate::input::vim::VimState,
113 pub compile_time: Option<Duration>,
114 pub match_time: Option<Duration>,
115 pub error_offset: Option<usize>,
116 pub output_on_quit: bool,
117 pub workspace_path: Option<String>,
118 pub benchmark_results: Vec<BenchmarkResult>,
119 pub syntax_tokens: Vec<crate::ui::syntax_highlight::SyntaxToken>,
120 #[cfg(feature = "pcre2-engine")]
121 pub debug_session: Option<crate::engine::pcre2_debug::DebugSession>,
122 #[cfg(feature = "pcre2-engine")]
123 debug_cache: Option<crate::engine::pcre2_debug::DebugSession>,
124 pub grex_result_tx: tokio::sync::mpsc::UnboundedSender<(u64, String)>,
125 grex_result_rx: tokio::sync::mpsc::UnboundedReceiver<(u64, String)>,
126 engine: Box<dyn RegexEngine>,
127 compiled: Option<Box<dyn CompiledRegex>>,
128 pub help_scroll_offset: u16,
129 pub help_pages_lengths: HashMap<EngineKind, Vec<u16>>,
130}
131
132impl App {
133 pub const PANEL_REGEX: u8 = 0;
134 pub const PANEL_TEST: u8 = 1;
135 pub const PANEL_REPLACE: u8 = 2;
136 pub const PANEL_MATCHES: u8 = 3;
137 pub const PANEL_EXPLAIN: u8 = 4;
138 pub const PANEL_COUNT: u8 = 5;
139}
140
141impl App {
142 pub fn new(engine_kind: EngineKind, flags: EngineFlags) -> Self {
143 let engine = engine::create_engine(engine_kind);
144 let (grex_result_tx, grex_result_rx) = tokio::sync::mpsc::unbounded_channel();
145 Self {
146 regex_editor: Editor::new(),
147 test_editor: Editor::new(),
148 replace_editor: Editor::new(),
149 focused_panel: 0,
150 engine_kind,
151 flags,
152 matches: Vec::new(),
153 replace_result: None,
154 explanation: Vec::new(),
155 error: None,
156 overlay: OverlayState::default(),
157 should_quit: false,
158 scroll: ScrollState::default(),
159 history: PatternHistory::default(),
160 selection: MatchSelection::default(),
161 status: StatusMessage::default(),
162 show_whitespace: false,
163 show_quickref: false,
164 rounded_borders: false,
165 vim_mode: false,
166 vim_state: crate::input::vim::VimState::new(),
167 compile_time: None,
168 match_time: None,
169 error_offset: None,
170 output_on_quit: false,
171 workspace_path: None,
172 benchmark_results: Vec::new(),
173 syntax_tokens: Vec::new(),
174 #[cfg(feature = "pcre2-engine")]
175 debug_session: None,
176 #[cfg(feature = "pcre2-engine")]
177 debug_cache: None,
178 grex_result_tx,
179 grex_result_rx,
180 engine,
181 compiled: None,
182 help_scroll_offset: 0u16,
183 help_pages_lengths: ui::build_lengths_of_help_pages(),
184 }
185 }
186
187 pub fn set_replacement(&mut self, text: &str) {
188 self.replace_editor = Editor::with_content(text.to_string());
189 self.rereplace();
190 }
191
192 pub fn scroll_replace_up(&mut self) {
193 self.scroll.replace_scroll = self.scroll.replace_scroll.saturating_sub(1);
194 }
195
196 pub fn scroll_replace_down(&mut self) {
197 self.scroll.replace_scroll = self.scroll.replace_scroll.saturating_add(1);
198 }
199
200 pub fn rereplace(&mut self) {
201 let template = self.replace_editor.content().to_string();
202 if template.is_empty() || self.matches.is_empty() {
203 self.replace_result = None;
204 return;
205 }
206 let text = self.test_editor.content().to_string();
207 self.replace_result = Some(engine::replace_all(&text, &self.matches, &template));
208 }
209
210 pub fn set_pattern(&mut self, pattern: &str) {
211 self.regex_editor = Editor::with_content(pattern.to_string());
212 self.recompute();
213 }
214
215 pub fn set_test_string(&mut self, text: &str) {
216 self.test_editor = Editor::with_content(text.to_string());
217 self.rematch();
218 }
219
220 pub fn switch_engine(&mut self) {
221 self.engine_kind = self.engine_kind.next();
222 self.engine = engine::create_engine(self.engine_kind);
223 self.recompute();
224 }
225
226 pub fn switch_engine_to(&mut self, kind: EngineKind) {
229 self.engine_kind = kind;
230 self.engine = engine::create_engine(kind);
231 }
232
233 pub fn scroll_match_up(&mut self) {
234 self.scroll.match_scroll = self.scroll.match_scroll.saturating_sub(1);
235 }
236
237 pub fn scroll_match_down(&mut self) {
238 self.scroll.match_scroll = self.scroll.match_scroll.saturating_add(1);
239 }
240
241 pub fn scroll_explain_up(&mut self) {
242 self.scroll.explain_scroll = self.scroll.explain_scroll.saturating_sub(1);
243 }
244
245 pub fn scroll_explain_down(&mut self) {
246 self.scroll.explain_scroll = self.scroll.explain_scroll.saturating_add(1);
247 }
248
249 pub fn recompute(&mut self) {
250 let pattern = self.regex_editor.content().to_string();
251 self.scroll.match_scroll = 0;
252 self.scroll.explain_scroll = 0;
253 self.error_offset = None;
254
255 if pattern.is_empty() {
256 self.compiled = None;
257 self.matches.clear();
258 self.explanation.clear();
259 self.error = None;
260 self.compile_time = None;
261 self.match_time = None;
262 self.syntax_tokens.clear();
263 return;
264 }
265
266 let suggested = engine::detect_minimum_engine(&pattern);
269 if engine::is_engine_upgrade(self.engine_kind, suggested) {
270 let prev = self.engine_kind;
271 self.engine_kind = suggested;
272 self.engine = engine::create_engine(suggested);
273 self.status.set(format!(
274 "Auto-switched {prev} \u{2192} {suggested} for this pattern",
275 ));
276 }
277
278 let compile_start = Instant::now();
280 match self.engine.compile(&pattern, &self.flags) {
281 Ok(compiled) => {
282 self.compile_time = Some(compile_start.elapsed());
283 self.compiled = Some(compiled);
284 self.error = None;
285 }
286 Err(e) => {
287 self.compile_time = Some(compile_start.elapsed());
288 self.compiled = None;
289 self.matches.clear();
290 self.error = Some(e.to_string());
291 }
292 }
293
294 self.syntax_tokens = crate::ui::syntax_highlight::highlight(&pattern);
296
297 match explain::explain(&pattern) {
308 Ok(nodes) => self.explanation = nodes,
309 Err((msg, offset)) => {
310 self.explanation.clear();
311 if self.error.is_some() {
312 if self.error_offset.is_none() {
315 self.error_offset = offset;
316 }
317 } else {
318 let _ = msg;
323 let _ = offset;
324 }
325 }
326 }
327
328 self.rematch();
330 }
331
332 pub fn rematch(&mut self) {
333 self.scroll.match_scroll = 0;
334 self.selection.match_index = 0;
335 self.selection.capture_index = None;
336 if let Some(compiled) = &self.compiled {
337 let text = self.test_editor.content().to_string();
338 if text.is_empty() {
339 self.matches.clear();
340 self.replace_result = None;
341 self.match_time = None;
342 return;
343 }
344 let match_start = Instant::now();
345 match compiled.find_matches(&text) {
346 Ok(m) => {
347 self.match_time = Some(match_start.elapsed());
348 self.matches = m;
349 }
350 Err(e) => {
351 self.match_time = Some(match_start.elapsed());
352 self.matches.clear();
353 self.error = Some(e.to_string());
354 }
355 }
356 } else {
357 self.matches.clear();
358 self.match_time = None;
359 }
360 self.rereplace();
361 }
362
363 pub fn commit_pattern_to_history(&mut self) {
366 let pattern = self.regex_editor.content().to_string();
367 if pattern.is_empty() {
368 return;
369 }
370 if self.history.entries.back().map(String::as_str) == Some(&pattern) {
371 return;
372 }
373 self.history.entries.push_back(pattern);
374 if self.history.entries.len() > MAX_PATTERN_HISTORY {
375 self.history.entries.pop_front();
376 }
377 self.history.index = None;
378 self.history.temp = None;
379 }
380
381 pub fn history_prev(&mut self) {
382 if self.history.entries.is_empty() {
383 return;
384 }
385 let new_index = match self.history.index {
386 Some(0) => return,
387 Some(idx) => idx - 1,
388 None => {
389 self.history.temp = Some(self.regex_editor.content().to_string());
390 self.history.entries.len() - 1
391 }
392 };
393 self.history.index = Some(new_index);
394 let pattern = self.history.entries[new_index].clone();
395 self.regex_editor = Editor::with_content(pattern);
396 self.recompute();
397 }
398
399 pub fn history_next(&mut self) {
400 let Some(idx) = self.history.index else {
401 return;
402 };
403 let new_content = if idx + 1 < self.history.entries.len() {
404 let new_index = idx + 1;
405 self.history.index = Some(new_index);
406 self.history.entries[new_index].clone()
407 } else {
408 self.history.index = None;
410 self.history.temp.take().unwrap_or_default()
411 };
412 self.regex_editor = Editor::with_content(new_content);
413 self.recompute();
414 }
415
416 pub fn select_match_next(&mut self) {
419 if self.matches.is_empty() {
420 return;
421 }
422 match self.selection.capture_index {
423 None => {
424 let m = &self.matches[self.selection.match_index];
425 if !m.captures.is_empty() {
426 self.selection.capture_index = Some(0);
427 } else if self.selection.match_index + 1 < self.matches.len() {
428 self.selection.match_index += 1;
429 }
430 }
431 Some(ci) => {
432 let m = &self.matches[self.selection.match_index];
433 if ci + 1 < m.captures.len() {
434 self.selection.capture_index = Some(ci + 1);
435 } else if self.selection.match_index + 1 < self.matches.len() {
436 self.selection.match_index += 1;
437 self.selection.capture_index = None;
438 }
439 }
440 }
441 self.scroll_to_selected();
442 }
443
444 pub fn select_match_prev(&mut self) {
445 if self.matches.is_empty() {
446 return;
447 }
448 match self.selection.capture_index {
449 Some(0) => {
450 self.selection.capture_index = None;
451 }
452 Some(ci) => {
453 self.selection.capture_index = Some(ci - 1);
454 }
455 None => {
456 if self.selection.match_index > 0 {
457 self.selection.match_index -= 1;
458 let m = &self.matches[self.selection.match_index];
459 if !m.captures.is_empty() {
460 self.selection.capture_index = Some(m.captures.len() - 1);
461 }
462 }
463 }
464 }
465 self.scroll_to_selected();
466 }
467
468 fn scroll_to_selected(&mut self) {
469 if self.matches.is_empty() || self.selection.match_index >= self.matches.len() {
470 return;
471 }
472 let mut line = 0usize;
473 for i in 0..self.selection.match_index {
474 line += 1 + self.matches[i].captures.len();
475 }
476 if let Some(ci) = self.selection.capture_index {
477 line += 1 + ci;
478 }
479 self.scroll.match_scroll = u16::try_from(line).unwrap_or(u16::MAX);
480 }
481
482 pub fn copy_selected_match(&mut self) {
483 let text = self.selected_text();
484 let Some(text) = text else { return };
485 let msg = format!("Copied: \"{}\"", truncate(&text, 40));
486 self.copy_to_clipboard(&text, &msg);
487 }
488
489 pub fn copy_pattern(&mut self) {
490 let pattern = self.regex_editor.content().to_string();
491 if pattern.is_empty() {
492 return;
493 }
494 let msg = format!("Copied pattern: \"{}\"", truncate(&pattern, 40));
495 self.copy_to_clipboard(&pattern, &msg);
496 }
497
498 fn copy_to_clipboard(&mut self, text: &str, success_msg: &str) {
499 #[cfg(target_os = "linux")]
517 {
518 let text = text.to_string();
519 std::thread::spawn(move || {
520 use arboard::SetExtLinux;
521 if let Ok(mut cb) = arboard::Clipboard::new() {
522 let _ = cb.set().wait().text(text);
523 }
524 });
525 self.status.set(success_msg.to_string());
526 }
527 #[cfg(not(target_os = "linux"))]
528 {
529 match arboard::Clipboard::new() {
530 Ok(mut cb) => match cb.set_text(text) {
531 Ok(()) => self.status.set(success_msg.to_string()),
532 Err(e) => self.status.set(format!("Clipboard error: {e}")),
533 },
534 Err(e) => self.status.set(format!("Clipboard error: {e}")),
535 }
536 }
537 }
538
539 pub fn print_output(&self, group: Option<&str>, count: bool, color: bool) {
541 if count {
542 println!("{}", self.matches.len());
543 return;
544 }
545 if let Some(ref result) = self.replace_result {
546 if color {
547 print_colored_replace(&result.output, &result.segments);
548 } else {
549 print!("{}", result.output);
550 }
551 } else if let Some(group_spec) = group {
552 for m in &self.matches {
553 if let Some(text) = engine::lookup_capture(m, group_spec) {
554 if color {
555 println!("{RED_BOLD}{text}{RESET}");
556 } else {
557 println!("{text}");
558 }
559 } else {
560 eprintln!("rgx: group '{group_spec}' not found in match");
561 }
562 }
563 } else if color {
564 let text = self.test_editor.content();
565 print_colored_matches(text, &self.matches);
566 } else {
567 for m in &self.matches {
568 println!("{}", m.text);
569 }
570 }
571 }
572
573 pub fn print_json_output(&self) {
575 println!(
576 "{}",
577 serde_json::to_string_pretty(&self.matches).unwrap_or_else(|_| "[]".to_string())
578 );
579 }
580
581 fn selected_text(&self) -> Option<String> {
582 let m = self.matches.get(self.selection.match_index)?;
583 match self.selection.capture_index {
584 None => Some(m.text.clone()),
585 Some(ci) => m.captures.get(ci).map(|c| c.text.clone()),
586 }
587 }
588
589 pub fn edit_focused(&mut self, f: impl FnOnce(&mut Editor)) {
592 match self.focused_panel {
593 Self::PANEL_REGEX => {
594 f(&mut self.regex_editor);
595 self.recompute();
596 }
597 Self::PANEL_TEST => {
598 f(&mut self.test_editor);
599 self.rematch();
600 }
601 Self::PANEL_REPLACE => {
602 f(&mut self.replace_editor);
603 self.rereplace();
604 }
605 _ => {}
606 }
607 }
608
609 pub fn move_focused(&mut self, f: impl FnOnce(&mut Editor)) {
611 match self.focused_panel {
612 Self::PANEL_REGEX => f(&mut self.regex_editor),
613 Self::PANEL_TEST => f(&mut self.test_editor),
614 Self::PANEL_REPLACE => f(&mut self.replace_editor),
615 _ => {}
616 }
617 }
618
619 pub fn run_benchmark(&mut self) {
620 let pattern = self.regex_editor.content().to_string();
621 let text = self.test_editor.content().to_string();
622 if pattern.is_empty() || text.is_empty() {
623 return;
624 }
625
626 let mut results = Vec::new();
627 for kind in EngineKind::all() {
628 let eng = engine::create_engine(kind);
629 let compile_start = Instant::now();
630 let compiled = match eng.compile(&pattern, &self.flags) {
631 Ok(c) => c,
632 Err(e) => {
633 results.push(BenchmarkResult {
634 engine: kind,
635 compile_time: compile_start.elapsed(),
636 match_time: Duration::ZERO,
637 match_count: 0,
638 error: Some(e.to_string()),
639 });
640 continue;
641 }
642 };
643 let compile_time = compile_start.elapsed();
644 let match_start = Instant::now();
645 let (match_count, error) = match compiled.find_matches(&text) {
646 Ok(matches) => (matches.len(), None),
647 Err(e) => (0, Some(e.to_string())),
648 };
649 results.push(BenchmarkResult {
650 engine: kind,
651 compile_time,
652 match_time: match_start.elapsed(),
653 match_count,
654 error,
655 });
656 }
657 self.benchmark_results = results;
658 self.overlay.benchmark = true;
659 }
660
661 pub fn regex101_url(&self) -> String {
663 let pattern = self.regex_editor.content();
664 let test_string = self.test_editor.content();
665
666 let flavor = match self.engine_kind {
667 #[cfg(feature = "pcre2-engine")]
668 EngineKind::Pcre2 => "pcre2",
669 _ => "ecmascript",
670 };
671
672 let mut flags = String::from("g");
673 if self.flags.case_insensitive {
674 flags.push('i');
675 }
676 if self.flags.multi_line {
677 flags.push('m');
678 }
679 if self.flags.dot_matches_newline {
680 flags.push('s');
681 }
682 if self.flags.unicode {
683 flags.push('u');
684 }
685 if self.flags.extended {
686 flags.push('x');
687 }
688
689 format!(
690 "https://regex101.com/?regex={}&testString={}&flags={}&flavor={}",
691 url_encode(pattern),
692 url_encode(test_string),
693 url_encode(&flags),
694 flavor,
695 )
696 }
697
698 pub fn copy_regex101_url(&mut self) {
700 let url = self.regex101_url();
701 self.copy_to_clipboard(&url, "regex101 URL copied to clipboard");
702 }
703
704 pub fn generate_code(&mut self, lang: &crate::codegen::Language) {
706 let pattern = self.regex_editor.content().to_string();
707 if pattern.is_empty() {
708 self.status
709 .set("No pattern to generate code for".to_string());
710 return;
711 }
712 let code = crate::codegen::generate_code(lang, &pattern, &self.flags);
713 self.copy_to_clipboard(&code, &format!("{lang} code copied to clipboard"));
714 self.overlay.codegen = false;
715 }
716
717 #[cfg(feature = "pcre2-engine")]
718 pub fn start_debug(&mut self, max_steps: usize) {
719 use crate::engine::pcre2_debug::{self, DebugSession};
720
721 let pattern = self.regex_editor.content().to_string();
722 let subject = self.test_editor.content().to_string();
723 if pattern.is_empty() || subject.is_empty() {
724 self.status
725 .set("Debugger needs both a pattern and test string".to_string());
726 return;
727 }
728
729 if self.engine_kind != EngineKind::Pcre2 {
730 self.switch_engine_to(EngineKind::Pcre2);
731 self.recompute();
732 }
733
734 if let Some(ref cached) = self.debug_cache {
737 if cached.pattern == pattern && cached.subject == subject {
738 self.debug_session = self.debug_cache.take();
739 return;
740 }
741 }
742
743 let start_offset = self.selected_match_start();
744
745 match pcre2_debug::debug_match(&pattern, &subject, &self.flags, max_steps, start_offset) {
746 Ok(trace) => {
747 self.debug_session = Some(DebugSession {
748 trace,
749 step: 0,
750 show_heatmap: false,
751 pattern,
752 subject,
753 });
754 }
755 Err(e) => {
756 self.status.set(format!("Debugger error: {e}"));
757 }
758 }
759 }
760
761 #[cfg(not(feature = "pcre2-engine"))]
762 pub fn start_debug(&mut self, _max_steps: usize) {
763 self.status
764 .set("Debugger requires PCRE2 (build with --features pcre2-engine)".to_string());
765 }
766
767 #[cfg(feature = "pcre2-engine")]
768 fn selected_match_start(&self) -> usize {
769 if !self.matches.is_empty() && self.selection.match_index < self.matches.len() {
770 self.matches[self.selection.match_index].start
771 } else {
772 0
773 }
774 }
775
776 #[cfg(feature = "pcre2-engine")]
777 pub fn close_debug(&mut self) {
778 self.debug_cache = self.debug_session.take();
779 }
780
781 pub fn debug_step_forward(&mut self) {
782 #[cfg(feature = "pcre2-engine")]
783 if let Some(ref mut s) = self.debug_session {
784 if s.step + 1 < s.trace.steps.len() {
785 s.step += 1;
786 }
787 }
788 }
789
790 pub fn debug_step_back(&mut self) {
791 #[cfg(feature = "pcre2-engine")]
792 if let Some(ref mut s) = self.debug_session {
793 s.step = s.step.saturating_sub(1);
794 }
795 }
796
797 pub fn debug_jump_start(&mut self) {
798 #[cfg(feature = "pcre2-engine")]
799 if let Some(ref mut s) = self.debug_session {
800 s.step = 0;
801 }
802 }
803
804 pub fn debug_jump_end(&mut self) {
805 #[cfg(feature = "pcre2-engine")]
806 if let Some(ref mut s) = self.debug_session {
807 if !s.trace.steps.is_empty() {
808 s.step = s.trace.steps.len() - 1;
809 }
810 }
811 }
812
813 pub fn debug_next_match(&mut self) {
814 #[cfg(feature = "pcre2-engine")]
815 if let Some(ref mut s) = self.debug_session {
816 let current_attempt = s.trace.steps.get(s.step).map_or(0, |st| st.match_attempt);
817 for (i, step) in s.trace.steps.iter().enumerate().skip(s.step + 1) {
818 if step.match_attempt > current_attempt {
819 s.step = i;
820 return;
821 }
822 }
823 }
824 }
825
826 pub fn debug_next_backtrack(&mut self) {
827 #[cfg(feature = "pcre2-engine")]
828 if let Some(ref mut s) = self.debug_session {
829 for (i, step) in s.trace.steps.iter().enumerate().skip(s.step + 1) {
830 if step.is_backtrack {
831 s.step = i;
832 return;
833 }
834 }
835 }
836 }
837
838 pub fn debug_toggle_heatmap(&mut self) {
839 #[cfg(feature = "pcre2-engine")]
840 if let Some(ref mut s) = self.debug_session {
841 s.show_heatmap = !s.show_heatmap;
842 }
843 }
844
845 pub fn handle_action(&mut self, action: Action, debug_max_steps: usize) {
846 match action {
847 Action::Quit => {
848 self.should_quit = true;
849 }
850 Action::OutputAndQuit => {
851 self.output_on_quit = true;
852 self.should_quit = true;
853 }
854 Action::SwitchPanel => {
855 if self.focused_panel == Self::PANEL_REGEX {
856 self.commit_pattern_to_history();
857 }
858 self.focused_panel = (self.focused_panel + 1) % Self::PANEL_COUNT;
859 }
860 Action::SwitchPanelBack => {
861 if self.focused_panel == Self::PANEL_REGEX {
862 self.commit_pattern_to_history();
863 }
864 self.focused_panel =
865 (self.focused_panel + Self::PANEL_COUNT - 1) % Self::PANEL_COUNT;
866 }
867 Action::SwitchEngine => {
868 self.switch_engine();
869 }
870 Action::Undo => {
871 if self.focused_panel == Self::PANEL_REGEX && self.regex_editor.undo() {
872 self.recompute();
873 } else if self.focused_panel == Self::PANEL_TEST && self.test_editor.undo() {
874 self.rematch();
875 } else if self.focused_panel == Self::PANEL_REPLACE && self.replace_editor.undo() {
876 self.rereplace();
877 }
878 }
879 Action::Redo => {
880 if self.focused_panel == Self::PANEL_REGEX && self.regex_editor.redo() {
881 self.recompute();
882 } else if self.focused_panel == Self::PANEL_TEST && self.test_editor.redo() {
883 self.rematch();
884 } else if self.focused_panel == Self::PANEL_REPLACE && self.replace_editor.redo() {
885 self.rereplace();
886 }
887 }
888 Action::HistoryPrev => {
889 if self.focused_panel == Self::PANEL_REGEX {
890 self.history_prev();
891 }
892 }
893 Action::HistoryNext => {
894 if self.focused_panel == Self::PANEL_REGEX {
895 self.history_next();
896 }
897 }
898 Action::CopyMatch => {
899 if self.focused_panel == Self::PANEL_REGEX {
900 self.copy_pattern();
901 } else if self.focused_panel == Self::PANEL_MATCHES {
902 self.copy_selected_match();
903 }
904 }
905 Action::ToggleWhitespace => {
906 self.show_whitespace = !self.show_whitespace;
907 }
908 Action::ToggleQuickref => {
909 self.show_quickref = !self.show_quickref;
910 }
911 Action::ToggleCaseInsensitive => {
912 self.flags.toggle_case_insensitive();
913 self.recompute();
914 }
915 Action::ToggleMultiLine => {
916 self.flags.toggle_multi_line();
917 self.recompute();
918 }
919 Action::ToggleDotAll => {
920 self.flags.toggle_dot_matches_newline();
921 self.recompute();
922 }
923 Action::ToggleUnicode => {
924 self.flags.toggle_unicode();
925 self.recompute();
926 }
927 Action::ToggleExtended => {
928 self.flags.toggle_extended();
929 self.recompute();
930 }
931 Action::ShowHelp => {
932 self.overlay.help = true;
933 }
934 Action::OpenRecipes => {
935 self.overlay.recipes = true;
936 self.overlay.recipe_index = 0;
937 }
938 Action::OpenGrex => {
939 self.overlay.grex = Some(crate::ui::grex_overlay::GrexOverlayState::default());
940 }
941 Action::Benchmark => {
942 self.run_benchmark();
943 }
944 Action::ExportRegex101 => {
945 self.copy_regex101_url();
946 }
947 Action::GenerateCode => {
948 self.overlay.codegen = true;
949 self.overlay.codegen_language_index = 0;
950 }
951 Action::InsertChar(c) => self.edit_focused(|ed| ed.insert_char(c)),
952 Action::InsertNewline => {
953 if self.focused_panel == Self::PANEL_TEST {
954 self.test_editor.insert_newline();
955 self.rematch();
956 }
957 }
958 Action::DeleteBack => self.edit_focused(Editor::delete_back),
959 Action::DeleteForward => self.edit_focused(Editor::delete_forward),
960 Action::MoveCursorLeft => self.move_focused(Editor::move_left),
961 Action::MoveCursorRight => self.move_focused(Editor::move_right),
962 Action::MoveCursorWordLeft => self.move_focused(Editor::move_word_left),
963 Action::MoveCursorWordRight => self.move_focused(Editor::move_word_right),
964 Action::ScrollUp => match self.focused_panel {
965 Self::PANEL_TEST => self.test_editor.move_up(),
966 Self::PANEL_MATCHES => self.select_match_prev(),
967 Self::PANEL_EXPLAIN => self.scroll_explain_up(),
968 _ => {}
969 },
970 Action::ScrollDown => match self.focused_panel {
971 Self::PANEL_TEST => self.test_editor.move_down(),
972 Self::PANEL_MATCHES => self.select_match_next(),
973 Self::PANEL_EXPLAIN => self.scroll_explain_down(),
974 _ => {}
975 },
976 Action::MoveCursorHome => self.move_focused(Editor::move_home),
977 Action::MoveCursorEnd => self.move_focused(Editor::move_end),
978 Action::DeleteCharAtCursor => self.edit_focused(Editor::delete_char_at_cursor),
979 Action::DeleteLine => self.edit_focused(Editor::delete_line),
980 Action::ChangeLine => self.edit_focused(Editor::clear_line),
981 Action::OpenLineBelow => {
982 if self.focused_panel == Self::PANEL_TEST {
983 self.test_editor.open_line_below();
984 self.rematch();
985 } else {
986 self.vim_state.cancel_insert();
987 }
988 }
989 Action::OpenLineAbove => {
990 if self.focused_panel == Self::PANEL_TEST {
991 self.test_editor.open_line_above();
992 self.rematch();
993 } else {
994 self.vim_state.cancel_insert();
995 }
996 }
997 Action::MoveToFirstNonBlank => self.move_focused(Editor::move_to_first_non_blank),
998 Action::MoveToFirstLine => self.move_focused(Editor::move_to_first_line),
999 Action::MoveToLastLine => self.move_focused(Editor::move_to_last_line),
1000 Action::MoveCursorWordForwardEnd => self.move_focused(Editor::move_word_forward_end),
1001 Action::EnterInsertMode => {}
1002 Action::EnterInsertModeAppend => self.move_focused(Editor::move_right),
1003 Action::EnterInsertModeLineStart => self.move_focused(Editor::move_to_first_non_blank),
1004 Action::EnterInsertModeLineEnd => self.move_focused(Editor::move_end),
1005 Action::EnterNormalMode => self.move_focused(Editor::move_left_in_line),
1006 Action::PasteClipboard => {
1007 if let Ok(mut cb) = arboard::Clipboard::new() {
1008 if let Ok(text) = cb.get_text() {
1009 self.edit_focused(|ed| ed.insert_str(&text));
1010 }
1011 }
1012 }
1013 Action::ToggleDebugger => {
1014 #[cfg(feature = "pcre2-engine")]
1015 if self.debug_session.is_some() {
1016 self.close_debug();
1017 } else {
1018 self.start_debug(debug_max_steps);
1019 }
1020 #[cfg(not(feature = "pcre2-engine"))]
1021 self.start_debug(debug_max_steps);
1022 }
1023 Action::SaveWorkspace | Action::None => {}
1024 }
1025 }
1026
1027 pub fn maybe_run_grex_generation(&mut self) {
1031 let Some(overlay) = self.overlay.grex.as_mut() else {
1032 return;
1033 };
1034 let Some(deadline) = overlay.debounce_deadline else {
1035 return;
1036 };
1037 if std::time::Instant::now() < deadline {
1038 return;
1039 }
1040 overlay.debounce_deadline = None;
1041 overlay.generation_counter += 1;
1042 let counter = overlay.generation_counter;
1043 let examples: Vec<String> = overlay
1044 .editor
1045 .content()
1046 .lines()
1047 .filter(|l| !l.is_empty())
1048 .map(ToString::to_string)
1049 .collect();
1050 let options = overlay.options;
1051 let tx = self.grex_result_tx.clone();
1052
1053 tokio::task::spawn_blocking(move || {
1054 let pattern = crate::grex_integration::generate(&examples, options);
1055 let _ = tx.send((counter, pattern));
1056 });
1057 }
1058
1059 pub fn drain_grex_results(&mut self) {
1062 while let Ok((counter, pattern)) = self.grex_result_rx.try_recv() {
1063 if let Some(overlay) = self.overlay.grex.as_mut() {
1064 if counter == overlay.generation_counter {
1065 overlay.generated_pattern = Some(pattern);
1066 }
1067 }
1068 }
1069 }
1070
1071 pub fn dispatch_grex_overlay_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
1074 use crossterm::event::{KeyCode, KeyModifiers};
1075 const DEBOUNCE_MS: u64 = 150;
1076 let debounce = std::time::Duration::from_millis(DEBOUNCE_MS);
1077
1078 let Some(overlay) = self.overlay.grex.as_mut() else {
1079 return false;
1080 };
1081
1082 match key.code {
1084 KeyCode::Esc => {
1085 self.overlay.grex = None;
1086 return true;
1087 }
1088 KeyCode::Tab => {
1089 let pattern = overlay
1090 .generated_pattern
1091 .as_deref()
1092 .filter(|p| !p.is_empty())
1093 .map(str::to_string);
1094 if let Some(pattern) = pattern {
1095 self.set_pattern(&pattern);
1096 self.overlay.grex = None;
1097 }
1098 return true;
1099 }
1100 _ => {}
1101 }
1102
1103 if key.modifiers.contains(KeyModifiers::ALT) {
1105 match key.code {
1106 KeyCode::Char('d') => {
1107 overlay.options.digit = !overlay.options.digit;
1108 overlay.debounce_deadline = Some(std::time::Instant::now() + debounce);
1109 return true;
1110 }
1111 KeyCode::Char('a') => {
1112 overlay.options.anchors = !overlay.options.anchors;
1113 overlay.debounce_deadline = Some(std::time::Instant::now() + debounce);
1114 return true;
1115 }
1116 KeyCode::Char('c') => {
1117 overlay.options.case_insensitive = !overlay.options.case_insensitive;
1118 overlay.debounce_deadline = Some(std::time::Instant::now() + debounce);
1119 return true;
1120 }
1121 _ => {}
1122 }
1123 }
1124
1125 let mut consumed = true;
1127 match key.code {
1128 KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
1129 overlay.editor.insert_char(c);
1130 }
1131 KeyCode::Enter => overlay.editor.insert_newline(),
1132 KeyCode::Backspace => overlay.editor.delete_back(),
1133 KeyCode::Delete => overlay.editor.delete_forward(),
1134 KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => {
1135 overlay.editor.move_word_left();
1136 }
1137 KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => {
1138 overlay.editor.move_word_right();
1139 }
1140 KeyCode::Left => overlay.editor.move_left(),
1141 KeyCode::Right => overlay.editor.move_right(),
1142 KeyCode::Up => overlay.editor.move_up(),
1143 KeyCode::Down => overlay.editor.move_down(),
1144 KeyCode::Home => overlay.editor.move_home(),
1145 KeyCode::End => overlay.editor.move_end(),
1146 _ => consumed = false,
1147 }
1148
1149 if consumed {
1150 overlay.debounce_deadline = Some(std::time::Instant::now() + debounce);
1151 }
1152 consumed
1153 }
1154
1155 pub fn help_page_max_scroll(&self) -> u16 {
1156 let total_lines = self.help_pages_lengths[&self.engine_kind][self.overlay.help_page];
1157 total_lines.saturating_sub(ui::HELP_PAGE_HEIGHT)
1158 }
1159}
1160
1161fn url_encode(s: &str) -> String {
1162 let mut out = String::with_capacity(s.len() * 3);
1163 for b in s.bytes() {
1164 match b {
1165 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1166 out.push(b as char);
1167 }
1168 _ => {
1169 let _ = write!(out, "%{b:02X}");
1170 }
1171 }
1172 }
1173 out
1174}
1175
1176fn print_colored_matches(text: &str, matches: &[engine::Match]) {
1177 let mut pos = 0;
1178 for m in matches {
1179 if m.start > pos {
1180 print!("{}", &text[pos..m.start]);
1181 }
1182 print!("{RED_BOLD}{}{RESET}", &text[m.start..m.end]);
1183 pos = m.end;
1184 }
1185 if pos < text.len() {
1186 print!("{}", &text[pos..]);
1187 }
1188 if !text.ends_with('\n') {
1189 println!();
1190 }
1191}
1192
1193fn print_colored_replace(output: &str, segments: &[engine::ReplaceSegment]) {
1195 for seg in segments {
1196 let chunk = &output[seg.start..seg.end];
1197 if seg.is_replacement {
1198 print!("{GREEN_BOLD}{chunk}{RESET}");
1199 } else {
1200 print!("{chunk}");
1201 }
1202 }
1203 if !output.ends_with('\n') {
1204 println!();
1205 }
1206}