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