use crate::clangd::session::ClangdSessionTrait;
use lsp_types::{DocumentSymbol, DocumentSymbolResponse, Position, Range};
use std::collections::VecDeque;
use std::path::Path;
use sublime_fuzzy::best_match;
use tracing::{trace, warn};
use crate::lsp::traits::LspClientTrait;
use crate::mcp_server::tools::analyze_symbols::AnalyzerError;
use crate::project::component_session::ComponentSession;
use crate::symbol::uri_from_pathbuf;
const FUZZY_MATCH_THRESHOLD: isize = 0;
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub document_symbol: lsp_types::DocumentSymbol,
#[allow(dead_code)]
pub container_path: Vec<String>,
}
#[derive(Debug, Clone)]
struct ScoredSymbol<'a> {
symbol: &'a DocumentSymbol,
score: isize,
}
pub trait PositionContains {
fn contains(&self, position: &Position) -> bool;
}
impl PositionContains for Range {
fn contains(&self, position: &Position) -> bool {
(position.line > self.start.line
|| (position.line == self.start.line && position.character >= self.start.character))
&& (position.line < self.end.line
|| (position.line == self.end.line && position.character <= self.end.character))
}
}
impl PositionContains for DocumentSymbol {
fn contains(&self, position: &Position) -> bool {
self.selection_range.contains(position)
}
}
#[allow(dead_code)]
pub trait SymbolMatcher {
fn matches(&self, symbol: &DocumentSymbol) -> bool;
}
#[allow(dead_code)]
pub struct MemberMatcher {
pub class_name: String,
pub member_kinds: Option<Vec<lsp_types::SymbolKind>>,
pub static_only: bool,
}
#[allow(dead_code)]
impl MemberMatcher {
pub fn for_class(class_name: &str) -> Self {
Self {
class_name: class_name.to_string(),
member_kinds: None,
static_only: false,
}
}
pub fn with_kinds(mut self, kinds: Vec<lsp_types::SymbolKind>) -> Self {
self.member_kinds = Some(kinds);
self
}
pub fn static_only(mut self) -> Self {
self.static_only = true;
self
}
fn is_callable_member(symbol: &DocumentSymbol) -> bool {
matches!(
symbol.kind,
lsp_types::SymbolKind::METHOD
| lsp_types::SymbolKind::FUNCTION
| lsp_types::SymbolKind::CONSTRUCTOR
)
}
fn is_static_method(symbol: &DocumentSymbol) -> bool {
if let Some(detail) = &symbol.detail {
detail.contains("static") || detail.starts_with("static ")
} else {
false
}
}
}
impl SymbolMatcher for MemberMatcher {
fn matches(&self, symbol: &DocumentSymbol) -> bool {
if !Self::is_callable_member(symbol) {
return false;
}
if let Some(ref kinds) = self.member_kinds
&& !kinds.contains(&symbol.kind)
{
return false;
}
if self.static_only && !Self::is_static_method(symbol) {
return false;
}
true
}
}
pub struct DocumentSymbolIterator<'a> {
stack: VecDeque<(&'a DocumentSymbol, Vec<&'a str>)>,
}
impl<'a> DocumentSymbolIterator<'a> {
pub fn new(symbols: &'a [DocumentSymbol]) -> Self {
let mut stack = VecDeque::new();
for symbol in symbols {
stack.push_back((symbol, vec![]));
}
Self { stack }
}
#[allow(dead_code)]
pub fn depth_first(symbols: &'a [DocumentSymbol]) -> Self {
Self::new(symbols)
}
#[allow(dead_code)]
pub fn breadth_first(symbols: &'a [DocumentSymbol]) -> Self {
let mut stack = VecDeque::new();
for symbol in symbols {
stack.push_front((symbol, vec![]));
}
Self { stack }
}
}
impl<'a> Iterator for DocumentSymbolIterator<'a> {
type Item = (&'a DocumentSymbol, Vec<&'a str>);
fn next(&mut self) -> Option<Self::Item> {
if let Some((symbol, path)) = self.stack.pop_front() {
if let Some(children) = &symbol.children {
let mut new_path = path.clone();
new_path.push(&symbol.name);
for child in children.iter().rev() {
self.stack.push_front((child, new_path.clone()));
}
}
Some((symbol, path))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolSearchBuilder {
position: Option<Position>,
name: Option<String>,
kind: Option<lsp_types::SymbolKind>,
kinds: Option<Vec<lsp_types::SymbolKind>>,
path_contains: Option<String>,
}
impl SymbolSearchBuilder {
pub fn new() -> Self {
Self {
position: None,
name: None,
kind: None,
kinds: None,
path_contains: None,
}
}
pub fn at_position(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
pub fn with_kind(mut self, kind: lsp_types::SymbolKind) -> Self {
self.kind = Some(kind);
self
}
pub fn with_kinds(mut self, kinds: &[lsp_types::SymbolKind]) -> Self {
self.kinds = Some(kinds.to_vec());
self
}
#[allow(dead_code)]
pub fn path_contains<S: Into<String>>(mut self, path_part: S) -> Self {
self.path_contains = Some(path_part.into());
self
}
pub fn find_first(self, symbols: &[DocumentSymbol]) -> Option<&DocumentSymbol> {
let results = self.find_all(symbols);
results.first().copied()
}
pub fn find_all(self, symbols: &[DocumentSymbol]) -> Vec<&DocumentSymbol> {
let mut scored_matches: Vec<ScoredSymbol> = DocumentSymbolIterator::new(symbols)
.filter_map(|(symbol, path)| {
if !self.matches_symbol(symbol, &path) {
return None;
}
self.fuzzy_match_score(symbol)
.map(|score| ScoredSymbol { symbol, score })
})
.collect();
scored_matches.sort_by(|a, b| b.score.cmp(&a.score));
scored_matches
.into_iter()
.map(|scored| scored.symbol)
.collect()
}
#[allow(dead_code)]
pub fn class_members_only(mut self) -> Self {
self.kind = None; self
}
#[allow(dead_code)]
pub fn static_methods_only(mut self) -> Self {
self.kind = Some(lsp_types::SymbolKind::METHOD);
self
}
#[allow(dead_code)]
pub fn constructors_only(mut self) -> Self {
self.kind = Some(lsp_types::SymbolKind::CONSTRUCTOR);
self
}
#[allow(dead_code)]
pub fn methods_in_class<S: Into<String>>(mut self, class_name: S) -> Self {
let class_name = class_name.into();
self.path_contains = Some(class_name.clone());
self.kind = Some(lsp_types::SymbolKind::METHOD);
self
}
pub async fn search_multiple_files(
self,
component_session: &ComponentSession,
files: &[String],
max_results: Option<u32>,
) -> Result<Vec<(String, Vec<DocumentSymbol>)>, AnalyzerError> {
let mut file_results = Vec::new();
for file_path in files {
match self.search_single_file(component_session, file_path).await {
Ok(symbols) => {
file_results.push((file_path.clone(), symbols));
}
Err(e) => {
trace!("Failed to search file {}: {}", file_path, e);
file_results.push((file_path.clone(), Vec::new()));
}
}
}
if let Some(max) = max_results {
let mut all_symbols = Vec::new();
for (file_path, symbols) in &file_results {
for symbol in symbols {
all_symbols.push((file_path.clone(), symbol.clone()));
if all_symbols.len() >= max as usize {
break;
}
}
if all_symbols.len() >= max as usize {
break;
}
}
let mut limited_results = Vec::new();
let mut current_file = String::new();
let mut current_symbols = Vec::new();
for (file_path, symbol) in all_symbols {
if file_path != current_file {
if !current_file.is_empty() {
limited_results.push((current_file.clone(), current_symbols));
current_symbols = Vec::new();
}
current_file = file_path;
}
current_symbols.push(symbol);
}
if !current_file.is_empty() {
limited_results.push((current_file, current_symbols));
}
Ok(limited_results)
} else {
Ok(file_results)
}
}
async fn search_single_file(
&self,
component_session: &ComponentSession,
file_path: &str,
) -> Result<Vec<DocumentSymbol>, AnalyzerError> {
trace!(
"Searching single file: {} with criteria: {:?}",
file_path, self
);
let file_uri = if file_path.starts_with("file://") {
file_path
.parse()
.map_err(|e| AnalyzerError::NoData(format!("Invalid URI: {}", e)))?
} else {
uri_from_pathbuf(Path::new(file_path))
};
trace!("Getting document symbols for URI: {:?}", file_uri);
let document_symbols = get_document_symbols(component_session, file_uri).await?;
trace!("Found {} document symbols", document_symbols.len());
let filtered_symbols: Vec<DocumentSymbol> = self
.clone()
.find_all(&document_symbols)
.into_iter()
.cloned()
.collect();
trace!(
"After filtering: {} symbols match criteria",
filtered_symbols.len()
);
Ok(filtered_symbols)
}
fn fuzzy_match_score(&self, symbol: &DocumentSymbol) -> Option<isize> {
if let Some(ref name) = self.name {
if let Some(fuzzy_match) = best_match(name, &symbol.name)
&& fuzzy_match.score() >= FUZZY_MATCH_THRESHOLD
{
return Some(fuzzy_match.score());
}
None
} else {
Some(0)
}
}
fn matches_symbol(&self, symbol: &DocumentSymbol, path: &[&str]) -> bool {
trace!(
"Checking symbol '{}' (kind: {:?}) against criteria: {:?}",
symbol.name, symbol.kind, self
);
if let Some(ref position) = self.position
&& !symbol.contains(position)
{
trace!("Symbol '{}' rejected: position mismatch", symbol.name);
return false;
}
if let Some(ref name) = self.name {
if let Some(fuzzy_match) = best_match(name, &symbol.name) {
if fuzzy_match.score() < FUZZY_MATCH_THRESHOLD {
trace!(
"Symbol '{}' rejected: fuzzy match score {} below threshold {}",
symbol.name,
fuzzy_match.score(),
FUZZY_MATCH_THRESHOLD
);
return false;
}
trace!(
"Symbol '{}' accepted: fuzzy match score {}",
symbol.name,
fuzzy_match.score()
);
} else {
trace!(
"Symbol '{}' rejected: no fuzzy match found for query '{}'",
symbol.name, name
);
return false;
}
}
if let Some(kind) = self.kind {
if symbol.kind != kind {
trace!(
"Symbol '{}' rejected: kind {:?} != {:?}",
symbol.name, symbol.kind, kind
);
return false;
}
} else if let Some(ref kinds) = self.kinds
&& !kinds.contains(&symbol.kind)
{
trace!(
"Symbol '{}' rejected: kind {:?} not in {:?}",
symbol.name, symbol.kind, kinds
);
return false;
}
if let Some(ref path_contains) = self.path_contains {
let full_path = path.join("::");
if !full_path.contains(path_contains) {
trace!(
"Symbol '{}' rejected: path '{}' does not contain '{}'",
symbol.name, full_path, path_contains
);
return false;
}
}
if self.kind.is_none() && self.path_contains.is_some() {
if !matches!(
symbol.kind,
lsp_types::SymbolKind::METHOD
| lsp_types::SymbolKind::FUNCTION
| lsp_types::SymbolKind::CONSTRUCTOR
) {
trace!(
"Symbol '{}' rejected: class_members_only filter - not a callable member",
symbol.name
);
return false;
}
}
trace!("Symbol '{}' matches criteria", symbol.name);
true
}
}
impl Default for SymbolSearchBuilder {
fn default() -> Self {
Self::new()
}
}
pub async fn get_document_symbols(
component_session: &ComponentSession,
file_uri: lsp_types::Uri,
) -> Result<Vec<DocumentSymbol>, AnalyzerError> {
trace!("Requesting document symbols for URI: {:?}", file_uri);
let uri_str = file_uri.to_string();
let file_path_str = uri_str.strip_prefix("file://").unwrap_or(&uri_str);
component_session
.ensure_file_ready(Path::new(file_path_str))
.await?;
let mut session = component_session.lsp_session().await;
let client = session.client_mut();
let document_symbols = client
.text_document_document_symbol(file_uri.clone())
.await
.map_err(AnalyzerError::from)?;
trace!(
"Document symbols response type: {:?}",
match &document_symbols {
DocumentSymbolResponse::Nested(_) => "Nested (hierarchical)",
DocumentSymbolResponse::Flat(_) => "Flat (legacy)",
}
);
match document_symbols {
DocumentSymbolResponse::Nested(nested_symbols) => {
trace!(
"Successfully received {} top-level hierarchical symbols",
nested_symbols.len()
);
Ok(nested_symbols)
}
DocumentSymbolResponse::Flat(flat_symbols) => {
warn!(
"Received flat document symbols response for '{:?}' despite hierarchical support enabled. \
This is unexpected and may indicate a clangd configuration issue. \
Flat response contains {} symbols.",
file_uri,
flat_symbols.len()
);
Err(AnalyzerError::NoData(format!(
"Unexpected flat document symbols response for '{:?}' despite hierarchical client capability. Expected nested DocumentSymbol format.",
file_uri
)))
}
}
}
#[allow(dead_code)]
pub fn find_symbol_at_position<'a>(
symbols: &'a [DocumentSymbol],
position: &Position,
) -> Option<&'a DocumentSymbol> {
SymbolSearchBuilder::new()
.at_position(*position)
.find_first(symbols)
}
pub fn find_symbol_at_position_with_path<'a>(
symbols: &'a [DocumentSymbol],
position: &Position,
) -> Option<(&'a DocumentSymbol, Vec<String>)> {
find_symbol_recursive_with_path(symbols, position, Vec::new())
}
fn find_symbol_recursive_with_path<'a>(
symbols: &'a [DocumentSymbol],
position: &Position,
current_path: Vec<String>,
) -> Option<(&'a DocumentSymbol, Vec<String>)> {
for symbol in symbols {
if symbol.range.contains(position) {
if let Some(children) = &symbol.children {
let mut child_path = current_path.clone();
child_path.push(symbol.name.clone());
if let Some(result) =
find_symbol_recursive_with_path(children, position, child_path)
{
return Some(result);
}
}
return Some((symbol, current_path));
}
}
None
}
#[allow(dead_code)]
pub fn find_symbols_by_name<'a>(
symbols: &'a [DocumentSymbol],
name: &str,
) -> Vec<&'a DocumentSymbol> {
SymbolSearchBuilder::new().with_name(name).find_all(symbols)
}
#[allow(dead_code)]
pub fn find_symbols_by_kind(
symbols: &[DocumentSymbol],
kind: lsp_types::SymbolKind,
) -> Vec<&DocumentSymbol> {
SymbolSearchBuilder::new().with_kind(kind).find_all(symbols)
}
#[allow(dead_code)]
pub fn count_total_symbols(symbols: &[DocumentSymbol]) -> usize {
DocumentSymbolIterator::new(symbols).count()
}
#[allow(dead_code)]
pub fn count_symbols_by_kind(symbols: &[DocumentSymbol], kind: lsp_types::SymbolKind) -> usize {
DocumentSymbolIterator::new(symbols)
.filter(|(symbol, _)| symbol.kind == kind)
.count()
}
#[allow(dead_code)]
pub fn get_symbol_paths(symbols: &[DocumentSymbol]) -> Vec<(String, String)> {
DocumentSymbolIterator::new(symbols)
.map(|(symbol, path)| (symbol.name.clone(), path.join("::")))
.collect()
}
#[allow(dead_code)]
pub fn extract_class_members<'a>(
symbols: &'a [DocumentSymbol],
class_name: &str,
) -> Vec<&'a DocumentSymbol> {
if let Some(target_class) = SymbolSearchBuilder::new()
.with_name(class_name)
.with_kind(lsp_types::SymbolKind::CLASS)
.find_first(symbols)
{
if let Some(children) = &target_class.children {
return SymbolSearchBuilder::new()
.class_members_only()
.find_all(children);
}
}
if let Some(target_struct) = SymbolSearchBuilder::new()
.with_name(class_name)
.with_kind(lsp_types::SymbolKind::STRUCT)
.find_first(symbols)
&& let Some(children) = &target_struct.children
{
return SymbolSearchBuilder::new()
.class_members_only()
.find_all(children);
}
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{Position, Range, SymbolKind};
fn create_test_symbol(
name: &str,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
) -> DocumentSymbol {
DocumentSymbol {
name: name.to_string(),
detail: None,
kind: SymbolKind::CLASS,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: Range {
start: Position {
line: start_line,
character: start_char,
},
end: Position {
line: end_line,
character: end_char,
},
},
selection_range: Range {
start: Position {
line: start_line,
character: start_char,
},
end: Position {
line: start_line,
character: start_char + name.len() as u32,
},
},
children: None,
}
}
fn create_test_symbol_with_kind(
name: &str,
kind: SymbolKind,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
) -> DocumentSymbol {
let mut symbol = create_test_symbol(name, start_line, start_char, end_line, end_char);
symbol.kind = kind;
symbol
}
#[test]
fn test_position_contains_trait() {
let range = Range {
start: Position {
line: 10,
character: 0,
},
end: Position {
line: 10,
character: 10,
},
};
assert!(range.contains(&Position {
line: 10,
character: 5
}));
assert!(range.contains(&Position {
line: 10,
character: 5
}));
assert!(!range.contains(&Position {
line: 11,
character: 5
}));
assert!(!range.contains(&Position {
line: 10,
character: 15
}));
}
#[test]
fn test_document_symbol_iterator() {
let mut parent = create_test_symbol("Parent", 0, 0, 10, 0);
let child1 = create_test_symbol("Child1", 2, 4, 4, 0);
let child2 = create_test_symbol("Child2", 6, 4, 8, 0);
parent.children = Some(vec![child1, child2]);
let symbols = vec![parent];
let collected: Vec<_> = DocumentSymbolIterator::new(&symbols)
.map(|(symbol, path)| (symbol.name.clone(), path.join("::")))
.collect();
assert_eq!(collected.len(), 3);
assert_eq!(collected[0], ("Parent".to_string(), "".to_string()));
assert_eq!(collected[1], ("Child1".to_string(), "Parent".to_string()));
assert_eq!(collected[2], ("Child2".to_string(), "Parent".to_string()));
}
#[test]
fn test_symbol_search_builder_position() {
let symbol = create_test_symbol("TestClass", 10, 0, 20, 0);
let symbols = vec![symbol];
let found = SymbolSearchBuilder::new()
.at_position(Position {
line: 10,
character: 5,
})
.find_first(&symbols);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "TestClass");
let not_found = SymbolSearchBuilder::new()
.at_position(Position {
line: 25,
character: 0,
})
.find_first(&symbols);
assert!(not_found.is_none());
}
#[test]
fn test_symbol_search_builder_name() {
let symbol1 = create_test_symbol("ClassA", 0, 0, 5, 0);
let symbol2 = create_test_symbol("ClassB", 6, 0, 10, 0);
let symbols = vec![symbol1, symbol2];
let found = SymbolSearchBuilder::new()
.with_name("ClassB")
.find_first(&symbols);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "ClassB");
}
#[test]
fn test_symbol_search_builder_kind() {
let class_symbol = create_test_symbol_with_kind("MyClass", SymbolKind::CLASS, 0, 0, 5, 0);
let function_symbol =
create_test_symbol_with_kind("myFunction", SymbolKind::FUNCTION, 6, 0, 8, 0);
let symbols = vec![class_symbol, function_symbol];
let functions = SymbolSearchBuilder::new()
.with_kind(SymbolKind::FUNCTION)
.find_all(&symbols);
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "myFunction");
}
#[test]
fn test_symbol_search_builder_combined() {
let mut parent = create_test_symbol_with_kind("Parent", SymbolKind::CLASS, 0, 0, 10, 0);
let child = create_test_symbol_with_kind("method", SymbolKind::METHOD, 2, 4, 4, 0);
parent.children = Some(vec![child]);
let symbols = vec![parent];
let found = SymbolSearchBuilder::new()
.with_name("method")
.with_kind(SymbolKind::METHOD)
.find_first(&symbols);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "method");
let found_with_path = SymbolSearchBuilder::new()
.with_name("method")
.path_contains("Parent")
.find_first(&symbols);
assert!(found_with_path.is_some());
assert_eq!(found_with_path.unwrap().name, "method");
}
#[test]
fn test_find_symbol_at_position() {
let symbol = create_test_symbol("TestClass", 10, 0, 20, 0);
let symbols = vec![symbol];
let found = find_symbol_at_position(
&symbols,
&Position {
line: 10,
character: 5,
},
);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "TestClass");
let not_found = find_symbol_at_position(
&symbols,
&Position {
line: 25,
character: 0,
},
);
assert!(not_found.is_none());
}
#[test]
fn test_find_symbols_by_name() {
let symbol1 = create_test_symbol("Test", 0, 0, 5, 0);
let symbol2 = create_test_symbol("Other", 6, 0, 10, 0);
let symbol3 = create_test_symbol("Test", 11, 0, 15, 0);
let symbols = vec![symbol1, symbol2, symbol3];
let found = find_symbols_by_name(&symbols, "Test");
assert_eq!(found.len(), 2);
assert!(found.iter().all(|s| s.name == "Test"));
}
#[test]
fn test_find_symbols_by_kind() {
let class_symbol = create_test_symbol_with_kind("MyClass", SymbolKind::CLASS, 0, 0, 5, 0);
let function_symbol =
create_test_symbol_with_kind("myFunction", SymbolKind::FUNCTION, 6, 0, 8, 0);
let symbols = vec![class_symbol, function_symbol];
let classes = find_symbols_by_kind(&symbols, SymbolKind::CLASS);
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "MyClass");
}
#[test]
fn test_count_total_symbols() {
let mut parent = create_test_symbol("Parent", 0, 0, 10, 0);
let child1 = create_test_symbol("Child1", 2, 4, 4, 0);
let child2 = create_test_symbol("Child2", 6, 4, 8, 0);
parent.children = Some(vec![child1, child2]);
let symbols = vec![parent];
assert_eq!(count_total_symbols(&symbols), 3);
}
#[test]
fn test_count_symbols_by_kind() {
let class1 = create_test_symbol_with_kind("Class1", SymbolKind::CLASS, 0, 0, 5, 0);
let class2 = create_test_symbol_with_kind("Class2", SymbolKind::CLASS, 6, 0, 10, 0);
let function = create_test_symbol_with_kind("func", SymbolKind::FUNCTION, 11, 0, 13, 0);
let symbols = vec![class1, class2, function];
assert_eq!(count_symbols_by_kind(&symbols, SymbolKind::CLASS), 2);
assert_eq!(count_symbols_by_kind(&symbols, SymbolKind::FUNCTION), 1);
assert_eq!(count_symbols_by_kind(&symbols, SymbolKind::VARIABLE), 0);
}
#[test]
fn test_get_symbol_paths() {
let mut parent = create_test_symbol("Namespace", 0, 0, 10, 0);
let mut child_class = create_test_symbol("MyClass", 2, 0, 8, 0);
let child_method = create_test_symbol("method", 4, 4, 6, 0);
child_class.children = Some(vec![child_method]);
parent.children = Some(vec![child_class]);
let symbols = vec![parent];
let paths = get_symbol_paths(&symbols);
assert_eq!(paths.len(), 3);
assert!(paths.contains(&("Namespace".to_string(), "".to_string())));
assert!(paths.contains(&("MyClass".to_string(), "Namespace".to_string())));
assert!(paths.contains(&("method".to_string(), "Namespace::MyClass".to_string())));
}
#[test]
fn test_fuzzy_matching_basic() {
let factorial_symbol = create_test_symbol("factorial", 0, 0, 5, 0);
let calculate_symbol = create_test_symbol("calculate", 6, 0, 10, 0);
let math_symbol = create_test_symbol("Math", 11, 0, 15, 0);
let symbols = vec![factorial_symbol, calculate_symbol, math_symbol];
let found = SymbolSearchBuilder::new()
.with_name("factorial")
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "factorial");
let found = SymbolSearchBuilder::new()
.with_name("factrl")
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "factorial");
let found = SymbolSearchBuilder::new()
.with_name("calc")
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "calculate");
}
#[test]
fn test_fuzzy_matching_case_insensitive() {
let string_utils_symbol = create_test_symbol("StringUtils", 0, 0, 5, 0);
let symbols = vec![string_utils_symbol];
let found = SymbolSearchBuilder::new()
.with_name("strutil")
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "StringUtils");
let found = SymbolSearchBuilder::new()
.with_name("STRINGUTILS")
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "StringUtils");
}
#[test]
fn test_fuzzy_matching_scoring_order() {
let math_symbol = create_test_symbol("Math", 0, 0, 5, 0);
let matrix_symbol = create_test_symbol("Matrix", 6, 0, 10, 0);
let algorithm_symbol = create_test_symbol("Algorithm", 11, 0, 15, 0);
let symbols = vec![math_symbol, matrix_symbol, algorithm_symbol];
let found = SymbolSearchBuilder::new()
.with_name("Mat")
.find_all(&symbols);
assert!(!found.is_empty());
assert_eq!(found[0].name, "Math");
}
#[test]
fn test_fuzzy_matching_no_matches() {
let factorial_symbol = create_test_symbol("factorial", 0, 0, 5, 0);
let symbols = vec![factorial_symbol];
let found = SymbolSearchBuilder::new()
.with_name("xyz")
.find_all(&symbols);
assert_eq!(found.len(), 0);
}
#[test]
fn test_fuzzy_matching_with_kind_filter() {
let class_symbol = create_test_symbol_with_kind("Math", SymbolKind::CLASS, 0, 0, 5, 0);
let function_symbol =
create_test_symbol_with_kind("mathFunction", SymbolKind::FUNCTION, 6, 0, 8, 0);
let symbols = vec![class_symbol, function_symbol];
let found = SymbolSearchBuilder::new()
.with_name("mat")
.with_kind(SymbolKind::CLASS)
.find_all(&symbols);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "Math");
assert_eq!(found[0].kind, SymbolKind::CLASS);
}
#[test]
fn test_fuzzy_matching_multiple_results_sorted() {
let math_class = create_test_symbol("Math", 0, 0, 5, 0);
let mathematics = create_test_symbol("Mathematics", 6, 0, 10, 0);
let matrix = create_test_symbol("Matrix", 11, 0, 15, 0);
let symbols = vec![math_class, mathematics, matrix];
let found = SymbolSearchBuilder::new()
.with_name("Math")
.find_all(&symbols);
assert!(found.len() >= 2); assert_eq!(found[0].name, "Math");
}
}