1use std::collections::BTreeSet;
6
7const LEXICAL_STOPWORDS: &[&str] = &[
11 "a", "an", "the", "of", "in", "on", "to", "for", "with", "and", "or", "by", "as", "at", "is",
12 "are", "be", "it", "this", "that", "from", "into", "via", "add", "new", "make", "use", "using",
13];
14
15pub fn lexical_tokens(s: &str) -> BTreeSet<String> {
23 let mut tokens = BTreeSet::new();
24 let mut current = String::new();
25 let mut prev_lower_or_digit = false;
28
29 for ch in s.chars() {
30 if ch.is_alphanumeric() {
31 if ch.is_uppercase() && prev_lower_or_digit && !current.is_empty() {
32 push_token(&mut tokens, ¤t);
33 current.clear();
34 }
35 current.extend(ch.to_lowercase());
36 prev_lower_or_digit = ch.is_lowercase() || ch.is_numeric();
37 } else {
38 if !current.is_empty() {
39 push_token(&mut tokens, ¤t);
40 current.clear();
41 }
42 prev_lower_or_digit = false;
43 }
44 }
45 if !current.is_empty() {
46 push_token(&mut tokens, ¤t);
47 }
48 tokens
49}
50
51fn push_token(tokens: &mut BTreeSet<String>, tok: &str) {
53 if tok.len() > 1 && !LEXICAL_STOPWORDS.contains(&tok) {
54 tokens.insert(tok.to_string());
55 }
56}
57
58pub fn truncate_str(s: &str, max: usize) -> String {
70 if s.len() <= max {
71 s.to_string()
72 } else {
73 let target = max.saturating_sub(3);
74 let mut end = target;
75 while end > 0 && !s.is_char_boundary(end) {
76 end -= 1;
77 }
78 format!("{}...", &s[..end])
79 }
80}
81
82pub fn truncate_path(s: &str, max: usize) -> String {
94 if s.len() <= max {
95 s.to_string()
96 } else {
97 let target = s.len() - max + 3;
98 let mut start = target;
99 while start < s.len() && !s.is_char_boundary(start) {
100 start += 1;
101 }
102 format!("...{}", &s[start..])
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn toks(s: &str) -> Vec<String> {
111 lexical_tokens(s).into_iter().collect()
112 }
113
114 #[test]
115 fn test_lexical_tokens_path_splits_on_separators() {
116 assert_eq!(
118 toks("src/embeddings/openai.rs"),
119 vec!["embeddings", "openai", "rs", "src"]
120 );
121 }
122
123 #[test]
124 fn test_lexical_tokens_underscore_splits_each_word() {
125 assert_eq!(toks("run_sql_duckdb"), vec!["duckdb", "run", "sql"]);
127 }
128
129 #[test]
130 fn test_lexical_tokens_camelcase() {
131 assert_eq!(toks("parseSolidity"), vec!["parse", "solidity"]);
133 assert_eq!(toks("SmartConfig"), vec!["config", "smart"]);
134 assert_eq!(toks("v1Symbols"), vec!["symbols", "v1"]);
136 }
137
138 #[test]
139 fn test_lexical_tokens_stopwords_and_length() {
140 assert_eq!(
142 toks("add a new output format to ctx sql"),
143 vec!["ctx", "format", "output", "sql"]
144 );
145 assert_eq!(
146 toks("generate embeddings with openai"),
147 vec!["embeddings", "generate", "openai"]
148 );
149 }
150
151 #[test]
152 fn test_truncate_str_ascii() {
153 assert_eq!(truncate_str("hello", 10), "hello");
155 assert_eq!(truncate_str("hello", 5), "hello");
156
157 assert_eq!(truncate_str("hello world", 8), "hello...");
159 assert_eq!(truncate_str("abcdefghij", 7), "abcd...");
160 }
161
162 #[test]
163 fn test_truncate_str_unicode() {
164 let box_line = "┌────────────────────┐";
166 let result = truncate_str(box_line, 10);
167 assert!(result.ends_with("..."));
168 let emoji = "Hello 🎉🎊🎁 World";
172 let result = truncate_str(emoji, 10);
173 assert!(result.ends_with("..."));
174
175 let chinese = "你好世界测试";
177 let result = truncate_str(chinese, 8);
178 assert!(result.ends_with("..."));
179 }
180
181 #[test]
182 fn test_truncate_path_ascii() {
183 assert_eq!(truncate_path("src/main.rs", 20), "src/main.rs");
185
186 let result = truncate_path("/very/long/path/to/file.rs", 15);
188 assert!(result.starts_with("..."));
189 assert!(result.contains("file.rs"));
190 }
191
192 #[test]
193 fn test_truncate_path_unicode() {
194 let path = "/home/用户/项目/文件.rs";
196 let result = truncate_path(path, 15);
197 assert!(result.starts_with("..."));
198 let path = "/home/📁/🎉/file.rs";
202 let result = truncate_path(path, 12);
203 assert!(result.starts_with("..."));
204 }
205
206 #[test]
207 fn test_truncate_edge_cases() {
208 assert_eq!(truncate_str("hello", 3), "...");
210 assert_eq!(truncate_str("hi", 3), "hi");
211
212 assert_eq!(truncate_str("", 10), "");
214 assert_eq!(truncate_path("", 10), "");
215 }
216}