Skip to main content

libsurfer/
variable_filter.rs

1//! Filtering of the variable list.
2use derive_more::Display;
3use egui::collapsing_header::CollapsingState;
4use egui::{Button, Layout, RichText, TextEdit, Ui};
5use egui_remixicon::icons;
6use emath::{Align, Vec2};
7use enum_iterator::Sequence;
8use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
9use itertools::Itertools;
10use regex::{Regex, RegexBuilder, escape};
11use serde::{Deserialize, Serialize};
12use std::cell::RefCell;
13
14use crate::data_container::DataContainer::Transactions;
15use crate::transaction_container::{StreamScopeRef, TransactionStreamRef};
16use crate::variable_direction::VariableDirectionExt;
17use crate::wave_container::{VariableRefExt, WaveContainer};
18use crate::wave_data::ScopeType;
19use crate::{SystemState, message::Message, wave_container::VariableRef};
20use surfer_translation_types::VariableDirection;
21
22use std::cmp::Ordering;
23
24pub const VARIABLE_FILTER_ID: &str = "variable-filter";
25
26#[derive(Clone, Debug, Display, PartialEq, Serialize, Deserialize, Sequence)]
27pub enum VariableNameFilterType {
28    #[display("Fuzzy")]
29    Fuzzy,
30
31    #[display("Regular expression")]
32    Regex,
33
34    #[display("Variable starts with")]
35    Start,
36
37    #[display("Variable contains")]
38    Contain,
39}
40
41#[derive(Serialize, Deserialize)]
42pub struct VariableFilter {
43    pub(crate) name_filter_type: VariableNameFilterType,
44    pub(crate) name_filter_str: String,
45    pub(crate) name_filter_case_insensitive: bool,
46
47    pub(crate) include_inputs: bool,
48    pub(crate) include_outputs: bool,
49    pub(crate) include_inouts: bool,
50    pub(crate) include_others: bool,
51
52    pub(crate) group_by_direction: bool,
53    #[serde(skip)]
54    cache: RefCell<VariableFilterRegexCache>,
55}
56
57// Lightweight cache for compiled regex and fuzzy matcher to avoid repeated compilation
58#[derive(Default)]
59struct VariableFilterRegexCache {
60    // For regex-based filters (Regex, Start, Contain)
61    regex_pattern: Option<String>,
62    regex_case_insensitive: bool,
63    regex: Option<Regex>,
64    regex_error: Option<String>,
65}
66
67#[derive(Debug, Deserialize)]
68pub enum VariableIOFilterType {
69    Input,
70    Output,
71    InOut,
72    Other,
73}
74
75impl Default for VariableFilter {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl VariableFilter {
82    #[must_use]
83    pub fn new() -> VariableFilter {
84        VariableFilter {
85            name_filter_type: VariableNameFilterType::Contain,
86            name_filter_str: String::new(),
87            name_filter_case_insensitive: true,
88
89            include_inputs: true,
90            include_outputs: true,
91            include_inouts: true,
92            include_others: true,
93
94            group_by_direction: false,
95            cache: RefCell::new(Default::default()),
96        }
97    }
98
99    fn name_filter_fn(&self) -> Box<dyn FnMut(&str) -> bool> {
100        if self.name_filter_str.is_empty() {
101            if self.name_filter_type == VariableNameFilterType::Regex {
102                // Clear cached regex when filter string is empty
103                let mut cache = self.cache.borrow_mut();
104                cache.regex_pattern = None;
105                cache.regex = None;
106                cache.regex_error = None;
107            }
108            return Box::new(|_var_name| true);
109        }
110
111        // Copy the decisions/inputs out of self so the borrow of self.cache can be short-lived.
112        let filter_type = &self.name_filter_type;
113        let filter_str = self.name_filter_str.clone();
114        let case_insensitive = self.name_filter_case_insensitive;
115
116        // Prepare owned clones that we will move into the returned closure.
117        let mut owned_regex: Option<Regex> = None;
118
119        if *filter_type != VariableNameFilterType::Fuzzy
120        // Short-lived borrow of the cache to potentially rebuild and to clone out owned values.
121        {
122            let mut cache = self.cache.borrow_mut();
123
124            let pat = match filter_type {
125                VariableNameFilterType::Regex => filter_str.clone(),
126                VariableNameFilterType::Start => format!("^{}", escape(&filter_str)),
127                VariableNameFilterType::Contain => escape(&filter_str),
128                VariableNameFilterType::Fuzzy => unreachable!(),
129            };
130            let rebuild = (cache.regex_pattern.as_ref() != Some(&pat))
131                || cache.regex_case_insensitive != case_insensitive
132                || cache.regex.is_none();
133
134            if rebuild {
135                cache.regex_pattern = Some(pat.clone());
136                cache.regex_case_insensitive = case_insensitive;
137                match RegexBuilder::new(&pat)
138                    .case_insensitive(case_insensitive)
139                    .build()
140                {
141                    Ok(r) => {
142                        cache.regex = Some(r);
143                        cache.regex_error = None;
144                    }
145                    Err(e) => {
146                        cache.regex = None;
147                        cache.regex_error = Some(e.to_string());
148                    }
149                }
150            }
151
152            if let Some(r) = cache.regex.as_ref() {
153                owned_regex = Some(r.clone());
154            }
155        } // cache borrow ends here
156
157        // Now build the closure using only owned values (no borrow of cache/self remains).
158        match filter_type {
159            VariableNameFilterType::Fuzzy => {
160                let mut matcher = SkimMatcherV2::default();
161                matcher = if case_insensitive {
162                    matcher.ignore_case()
163                } else {
164                    matcher.respect_case()
165                };
166                let pat = filter_str;
167                Box::new(move |var_name| matcher.fuzzy_match(var_name, &pat).is_some())
168            }
169            VariableNameFilterType::Regex
170            | VariableNameFilterType::Start
171            | VariableNameFilterType::Contain => {
172                if let Some(regex) = owned_regex {
173                    Box::new(move |var_name| regex.is_match(var_name))
174                } else {
175                    Box::new(|_var_name| false)
176                }
177            }
178        }
179    }
180
181    fn kind_filter(&self, vr: &VariableRef, wave_container_opt: Option<&WaveContainer>) -> bool {
182        match get_variable_direction(vr, wave_container_opt) {
183            VariableDirection::Input => self.include_inputs,
184            VariableDirection::Output => self.include_outputs,
185            VariableDirection::InOut => self.include_inouts,
186            _ => self.include_others,
187        }
188    }
189
190    fn matching_variables(
191        &self,
192        variables: &[VariableRef],
193        wave_container_opt: Option<&WaveContainer>,
194        full_path: bool,
195    ) -> Vec<VariableRef> {
196        let mut name_filter = self.name_filter_fn();
197        if full_path {
198            variables
199                .iter()
200                .filter(|&vr| self.kind_filter(vr, wave_container_opt))
201                .filter(|&vr| name_filter(&vr.full_path_string()))
202                .cloned()
203                .collect_vec()
204        } else {
205            variables
206                .iter()
207                .filter(|&vr| self.kind_filter(vr, wave_container_opt))
208                .filter(|&vr| name_filter(&vr.name))
209                .cloned()
210                .collect_vec()
211        }
212    }
213
214    /// Returns true if the current `name_filter_type` is `Regex` and the cached
215    /// compiled regex is invalid.
216    fn is_regex_and_invalid(&self) -> bool {
217        if self.name_filter_type != VariableNameFilterType::Regex {
218            return false;
219        }
220        let cache = self.cache.borrow();
221        cache.regex_error.is_some()
222    }
223
224    /// Returns the regex error message if the current filter type is Regex and
225    /// the regex compilation failed.
226    fn regex_error(&self) -> Option<String> {
227        if self.name_filter_type != VariableNameFilterType::Regex {
228            return None;
229        }
230        let cache = self.cache.borrow();
231        cache.regex_error.clone()
232    }
233}
234
235impl SystemState {
236    pub(crate) fn draw_variable_filter_edit(
237        &mut self,
238        ui: &mut Ui,
239        msgs: &mut Vec<Message>,
240        full_path: bool,
241    ) {
242        ui.with_layout(Layout::top_down(Align::LEFT), |ui| {
243            CollapsingState::load_with_default_open(
244                ui.ctx(),
245                ui.make_persistent_id("variable_filter"),
246                false,
247            )
248            .show_header(ui, |ui| {
249                ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
250                    let default_padding = ui.spacing().button_padding;
251                    ui.spacing_mut().button_padding = Vec2 {
252                        x: 0.,
253                        y: default_padding.y,
254                    };
255                    if ui
256                        .button(icons::ADD_FILL)
257                        .on_hover_text("Add all variables from active Scope")
258                        .clicked()
259                    {
260                        self.add_filtered_variables(msgs, full_path);
261                    }
262                    if ui
263                        .add_enabled(
264                            !self.user.variable_filter.name_filter_str.is_empty(),
265                            Button::new(icons::CLOSE_FILL),
266                        )
267                        .on_hover_text("Clear filter")
268                        .clicked()
269                    {
270                        self.user.variable_filter.name_filter_str.clear();
271                    }
272
273                    // Create text edit with isolated style for invalid regex
274                    let is_invalid = self.user.variable_filter.is_regex_and_invalid();
275                    let error_msg = self.user.variable_filter.regex_error();
276
277                    // Save original style to restore after
278                    let original_bg = ui.style().visuals.extreme_bg_color;
279
280                    if is_invalid {
281                        ui.style_mut().visuals.extreme_bg_color =
282                            self.user.config.theme.accent_error.background;
283                    }
284
285                    let mut response = ui.add(
286                        TextEdit::singleline(&mut self.user.variable_filter.name_filter_str)
287                            .hint_text("Filter"),
288                    );
289
290                    // Restore original style immediately after rendering
291                    ui.style_mut().visuals.extreme_bg_color = original_bg;
292
293                    // Add hover text with error message if regex is invalid
294                    if let Some(err) = error_msg {
295                        response = response.on_hover_ui(|ui| {
296                            ui.label("Invalid regex:");
297                            // Use monospace font for error details as it contains position information
298                            ui.label(RichText::new(err).family(epaint::FontFamily::Monospace));
299                        });
300                    }
301
302                    // Handle focus request
303                    let request_focus = *self
304                        .text_edit_request_focus
305                        .get(VARIABLE_FILTER_ID)
306                        .unwrap_or(&false);
307                    if request_focus && !response.has_focus() {
308                        response.request_focus();
309                        msgs.push(Message::SetRequestTextEditFocus(
310                            VARIABLE_FILTER_ID.to_string(),
311                            false,
312                        ));
313                    }
314
315                    // Handle focus via generic widget focus messages
316                    if response.gained_focus() {
317                        msgs.push(Message::SetTextEditFocused(
318                            VARIABLE_FILTER_ID.to_string(),
319                            true,
320                        ));
321                    }
322                    if response.lost_focus() {
323                        msgs.push(Message::SetTextEditFocused(
324                            VARIABLE_FILTER_ID.to_string(),
325                            false,
326                        ));
327                    }
328                    ui.spacing_mut().button_padding = default_padding;
329                });
330            })
331            .body(|ui| self.variable_filter_type_menu(ui, msgs));
332        });
333    }
334
335    fn add_filtered_variables(&mut self, msgs: &mut Vec<Message>, full_path: bool) {
336        if let Some(waves) = self.user.waves.as_ref() {
337            if full_path {
338                let variables = waves.inner.as_waves().unwrap().variables();
339                msgs.push(Message::AddVariables(
340                    self.filtered_variables(&variables, false),
341                ));
342            } else {
343                // Iterate over the reversed list to get
344                // waves in the same order as the variable
345                // list
346                if let Some(active_scope) = waves.active_scope.as_ref() {
347                    match active_scope {
348                        ScopeType::WaveScope(active_scope) => {
349                            let variables = waves
350                                .inner
351                                .as_waves()
352                                .unwrap()
353                                .variables_in_scope(active_scope);
354                            msgs.push(Message::AddVariables(
355                                self.filtered_variables(&variables, false),
356                            ));
357                        }
358                        ScopeType::StreamScope(active_scope) => {
359                            if let Transactions(inner) = &waves.inner {
360                                match active_scope {
361                                    StreamScopeRef::Root => {
362                                        for stream in inner.get_streams() {
363                                            msgs.push(Message::AddStreamOrGenerator(
364                                                TransactionStreamRef::new_stream(
365                                                    stream.id,
366                                                    stream.name.clone(),
367                                                ),
368                                            ));
369                                        }
370                                    }
371                                    StreamScopeRef::Stream(s) => {
372                                        for gen_id in
373                                            &inner.get_stream(s.stream_id).unwrap().generators
374                                        {
375                                            let generator = inner.get_generator(*gen_id).unwrap();
376
377                                            msgs.push(Message::AddStreamOrGenerator(
378                                                TransactionStreamRef::new_gen(
379                                                    generator.stream_id,
380                                                    generator.id,
381                                                    generator.name.clone(),
382                                                ),
383                                            ));
384                                        }
385                                    }
386                                    StreamScopeRef::Empty(_) => {}
387                                }
388                            }
389                        }
390                    }
391                }
392            }
393        }
394    }
395
396    fn variable_filter_type_menu(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
397        // Checkbox wants a mutable bool reference but we don't have mutable self to give it a
398        // mutable 'group_by_direction' directly. Plus we want to update things via a message. So
399        // make a copy of the flag here that can be mutable and just ensure we update the actual
400        // flag on a click.
401        let mut name_filter_case_insensitive =
402            self.user.variable_filter.name_filter_case_insensitive;
403
404        if ui
405            .checkbox(&mut name_filter_case_insensitive, "Case insensitive")
406            .clicked()
407        {
408            msgs.push(Message::SetVariableNameFilterCaseInsensitive(
409                !self.user.variable_filter.name_filter_case_insensitive,
410            ));
411        }
412
413        ui.separator();
414
415        for filter_type in enum_iterator::all::<VariableNameFilterType>() {
416            if ui
417                .radio(
418                    self.user.variable_filter.name_filter_type == filter_type,
419                    filter_type.to_string(),
420                )
421                .clicked()
422            {
423                msgs.push(Message::SetVariableNameFilterType(filter_type));
424            }
425        }
426
427        ui.separator();
428
429        // Checkbox wants a mutable bool reference but we don't have mutable self to give it a
430        // mutable 'group_by_direction' directly. Plus we want to update things via a message. So
431        // make a copy of the flag here that can be mutable and just ensure we update the actual
432        // flag on a click.
433        let mut group_by_direction = self.user.variable_filter.group_by_direction;
434
435        if ui
436            .checkbox(&mut group_by_direction, "Group by direction")
437            .clicked()
438        {
439            msgs.push(Message::SetVariableGroupByDirection(
440                !self.user.variable_filter.group_by_direction,
441            ));
442        }
443
444        ui.separator();
445
446        ui.horizontal(|ui| {
447            let input = VariableDirection::Input;
448            let output = VariableDirection::Output;
449            let inout = VariableDirection::InOut;
450
451            if ui
452                .add(
453                    Button::new(input.get_icon().unwrap())
454                        .selected(self.user.variable_filter.include_inputs),
455                )
456                .on_hover_text("Show inputs")
457                .clicked()
458            {
459                msgs.push(Message::SetVariableIOFilter(
460                    VariableIOFilterType::Input,
461                    !self.user.variable_filter.include_inputs,
462                ));
463            }
464
465            if ui
466                .add(
467                    Button::new(output.get_icon().unwrap())
468                        .selected(self.user.variable_filter.include_outputs),
469                )
470                .on_hover_text("Show outputs")
471                .clicked()
472            {
473                msgs.push(Message::SetVariableIOFilter(
474                    VariableIOFilterType::Output,
475                    !self.user.variable_filter.include_outputs,
476                ));
477            }
478
479            if ui
480                .add(
481                    Button::new(inout.get_icon().unwrap())
482                        .selected(self.user.variable_filter.include_inouts),
483                )
484                .on_hover_text("Show inouts")
485                .clicked()
486            {
487                msgs.push(Message::SetVariableIOFilter(
488                    VariableIOFilterType::InOut,
489                    !self.user.variable_filter.include_inouts,
490                ));
491            }
492
493            if ui
494                .add(
495                    Button::new(icons::GLOBAL_LINE)
496                        .selected(self.user.variable_filter.include_others),
497                )
498                .on_hover_text("Show others")
499                .clicked()
500            {
501                msgs.push(Message::SetVariableIOFilter(
502                    VariableIOFilterType::Other,
503                    !self.user.variable_filter.include_others,
504                ));
505            }
506        });
507    }
508
509    pub fn variable_cmp(
510        &self,
511        a: &VariableRef,
512        b: &VariableRef,
513        wave_container: Option<&WaveContainer>,
514    ) -> Ordering {
515        // Fast path: if not grouping by direction, just compare names
516        if !self.user.variable_filter.group_by_direction {
517            return numeric_sort::cmp(&a.name, &b.name);
518        }
519
520        let a_direction = get_variable_direction(a, wave_container);
521        let b_direction = get_variable_direction(b, wave_container);
522
523        if a_direction == b_direction {
524            numeric_sort::cmp(&a.name, &b.name)
525        } else if a_direction < b_direction {
526            Ordering::Less
527        } else {
528            Ordering::Greater
529        }
530    }
531
532    pub(crate) fn filtered_variables(
533        &self,
534        variables: &[VariableRef],
535        full_path: bool,
536    ) -> Vec<VariableRef> {
537        let wave_container = match &self.user.waves {
538            Some(wd) => wd.inner.as_waves(),
539            None => None,
540        };
541
542        self.user
543            .variable_filter
544            .matching_variables(variables, wave_container, full_path)
545            .iter()
546            .sorted_by(|a, b| self.variable_cmp(a, b, wave_container))
547            .cloned()
548            .collect_vec()
549    }
550
551    /// Like `filtered_variables` but skips sorting — use when the caller will sort the result
552    /// itself (e.g. `build_variable_rows`).
553    pub(crate) fn filtered_variables_unsorted(
554        &self,
555        variables: &[VariableRef],
556        full_path: bool,
557    ) -> Vec<VariableRef> {
558        let wave_container = match &self.user.waves {
559            Some(wd) => wd.inner.as_waves(),
560            None => None,
561        };
562
563        self.user
564            .variable_filter
565            .matching_variables(variables, wave_container, full_path)
566            .clone()
567    }
568}
569
570fn get_variable_direction(
571    vr: &VariableRef,
572    wave_container_opt: Option<&WaveContainer>,
573) -> VariableDirection {
574    match wave_container_opt {
575        Some(wave_container) => wave_container
576            .variable_meta(vr)
577            .map_or(VariableDirection::Unknown, |m| {
578                m.direction.unwrap_or(VariableDirection::Unknown)
579            }),
580        None => VariableDirection::Unknown,
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    #[test]
589    fn test_empty_filter_matches_all() {
590        let filter = VariableFilter::new();
591        assert!(filter.name_filter_str.is_empty());
592
593        let mut filter_fn = filter.name_filter_fn();
594        // Empty filter should match everything
595        assert!(filter_fn("test"));
596        assert!(filter_fn("anything"));
597        assert!(filter_fn(""));
598    }
599
600    #[test]
601    fn test_contain_filter_basic() {
602        let mut filter = VariableFilter::new();
603        filter.name_filter_type = VariableNameFilterType::Contain;
604        filter.name_filter_str = "clock".to_string();
605        filter.name_filter_case_insensitive = false;
606
607        let mut filter_fn = filter.name_filter_fn();
608        assert!(filter_fn("clock"));
609        assert!(filter_fn("my_clock"));
610        assert!(filter_fn("clock_signal"));
611        assert!(filter_fn("sys_clock_div"));
612        assert!(!filter_fn("clk"));
613        assert!(!filter_fn("CLOCK")); // Case sensitive
614    }
615
616    #[test]
617    fn test_contain_filter_case_insensitive() {
618        let mut filter = VariableFilter::new();
619        filter.name_filter_type = VariableNameFilterType::Contain;
620        filter.name_filter_str = "Clock".to_string();
621        filter.name_filter_case_insensitive = true;
622
623        let mut filter_fn = filter.name_filter_fn();
624        assert!(filter_fn("clock"));
625        assert!(filter_fn("CLOCK"));
626        assert!(filter_fn("ClOcK"));
627        assert!(filter_fn("my_Clock_signal"));
628        assert!(!filter_fn("clk"));
629    }
630
631    #[test]
632    fn test_start_filter() {
633        let mut filter = VariableFilter::new();
634        filter.name_filter_type = VariableNameFilterType::Start;
635        filter.name_filter_str = "sys".to_string();
636        filter.name_filter_case_insensitive = false;
637
638        let mut filter_fn = filter.name_filter_fn();
639        assert!(filter_fn("sys"));
640        assert!(filter_fn("sys_clock"));
641        assert!(filter_fn("system"));
642        assert!(!filter_fn("my_sys"));
643        assert!(!filter_fn("SYS")); // Case sensitive
644    }
645
646    #[test]
647    fn test_start_filter_case_insensitive() {
648        let mut filter = VariableFilter::new();
649        filter.name_filter_type = VariableNameFilterType::Start;
650        filter.name_filter_str = "Sys".to_string();
651        filter.name_filter_case_insensitive = true;
652
653        let mut filter_fn = filter.name_filter_fn();
654        assert!(filter_fn("sys"));
655        assert!(filter_fn("SYS_CLOCK"));
656        assert!(filter_fn("System"));
657        assert!(!filter_fn("my_sys"));
658    }
659
660    #[test]
661    fn test_regex_filter_valid() {
662        let mut filter = VariableFilter::new();
663        filter.name_filter_type = VariableNameFilterType::Regex;
664        filter.name_filter_str = r"^clk_\d+$".to_string();
665        filter.name_filter_case_insensitive = false;
666
667        let mut filter_fn = filter.name_filter_fn();
668        assert!(filter_fn("clk_0"));
669        assert!(filter_fn("clk_123"));
670        assert!(!filter_fn("clk_"));
671        assert!(!filter_fn("clk_abc"));
672        assert!(!filter_fn("my_clk_0"));
673    }
674
675    #[test]
676    fn test_regex_filter_invalid() {
677        let mut filter = VariableFilter::new();
678        filter.name_filter_type = VariableNameFilterType::Regex;
679        filter.name_filter_str = "[invalid(".to_string(); // Invalid regex
680        filter.name_filter_case_insensitive = false;
681
682        // Should not match anything when regex is invalid
683        let mut filter_fn = filter.name_filter_fn();
684        assert!(!filter_fn("anything"));
685        assert!(!filter_fn("test"));
686
687        // Should report as invalid
688        assert!(filter.is_regex_and_invalid());
689
690        // Should have an error message
691        let error = filter.regex_error();
692        assert!(error.is_some());
693        assert!(error.unwrap().contains("unclosed"));
694    }
695
696    #[test]
697    fn test_is_regex_and_invalid_only_for_regex_type() {
698        let mut filter = VariableFilter::new();
699        filter.name_filter_str = "[invalid(".to_string();
700
701        // Not regex type, so should return false even with invalid pattern
702        filter.name_filter_type = VariableNameFilterType::Contain;
703        // Cache rebuild
704        let _ = filter.name_filter_fn();
705        assert!(!filter.is_regex_and_invalid());
706
707        filter.name_filter_type = VariableNameFilterType::Start;
708        // Cache rebuild
709        let _ = filter.name_filter_fn();
710        assert!(!filter.is_regex_and_invalid());
711
712        filter.name_filter_type = VariableNameFilterType::Fuzzy;
713        // Cache rebuild
714        let _ = filter.name_filter_fn();
715        assert!(!filter.is_regex_and_invalid());
716
717        // Only regex type should check validity
718        filter.name_filter_type = VariableNameFilterType::Regex;
719        // Cache rebuild
720        let _ = filter.name_filter_fn();
721        assert!(filter.is_regex_and_invalid());
722    }
723
724    #[test]
725    fn test_regex_error_only_for_regex_type() {
726        let mut filter = VariableFilter::new();
727        filter.name_filter_str = "[invalid(".to_string();
728
729        // Force cache rebuild
730        filter.name_filter_type = VariableNameFilterType::Regex;
731        let _ = filter.name_filter_fn();
732
733        // Now switch to non-regex types
734        filter.name_filter_type = VariableNameFilterType::Contain;
735        assert!(filter.regex_error().is_none());
736
737        filter.name_filter_type = VariableNameFilterType::Start;
738        assert!(filter.regex_error().is_none());
739
740        // Back to regex should show error
741        filter.name_filter_type = VariableNameFilterType::Regex;
742        assert!(filter.regex_error().is_some());
743    }
744
745    #[test]
746    fn test_fuzzy_filter() {
747        let mut filter = VariableFilter::new();
748        filter.name_filter_type = VariableNameFilterType::Fuzzy;
749        filter.name_filter_str = "clk".to_string();
750        filter.name_filter_case_insensitive = true;
751
752        let mut filter_fn = filter.name_filter_fn();
753        // Fuzzy should match with characters in order
754        assert!(filter_fn("clock"));
755        assert!(filter_fn("c_l_k"));
756        assert!(filter_fn("call_lock"));
757        assert!(!filter_fn("kclc")); // Wrong order
758    }
759
760    #[test]
761    fn test_special_chars_escaped_in_contain() {
762        let mut filter = VariableFilter::new();
763        filter.name_filter_type = VariableNameFilterType::Contain;
764        // These are regex special chars that should be escaped
765        filter.name_filter_str = "sig[0]".to_string();
766        filter.name_filter_case_insensitive = false;
767
768        let mut filter_fn = filter.name_filter_fn();
769        assert!(filter_fn("sig[0]"));
770        assert!(filter_fn("my_sig[0]_data"));
771        assert!(!filter_fn("sig0")); // Should require literal brackets
772        assert!(!filter_fn("siga")); // [0] is escaped, not a regex char class
773    }
774
775    #[test]
776    fn test_special_chars_escaped_in_start() {
777        let mut filter = VariableFilter::new();
778        filter.name_filter_type = VariableNameFilterType::Start;
779        filter.name_filter_str = "data.value".to_string();
780        filter.name_filter_case_insensitive = false;
781
782        let mut filter_fn = filter.name_filter_fn();
783        assert!(filter_fn("data.value"));
784        assert!(filter_fn("data.value_out"));
785        assert!(!filter_fn("dataxvalue")); // Dot should be literal
786        assert!(!filter_fn("my_data.value")); // Must start with pattern
787    }
788
789    #[test]
790    fn test_cache_reuses_compiled_regex() {
791        let mut filter = VariableFilter::new();
792        filter.name_filter_type = VariableNameFilterType::Regex;
793        filter.name_filter_str = r"\d+".to_string();
794        filter.name_filter_case_insensitive = false;
795
796        // First call compiles
797        let mut fn1 = filter.name_filter_fn();
798        assert!(fn1("123"));
799
800        // Second call should reuse cached regex
801        let mut fn2 = filter.name_filter_fn();
802        assert!(fn2("456"));
803
804        // Verify cache has the pattern
805        let cache = filter.cache.borrow();
806        assert_eq!(cache.regex_pattern.as_ref().unwrap(), r"\d+");
807        assert!(cache.regex.is_some());
808    }
809
810    #[test]
811    fn test_cache_rebuilds_on_pattern_change() {
812        let mut filter = VariableFilter::new();
813        filter.name_filter_type = VariableNameFilterType::Contain;
814        filter.name_filter_str = "old".to_string();
815        filter.name_filter_case_insensitive = false;
816
817        let mut fn1 = filter.name_filter_fn();
818        assert!(fn1("old_value"));
819
820        // Change pattern
821        filter.name_filter_str = "new".to_string();
822        let mut fn2 = filter.name_filter_fn();
823        assert!(fn2("new_value"));
824        assert!(!fn2("old_value"));
825    }
826
827    #[test]
828    fn test_cache_rebuilds_on_case_sensitivity_change() {
829        let mut filter = VariableFilter::new();
830        filter.name_filter_type = VariableNameFilterType::Contain;
831        filter.name_filter_str = "Test".to_string();
832        filter.name_filter_case_insensitive = false;
833
834        let mut fn1 = filter.name_filter_fn();
835        assert!(!fn1("test")); // Case sensitive
836
837        // Change case sensitivity
838        filter.name_filter_case_insensitive = true;
839        let mut fn2 = filter.name_filter_fn();
840        assert!(fn2("test")); // Now case insensitive
841    }
842
843    #[test]
844    fn test_default_filter_settings() {
845        let filter = VariableFilter::new();
846
847        assert_eq!(filter.name_filter_type, VariableNameFilterType::Contain);
848        assert_eq!(filter.name_filter_str, "");
849        assert!(filter.name_filter_case_insensitive);
850
851        assert!(filter.include_inputs);
852        assert!(filter.include_outputs);
853        assert!(filter.include_inouts);
854        assert!(filter.include_others);
855
856        assert!(!filter.group_by_direction);
857    }
858}