1use std::error::Error;
2
3use crossterm::event::{
4 KeyCode,
5 KeyEvent,
6 KeyModifiers,
7};
8use itertools::Itertools;
9use ratatui::{
10 style::Styled,
11 text::{
12 Line,
13 Span,
14 },
15 widgets::{
16 StatefulWidget,
17 Widget,
18 },
19};
20use tracexec_core::{
21 event::EventId,
22 primitives::regex::{
23 IntoCursor,
24 engines::pikevm,
25 regex_automata::util::syntax,
26 },
27};
28use tui_prompts::{
29 State,
30 TextPrompt,
31 TextState,
32};
33
34use super::{
35 event_line::EventLine,
36 help::help_item,
37 theme::THEME,
38};
39use crate::action::Action;
40
41#[derive(Debug, Clone)]
42pub struct Query {
43 pub kind: QueryKind,
44 pub value: QueryValue,
45 pub case_sensitive: bool,
46}
47
48#[derive(Debug, Clone)]
49pub enum QueryValue {
50 Regex(pikevm::PikeVM),
51 Text(String),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum QueryKind {
56 Search,
57 Filter,
58}
59
60#[derive(Debug, Clone)]
61pub struct QueryResult {
62 pub indices: indexset::BTreeSet<EventId>,
64 pub searched_id: EventId,
66 pub selection: Option<EventId>,
68}
69
70impl Query {
71 pub fn new(kind: QueryKind, value: QueryValue, case_sensitive: bool) -> Self {
72 Self {
73 kind,
74 value,
75 case_sensitive,
76 }
77 }
78
79 pub fn matches(&self, text: &EventLine) -> bool {
80 let result = match &self.value {
81 QueryValue::Regex(re) => pikevm::is_match(
82 re,
83 &mut pikevm::Cache::new(re),
84 &mut tracexec_core::primitives::regex::Input::new(text.into_cursor()),
85 ),
86 QueryValue::Text(query) => {
87 if self.case_sensitive {
89 text.to_string().contains(query)
90 } else {
91 text
92 .to_string()
93 .to_lowercase()
94 .contains(&query.to_lowercase())
95 }
96 }
97 };
98 if result {
99 tracing::trace!("{text:?} matches: {self:?}");
100 }
101 result
102 }
103}
104
105impl QueryResult {
106 pub fn next_result(&mut self) {
107 if let Some(selection) = self.selection {
108 self.selection = match self.indices.range((selection + 1)..).next() {
109 Some(id) => Some(*id),
110 None => self.indices.first().copied(),
111 }
112 } else if !self.indices.is_empty() {
113 self.selection = self.indices.first().copied();
114 }
115 }
116
117 pub fn prev_result(&mut self) {
118 if let Some(selection) = self.selection {
119 self.selection = match self.indices.range(..selection).next_back() {
120 Some(id) => Some(*id),
121 None => self.indices.last().copied(),
122 };
123 } else if !self.indices.is_empty() {
124 self.selection = self.indices.last().copied();
125 }
126 }
127
128 pub fn selection(&self) -> Option<EventId> {
130 self.selection
131 }
132
133 pub fn statistics(&self) -> Line<'_> {
134 if self.indices.is_empty() {
135 "No match".set_style(THEME.query_no_match).into()
136 } else {
137 let total = self
138 .indices
139 .len()
140 .to_string()
141 .set_style(THEME.query_match_total_cnt);
142 let selected = self
143 .selection
144 .map(|index| self.indices.rank(&index) + 1)
145 .unwrap_or(0)
146 .to_string()
147 .set_style(THEME.query_match_current_no);
148 Line::default().spans(vec![selected, "/".into(), total])
149 }
150 }
151}
152
153pub struct QueryBuilder {
154 kind: QueryKind,
155 case_sensitive: bool,
156 is_regex: bool,
157 state: TextState<'static>,
158 editing: bool,
159}
160
161impl QueryBuilder {
162 pub fn new(kind: QueryKind) -> Self {
163 Self {
164 kind,
165 case_sensitive: false,
166 state: TextState::new(),
167 editing: true,
168 is_regex: false,
169 }
170 }
171
172 pub fn editing(&self) -> bool {
173 self.editing
174 }
175
176 pub fn edit(&mut self) {
177 self.editing = true;
178 self.state.focus();
179 }
180
181 pub fn cursor(&self) -> (u16, u16) {
184 self.state.cursor()
185 }
186
187 pub fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>, Vec<Line<'static>>> {
188 match (key.code, key.modifiers) {
189 (KeyCode::Enter, _) => {
190 let text = self.state.value();
191 if text.is_empty() {
192 return Ok(Some(Action::EndSearch));
193 }
194 let query = Query::new(
195 self.kind,
196 if self.is_regex {
197 QueryValue::Regex(
198 pikevm::Builder::new()
199 .syntax(syntax::Config::new().case_insensitive(!self.case_sensitive))
200 .build(text)
201 .map_err(|e| {
202 e.source()
203 .unwrap() .to_string()
205 .lines()
206 .map(|line| Line::raw(line.to_owned()))
207 .collect_vec()
208 })?,
209 )
210 } else {
211 QueryValue::Text(text.to_owned())
212 },
213 self.case_sensitive,
214 );
215 self.editing = false;
216 return Ok(Some(Action::ExecuteSearch(query)));
217 }
218 (KeyCode::Esc, KeyModifiers::NONE) => {
219 return Ok(Some(Action::EndSearch));
220 }
221 (KeyCode::Char('i'), KeyModifiers::ALT) => {
222 self.case_sensitive = !self.case_sensitive;
223 }
224 (KeyCode::Char('r'), KeyModifiers::ALT) => {
225 self.is_regex = !self.is_regex;
226 }
227 _ => {
228 self.state.handle_key_event(key);
229 }
230 }
231 Ok(None)
232 }
233}
234
235impl QueryBuilder {
236 pub fn help(&self) -> Vec<Span<'_>> {
237 if self.editing {
238 [
239 help_item!("Esc", "Cancel\u{00a0}Search"),
240 help_item!("Enter", "Execute\u{00a0}Search"),
241 help_item!(
242 "Alt+I",
243 if self.case_sensitive {
244 "Case\u{00a0}Sensitive"
245 } else {
246 "Case\u{00a0}Insensitive"
247 }
248 ),
249 help_item!(
250 "Alt+R",
251 if self.is_regex {
252 "Regex\u{00a0}Mode"
253 } else {
254 "Text\u{00a0}Mode"
255 }
256 ),
257 help_item!("Ctrl+U", "Clear"),
258 ]
259 .into_iter()
260 .flatten()
261 .collect()
262 } else {
263 [
264 help_item!("N", "Next\u{00a0}Match"),
265 help_item!("P", "Previous\u{00a0}Match"),
266 ]
267 .into_iter()
268 .flatten()
269 .collect()
270 }
271 }
272}
273
274impl Widget for &mut QueryBuilder {
275 fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
276 where
277 Self: Sized,
278 {
279 TextPrompt::new(
280 match self.kind {
281 QueryKind::Search => "🔍",
282 QueryKind::Filter => "☔",
283 }
284 .into(),
285 )
286 .render(area, buf, &mut self.state);
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use crossterm::event::{
293 KeyCode,
294 KeyEvent,
295 KeyModifiers,
296 };
297 use ratatui::text::Line;
298 use tracexec_core::event::EventId;
299
300 use super::*;
301
302 fn make_event_line(text: &str) -> EventLine {
303 EventLine {
304 line: Line::from(text.to_string()),
305 cwd_mask: None,
306 env_mask: None,
307 }
308 }
309
310 #[test]
311 fn test_query_matches_text_case() {
312 let line = make_event_line("Hello World");
313
314 let q = Query::new(
315 QueryKind::Search,
316 QueryValue::Text("hello".to_string()),
317 false,
318 );
319 assert!(q.matches(&line));
320
321 let q = Query::new(
322 QueryKind::Search,
323 QueryValue::Text("hello".to_string()),
324 true,
325 );
326 assert!(!q.matches(&line));
327 }
328
329 #[test]
330 fn test_query_matches_regex() {
331 let line = make_event_line("abc123");
332 let re = pikevm::Builder::new()
333 .syntax(syntax::Config::new().case_insensitive(false))
334 .build(r"\d+")
335 .unwrap();
336 let q = Query::new(QueryKind::Search, QueryValue::Regex(re), false);
337 assert!(q.matches(&line));
338
339 let re = pikevm::Builder::new()
340 .syntax(syntax::Config::new())
341 .build(r"xyz")
342 .unwrap();
343 let q = Query::new(QueryKind::Search, QueryValue::Regex(re), false);
344 assert!(!q.matches(&line));
345 }
346
347 #[test]
348 fn test_query_result_navigation() {
349 let mut qr = QueryResult {
350 indices: vec![1, 3, 5].into_iter().map(EventId::new).collect(),
351 searched_id: EventId::new(5),
352 selection: None,
353 };
354
355 assert_eq!(qr.selection(), None);
356 qr.next_result();
357 assert_eq!(qr.selection(), Some(EventId::new(1)));
358 qr.next_result();
359 assert_eq!(qr.selection(), Some(EventId::new(3)));
360 qr.next_result();
361 assert_eq!(qr.selection(), Some(EventId::new(5)));
362 qr.next_result(); assert_eq!(qr.selection(), Some(EventId::new(1)));
364 qr.next_result();
365 assert_eq!(qr.selection(), Some(EventId::new(3)));
366
367 qr.prev_result();
368 assert_eq!(qr.selection(), Some(EventId::new(1)));
369 qr.prev_result(); assert_eq!(qr.selection(), Some(EventId::new(1)));
371 qr.prev_result(); assert_eq!(qr.selection(), Some(EventId::new(1)));
373 }
374
375 #[test]
376 fn test_query_result_statistics() {
377 let qr = QueryResult {
378 indices: vec![10, 20, 30].into_iter().map(EventId::new).collect(),
379 searched_id: EventId::new(30),
380 selection: Some(EventId::new(20)),
381 };
382
383 let line = qr.statistics();
384 let s = line.to_string();
385 assert!(s.contains("2")); assert!(s.contains("3")); }
388
389 #[test]
390 fn test_query_builder_toggle_flags_and_enter() {
391 let mut qb = QueryBuilder::new(QueryKind::Search);
392 assert!(qb.editing());
393 assert!(!qb.case_sensitive);
394 assert!(!qb.is_regex);
395
396 qb.handle_key_events(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::ALT))
398 .unwrap();
399 assert!(qb.case_sensitive);
400
401 qb.handle_key_events(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::ALT))
403 .unwrap();
404 assert!(qb.is_regex);
405
406 let mut empty_qb = QueryBuilder::new(QueryKind::Search);
408 let action = empty_qb
409 .handle_key_events(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
410 .unwrap();
411 assert!(matches!(action, Some(Action::EndSearch)));
412 }
413
414 #[test]
415 fn test_query_builder_edit_and_cursor() {
416 let mut qb = QueryBuilder::new(QueryKind::Search);
417 qb.edit();
418 assert!(qb.editing());
419
420 let cursor = qb.cursor();
421 assert_eq!(cursor, (0, 0));
422 }
423
424 #[test]
425 fn test_query_builder_help() {
426 let qb = QueryBuilder::new(QueryKind::Search);
427 let help = qb.help();
428 assert!(help.iter().any(|span| span.content.contains("Esc")));
429 assert!(help.iter().any(|span| span.content.contains("Enter")));
430 }
431}