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