use std::collections::BTreeSet;
const LEXICAL_STOPWORDS: &[&str] = &[
"a", "an", "the", "of", "in", "on", "to", "for", "with", "and", "or", "by", "as", "at", "is",
"are", "be", "it", "this", "that", "from", "into", "via", "add", "new", "make", "use", "using",
];
pub fn lexical_tokens(s: &str) -> BTreeSet<String> {
let mut tokens = BTreeSet::new();
let mut current = String::new();
let mut prev_lower_or_digit = false;
for ch in s.chars() {
if ch.is_alphanumeric() {
if ch.is_uppercase() && prev_lower_or_digit && !current.is_empty() {
push_token(&mut tokens, ¤t);
current.clear();
}
current.extend(ch.to_lowercase());
prev_lower_or_digit = ch.is_lowercase() || ch.is_numeric();
} else {
if !current.is_empty() {
push_token(&mut tokens, ¤t);
current.clear();
}
prev_lower_or_digit = false;
}
}
if !current.is_empty() {
push_token(&mut tokens, ¤t);
}
tokens
}
fn push_token(tokens: &mut BTreeSet<String>, tok: &str) {
if tok.len() > 1 && !LEXICAL_STOPWORDS.contains(&tok) {
tokens.insert(tok.to_string());
}
}
pub fn truncate_str(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
let target = max.saturating_sub(3);
let mut end = target;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &s[..end])
}
}
pub fn truncate_path(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
let target = s.len() - max + 3;
let mut start = target;
while start < s.len() && !s.is_char_boundary(start) {
start += 1;
}
format!("...{}", &s[start..])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toks(s: &str) -> Vec<String> {
lexical_tokens(s).into_iter().collect()
}
#[test]
fn test_lexical_tokens_path_splits_on_separators() {
assert_eq!(
toks("src/embeddings/openai.rs"),
vec!["embeddings", "openai", "rs", "src"]
);
}
#[test]
fn test_lexical_tokens_underscore_splits_each_word() {
assert_eq!(toks("run_sql_duckdb"), vec!["duckdb", "run", "sql"]);
}
#[test]
fn test_lexical_tokens_camelcase() {
assert_eq!(toks("parseSolidity"), vec!["parse", "solidity"]);
assert_eq!(toks("SmartConfig"), vec!["config", "smart"]);
assert_eq!(toks("v1Symbols"), vec!["symbols", "v1"]);
}
#[test]
fn test_lexical_tokens_stopwords_and_length() {
assert_eq!(
toks("add a new output format to ctx sql"),
vec!["ctx", "format", "output", "sql"]
);
assert_eq!(
toks("generate embeddings with openai"),
vec!["embeddings", "generate", "openai"]
);
}
#[test]
fn test_truncate_str_ascii() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello", 5), "hello");
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("abcdefghij", 7), "abcd...");
}
#[test]
fn test_truncate_str_unicode() {
let box_line = "┌────────────────────┐";
let result = truncate_str(box_line, 10);
assert!(result.ends_with("..."));
let emoji = "Hello 🎉🎊🎁 World";
let result = truncate_str(emoji, 10);
assert!(result.ends_with("..."));
let chinese = "你好世界测试";
let result = truncate_str(chinese, 8);
assert!(result.ends_with("..."));
}
#[test]
fn test_truncate_path_ascii() {
assert_eq!(truncate_path("src/main.rs", 20), "src/main.rs");
let result = truncate_path("/very/long/path/to/file.rs", 15);
assert!(result.starts_with("..."));
assert!(result.contains("file.rs"));
}
#[test]
fn test_truncate_path_unicode() {
let path = "/home/用户/项目/文件.rs";
let result = truncate_path(path, 15);
assert!(result.starts_with("..."));
let path = "/home/📁/🎉/file.rs";
let result = truncate_path(path, 12);
assert!(result.starts_with("..."));
}
#[test]
fn test_truncate_edge_cases() {
assert_eq!(truncate_str("hello", 3), "...");
assert_eq!(truncate_str("hi", 3), "hi");
assert_eq!(truncate_str("", 10), "");
assert_eq!(truncate_path("", 10), "");
}
}