pub fn utf8_prefix_at_or_before(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while !s.is_char_boundary(end) && end > 0 {
end -= 1;
}
&s[..end]
}
pub fn split_compound(name: &str) -> Vec<&str> {
if name.contains('_') {
return name.split('_').filter(|s| !s.is_empty()).collect();
}
let bytes = name.as_bytes();
let mut parts = Vec::new();
let mut start = 0;
for i in 1..bytes.len() {
let cur = bytes[i] as char;
let prev = bytes[i - 1] as char;
let boundary = prev.is_ascii_lowercase() && cur.is_ascii_uppercase();
let acronym_end = i + 1 < bytes.len()
&& prev.is_ascii_uppercase()
&& cur.is_ascii_uppercase()
&& (bytes[i + 1] as char).is_ascii_lowercase();
if boundary || acronym_end {
if i > start {
parts.push(&name[start..i]);
}
start = i;
}
}
if start < name.len() {
parts.push(&name[start..]);
}
parts
}
pub fn is_camel_case(word: &str) -> bool {
if word.len() < 2 {
return false;
}
if !word.chars().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
word[1..].chars().any(|c| c.is_ascii_uppercase())
}
#[must_use]
pub fn search_terms(name: &str, qualified_name: &str) -> String {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<&str> = Vec::new();
for word in name
.split(|c: char| !c.is_ascii_alphanumeric())
.chain(qualified_name.split(|c: char| !c.is_ascii_alphanumeric()))
{
if !is_camel_case(word) {
continue;
}
for part in split_compound(word) {
if part.len() >= 2 && seen.insert(part.to_ascii_lowercase()) {
out.push(part);
}
}
}
out.join(" ")
}
#[cfg(test)]
mod tests {
use super::{is_camel_case, search_terms, split_compound, utf8_prefix_at_or_before};
#[test]
fn split_compound_handles_camel_snake_screaming() {
assert_eq!(split_compound("getUserName"), vec!["get", "User", "Name"]);
assert_eq!(
split_compound("process_request"),
vec!["process", "request"]
);
assert_eq!(split_compound("MAX_RETRIES"), vec!["MAX", "RETRIES"]);
assert_eq!(split_compound("XMLParser"), vec!["XML", "Parser"]);
}
#[test]
fn is_camel_case_classifies() {
assert!(is_camel_case("UserService"));
assert!(is_camel_case("processRequest"));
assert!(!is_camel_case("user"));
assert!(!is_camel_case("U"));
assert!(!is_camel_case("process_request"));
}
#[test]
fn search_terms_emits_camel_parts_only() {
assert_eq!(
search_terms(
"rerank_candidates",
"src/context/ranking.rs::rerank_candidates"
),
""
);
let terms = search_terms(
"updateCloudClient",
"src/api/CloudSync.ts::updateCloudClient",
);
assert_eq!(terms, "update Cloud Client Sync");
}
#[test]
fn search_terms_dedupes_case_insensitively() {
let terms = search_terms("ParseJson", "parser/ParseJson.kt::ParseJson");
assert_eq!(terms, "Parse Json");
}
#[test]
fn returns_whole_string_when_under_budget() {
assert_eq!(utf8_prefix_at_or_before("hello", 10), "hello");
}
#[test]
fn returns_whole_string_when_at_budget() {
assert_eq!(utf8_prefix_at_or_before("hello", 5), "hello");
}
#[test]
fn truncates_ascii_at_budget() {
assert_eq!(utf8_prefix_at_or_before("abcdef", 3), "abc");
}
#[test]
fn walks_back_when_cut_lands_inside_multibyte_char() {
let s = format!("{}é", "a".repeat(20));
assert_eq!(utf8_prefix_at_or_before(&s, 21), "a".repeat(20));
}
#[test]
fn returns_empty_when_budget_lands_inside_leading_multibyte() {
let s = "🦀tail";
assert_eq!(utf8_prefix_at_or_before(s, 2), "");
}
#[test]
fn handles_empty_string() {
assert_eq!(utf8_prefix_at_or_before("", 10), "");
assert_eq!(utf8_prefix_at_or_before("", 0), "");
}
#[test]
fn handles_zero_budget() {
assert_eq!(utf8_prefix_at_or_before("abc", 0), "");
}
}