File size: 3,742 Bytes
d90101d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Span {
    pub start: usize,
    pub end: usize,
}

impl Span {
    pub fn new(start: usize, end: usize) -> Self {
        Self { start, end }
    }
}

pub struct SearchTerm {
    line: String,
    position: usize,
}

impl SearchTerm {
    pub fn new(line: &str, position: usize) -> Self {
        if position > line.len() {
            panic!(
                "Position {position} is out of bounds: string '{line}' (length: {})",
                line.len()
            );
        }
        Self { line: line.to_string(), position }
    }

    /// Get the search term from the line based on '@' marker or cursor position
    ///
    /// If '@' marker is present, returns the word following it.
    /// Otherwise, returns the word at the cursor position.
    /// If no word is found, returns None.
    pub fn process(&self) -> Option<TermResult<'_>> {
        // Ensure position is on a UTF-8 character boundary to prevent panics
        let safe_position = if self.line.is_char_boundary(self.position) {
            self.position
        } else {
            // Find the nearest lower character boundary
            (0..self.position)
                .rev()
                .find(|&i| self.line.is_char_boundary(i))
                .unwrap_or(0)
        };

        let prefix = self.line.get(..safe_position)?;
        let at_pos = prefix.rfind('@')?;
        let start_pos = at_pos + 1;
        let term = self.line.get(start_pos..safe_position)?;

        Some(TermResult { span: Span::new(start_pos, safe_position), term })
    }
}

#[derive(Debug)]
pub struct TermResult<'a> {
    pub span: Span,
    pub term: &'a str,
}

#[cfg(test)]
mod tests {
    use insta::assert_debug_snapshot;

    use super::SearchTerm;

    impl SearchTerm {
        fn test(line: &str) -> Vec<TermSpec> {
            // Test at each valid character boundary position, starting from 1
            (1..=line.len())
                .filter(|&pos| line.is_char_boundary(pos))
                .map(|pos| {
                    let input = SearchTerm::new(line, pos);
                    let output = input.process();
                    let (a, b) = line.split_at(pos);

                    TermSpec {
                        pos,
                        input: format!("{a}[{b}"),
                        output: output.as_ref().map(|term| term.term.to_string()),
                        span_start: output.as_ref().map(|term| term.span.start),
                        span_end: output.as_ref().map(|term| term.span.end),
                    }
                })
                .collect()
        }
    }

    #[derive(Debug)]
    #[allow(dead_code)] // Used to generate test snapshots
    struct TermSpec {
        input: String,
        output: Option<String>,
        span_start: Option<usize>,
        span_end: Option<usize>,
        pos: usize,
    }

    #[test]
    fn test_marker_based_search() {
        let results = SearchTerm::test("@abc @def ghi@");
        assert_debug_snapshot!(results);
    }

    #[test]
    fn test_marker_based_search_chinese() {
        let results = SearchTerm::test("@你好 @世界 测试@");
        assert_debug_snapshot!(results);
    }

    #[test]
    fn test_marker_based_search_mixed_chinese_english() {
        let results = SearchTerm::test("@hello @世界 test@中文");
        assert_debug_snapshot!(results);
    }

    #[test]
    fn test_marker_based_search_chinese_with_spaces() {
        let results = SearchTerm::test("@中 文 @测试");
        assert_debug_snapshot!(results);
    }

    #[test]
    fn test_marker_based_search_emoji() {
        let results = SearchTerm::test("@🚀 @🌟 emoji@");
        assert_debug_snapshot!(results);
    }
}