1use crate::engine::{self, CompiledRegex, EngineFlags, EngineKind, RegexEngine};
4use crate::filter::FilterOptions;
5use crate::input::editor::Editor;
6
7pub struct FilterApp {
8 pub pattern_editor: Editor,
9 pub options: FilterOptions,
10 pub lines: Vec<String>,
11 pub matched: Vec<usize>,
13 pub selected: usize,
15 pub scroll: usize,
17 pub error: Option<String>,
19 pub should_quit: bool,
21 pub outcome: Outcome,
23 engine: Box<dyn RegexEngine>,
24 engine_flags: EngineFlags,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Outcome {
29 Pending,
30 Emit,
31 Discard,
32}
33
34impl FilterApp {
35 pub fn new(lines: Vec<String>, initial_pattern: &str, options: FilterOptions) -> Self {
36 let pattern_editor = Editor::with_content(initial_pattern.to_string());
37 let engine_flags = EngineFlags {
38 case_insensitive: options.case_insensitive,
39 ..EngineFlags::default()
40 };
41 let engine = engine::create_engine(EngineKind::RustRegex);
42 let mut app = Self {
43 pattern_editor,
44 options,
45 lines,
46 matched: Vec::new(),
47 selected: 0,
48 scroll: 0,
49 error: None,
50 should_quit: false,
51 outcome: Outcome::Pending,
52 engine,
53 engine_flags,
54 };
55 app.recompute();
56 app
57 }
58
59 pub fn pattern(&self) -> &str {
60 self.pattern_editor.content()
61 }
62
63 pub fn recompute(&mut self) {
64 self.error = None;
65 let pattern = self.pattern().to_string();
66 if pattern.is_empty() {
67 self.matched = if self.options.invert {
68 Vec::new()
69 } else {
70 (0..self.lines.len()).collect()
71 };
72 self.clamp_selection();
73 return;
74 }
75 match self.engine.compile(&pattern, &self.engine_flags) {
76 Ok(compiled) => {
77 self.matched = self.collect_matches(&*compiled);
78 self.clamp_selection();
79 }
80 Err(err) => {
81 self.error = Some(err.to_string());
82 self.matched.clear();
83 self.selected = 0;
84 self.scroll = 0;
85 }
86 }
87 }
88
89 fn collect_matches(&self, compiled: &dyn CompiledRegex) -> Vec<usize> {
90 let mut out = Vec::with_capacity(self.lines.len());
91 for (idx, line) in self.lines.iter().enumerate() {
92 let hit = compiled
93 .find_matches(line)
94 .map(|v| !v.is_empty())
95 .unwrap_or(false);
96 if hit != self.options.invert {
97 out.push(idx);
98 }
99 }
100 out
101 }
102
103 fn clamp_selection(&mut self) {
104 if self.matched.is_empty() {
105 self.selected = 0;
106 self.scroll = 0;
107 } else if self.selected >= self.matched.len() {
108 self.selected = self.matched.len() - 1;
109 }
110 }
111
112 pub fn select_next(&mut self) {
113 if self.selected + 1 < self.matched.len() {
114 self.selected += 1;
115 }
116 }
117
118 pub fn select_prev(&mut self) {
119 self.selected = self.selected.saturating_sub(1);
120 }
121
122 pub fn toggle_case_insensitive(&mut self) {
123 self.options.case_insensitive = !self.options.case_insensitive;
124 self.engine_flags.case_insensitive = self.options.case_insensitive;
125 self.recompute();
126 }
127
128 pub fn toggle_invert(&mut self) {
129 self.options.invert = !self.options.invert;
130 self.recompute();
131 }
132}