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::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(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    pub 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().join(".")))
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    pub 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    pub 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 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    pub 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 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
531fn get_variable_direction(
532    vr: &VariableRef,
533    wave_container_opt: Option<&WaveContainer>,
534) -> VariableDirection {
535    match wave_container_opt {
536        Some(wave_container) => wave_container
537            .variable_meta(vr)
538            .map_or(VariableDirection::Unknown, |m| {
539                m.direction.unwrap_or(VariableDirection::Unknown)
540            }),
541        None => VariableDirection::Unknown,
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn test_empty_filter_matches_all() {
551        let filter = VariableFilter::new();
552        assert!(filter.name_filter_str.is_empty());
553
554        let mut filter_fn = filter.name_filter_fn();
555        // Empty filter should match everything
556        assert!(filter_fn("test"));
557        assert!(filter_fn("anything"));
558        assert!(filter_fn(""));
559    }
560
561    #[test]
562    fn test_contain_filter_basic() {
563        let mut filter = VariableFilter::new();
564        filter.name_filter_type = VariableNameFilterType::Contain;
565        filter.name_filter_str = "clock".to_string();
566        filter.name_filter_case_insensitive = false;
567
568        let mut filter_fn = filter.name_filter_fn();
569        assert!(filter_fn("clock"));
570        assert!(filter_fn("my_clock"));
571        assert!(filter_fn("clock_signal"));
572        assert!(filter_fn("sys_clock_div"));
573        assert!(!filter_fn("clk"));
574        assert!(!filter_fn("CLOCK")); // Case sensitive
575    }
576
577    #[test]
578    fn test_contain_filter_case_insensitive() {
579        let mut filter = VariableFilter::new();
580        filter.name_filter_type = VariableNameFilterType::Contain;
581        filter.name_filter_str = "Clock".to_string();
582        filter.name_filter_case_insensitive = true;
583
584        let mut filter_fn = filter.name_filter_fn();
585        assert!(filter_fn("clock"));
586        assert!(filter_fn("CLOCK"));
587        assert!(filter_fn("ClOcK"));
588        assert!(filter_fn("my_Clock_signal"));
589        assert!(!filter_fn("clk"));
590    }
591
592    #[test]
593    fn test_start_filter() {
594        let mut filter = VariableFilter::new();
595        filter.name_filter_type = VariableNameFilterType::Start;
596        filter.name_filter_str = "sys".to_string();
597        filter.name_filter_case_insensitive = false;
598
599        let mut filter_fn = filter.name_filter_fn();
600        assert!(filter_fn("sys"));
601        assert!(filter_fn("sys_clock"));
602        assert!(filter_fn("system"));
603        assert!(!filter_fn("my_sys"));
604        assert!(!filter_fn("SYS")); // Case sensitive
605    }
606
607    #[test]
608    fn test_start_filter_case_insensitive() {
609        let mut filter = VariableFilter::new();
610        filter.name_filter_type = VariableNameFilterType::Start;
611        filter.name_filter_str = "Sys".to_string();
612        filter.name_filter_case_insensitive = true;
613
614        let mut filter_fn = filter.name_filter_fn();
615        assert!(filter_fn("sys"));
616        assert!(filter_fn("SYS_CLOCK"));
617        assert!(filter_fn("System"));
618        assert!(!filter_fn("my_sys"));
619    }
620
621    #[test]
622    fn test_regex_filter_valid() {
623        let mut filter = VariableFilter::new();
624        filter.name_filter_type = VariableNameFilterType::Regex;
625        filter.name_filter_str = r"^clk_\d+$".to_string();
626        filter.name_filter_case_insensitive = false;
627
628        let mut filter_fn = filter.name_filter_fn();
629        assert!(filter_fn("clk_0"));
630        assert!(filter_fn("clk_123"));
631        assert!(!filter_fn("clk_"));
632        assert!(!filter_fn("clk_abc"));
633        assert!(!filter_fn("my_clk_0"));
634    }
635
636    #[test]
637    fn test_regex_filter_invalid() {
638        let mut filter = VariableFilter::new();
639        filter.name_filter_type = VariableNameFilterType::Regex;
640        filter.name_filter_str = "[invalid(".to_string(); // Invalid regex
641        filter.name_filter_case_insensitive = false;
642
643        // Should not match anything when regex is invalid
644        let mut filter_fn = filter.name_filter_fn();
645        assert!(!filter_fn("anything"));
646        assert!(!filter_fn("test"));
647
648        // Should report as invalid
649        assert!(filter.is_regex_and_invalid());
650
651        // Should have an error message
652        let error = filter.regex_error();
653        assert!(error.is_some());
654        assert!(error.unwrap().contains("unclosed"));
655    }
656
657    #[test]
658    fn test_is_regex_and_invalid_only_for_regex_type() {
659        let mut filter = VariableFilter::new();
660        filter.name_filter_str = "[invalid(".to_string();
661
662        // Not regex type, so should return false even with invalid pattern
663        filter.name_filter_type = VariableNameFilterType::Contain;
664        // Cache rebuild
665        let _ = filter.name_filter_fn();
666        assert!(!filter.is_regex_and_invalid());
667
668        filter.name_filter_type = VariableNameFilterType::Start;
669        // Cache rebuild
670        let _ = filter.name_filter_fn();
671        assert!(!filter.is_regex_and_invalid());
672
673        filter.name_filter_type = VariableNameFilterType::Fuzzy;
674        // Cache rebuild
675        let _ = filter.name_filter_fn();
676        assert!(!filter.is_regex_and_invalid());
677
678        // Only regex type should check validity
679        filter.name_filter_type = VariableNameFilterType::Regex;
680        // Cache rebuild
681        let _ = filter.name_filter_fn();
682        assert!(filter.is_regex_and_invalid());
683    }
684
685    #[test]
686    fn test_regex_error_only_for_regex_type() {
687        let mut filter = VariableFilter::new();
688        filter.name_filter_str = "[invalid(".to_string();
689
690        // Force cache rebuild
691        filter.name_filter_type = VariableNameFilterType::Regex;
692        let _ = filter.name_filter_fn();
693
694        // Now switch to non-regex types
695        filter.name_filter_type = VariableNameFilterType::Contain;
696        assert!(filter.regex_error().is_none());
697
698        filter.name_filter_type = VariableNameFilterType::Start;
699        assert!(filter.regex_error().is_none());
700
701        // Back to regex should show error
702        filter.name_filter_type = VariableNameFilterType::Regex;
703        assert!(filter.regex_error().is_some());
704    }
705
706    #[test]
707    fn test_fuzzy_filter() {
708        let mut filter = VariableFilter::new();
709        filter.name_filter_type = VariableNameFilterType::Fuzzy;
710        filter.name_filter_str = "clk".to_string();
711        filter.name_filter_case_insensitive = true;
712
713        let mut filter_fn = filter.name_filter_fn();
714        // Fuzzy should match with characters in order
715        assert!(filter_fn("clock"));
716        assert!(filter_fn("c_l_k"));
717        assert!(filter_fn("call_lock"));
718        assert!(!filter_fn("kclc")); // Wrong order
719    }
720
721    #[test]
722    fn test_special_chars_escaped_in_contain() {
723        let mut filter = VariableFilter::new();
724        filter.name_filter_type = VariableNameFilterType::Contain;
725        // These are regex special chars that should be escaped
726        filter.name_filter_str = "sig[0]".to_string();
727        filter.name_filter_case_insensitive = false;
728
729        let mut filter_fn = filter.name_filter_fn();
730        assert!(filter_fn("sig[0]"));
731        assert!(filter_fn("my_sig[0]_data"));
732        assert!(!filter_fn("sig0")); // Should require literal brackets
733        assert!(!filter_fn("siga")); // [0] is escaped, not a regex char class
734    }
735
736    #[test]
737    fn test_special_chars_escaped_in_start() {
738        let mut filter = VariableFilter::new();
739        filter.name_filter_type = VariableNameFilterType::Start;
740        filter.name_filter_str = "data.value".to_string();
741        filter.name_filter_case_insensitive = false;
742
743        let mut filter_fn = filter.name_filter_fn();
744        assert!(filter_fn("data.value"));
745        assert!(filter_fn("data.value_out"));
746        assert!(!filter_fn("dataxvalue")); // Dot should be literal
747        assert!(!filter_fn("my_data.value")); // Must start with pattern
748    }
749
750    #[test]
751    fn test_cache_reuses_compiled_regex() {
752        let mut filter = VariableFilter::new();
753        filter.name_filter_type = VariableNameFilterType::Regex;
754        filter.name_filter_str = r"\d+".to_string();
755        filter.name_filter_case_insensitive = false;
756
757        // First call compiles
758        let mut fn1 = filter.name_filter_fn();
759        assert!(fn1("123"));
760
761        // Second call should reuse cached regex
762        let mut fn2 = filter.name_filter_fn();
763        assert!(fn2("456"));
764
765        // Verify cache has the pattern
766        let cache = filter.cache.borrow();
767        assert_eq!(cache.regex_pattern.as_ref().unwrap(), r"\d+");
768        assert!(cache.regex.is_some());
769    }
770
771    #[test]
772    fn test_cache_rebuilds_on_pattern_change() {
773        let mut filter = VariableFilter::new();
774        filter.name_filter_type = VariableNameFilterType::Contain;
775        filter.name_filter_str = "old".to_string();
776        filter.name_filter_case_insensitive = false;
777
778        let mut fn1 = filter.name_filter_fn();
779        assert!(fn1("old_value"));
780
781        // Change pattern
782        filter.name_filter_str = "new".to_string();
783        let mut fn2 = filter.name_filter_fn();
784        assert!(fn2("new_value"));
785        assert!(!fn2("old_value"));
786    }
787
788    #[test]
789    fn test_cache_rebuilds_on_case_sensitivity_change() {
790        let mut filter = VariableFilter::new();
791        filter.name_filter_type = VariableNameFilterType::Contain;
792        filter.name_filter_str = "Test".to_string();
793        filter.name_filter_case_insensitive = false;
794
795        let mut fn1 = filter.name_filter_fn();
796        assert!(!fn1("test")); // Case sensitive
797
798        // Change case sensitivity
799        filter.name_filter_case_insensitive = true;
800        let mut fn2 = filter.name_filter_fn();
801        assert!(fn2("test")); // Now case insensitive
802    }
803
804    #[test]
805    fn test_default_filter_settings() {
806        let filter = VariableFilter::new();
807
808        assert_eq!(filter.name_filter_type, VariableNameFilterType::Contain);
809        assert_eq!(filter.name_filter_str, "");
810        assert!(filter.name_filter_case_insensitive);
811
812        assert!(filter.include_inputs);
813        assert!(filter.include_outputs);
814        assert!(filter.include_inouts);
815        assert!(filter.include_others);
816
817        assert!(!filter.group_by_direction);
818    }
819}