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