use crate::ast::{Node, NodeKind};
use crate::position::{Position, Range};
use crate::workspace::workspace_index::{WorkspaceIndex, SymbolReference};
use lsp_types::*;
use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone)]
pub struct WorkspaceSymbolProvider {
workspace_index: WorkspaceIndex,
config: WorkspaceSymbolConfig,
symbol_cache: HashMap<String, Vec<SymbolInformation>>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceSymbolConfig {
pub enable_fuzzy_matching: bool,
pub max_results: usize,
pub include_test_symbols: bool,
pub include_private_symbols: bool,
pub min_query_length: usize,
}
impl Default for WorkspaceSymbolConfig {
fn default() -> Self {
Self {
enable_fuzzy_matching: true,
max_results: 100,
include_test_symbols: true,
include_private_symbols: false,
min_query_length: 2,
}
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceSymbol {
pub symbol: SymbolInformation,
pub file_path: String,
pub line_number: usize,
pub is_public: bool,
pub category: SymbolCategory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolCategory {
Function,
Variable,
Package,
Constant,
Type,
Method,
Import,
Pragma,
}
impl WorkspaceSymbolProvider {
pub fn new() -> Self {
Self {
workspace_index: WorkspaceIndex::new(),
config: WorkspaceSymbolConfig::default(),
symbol_cache: HashMap::new(),
}
}
pub fn with_config(config: WorkspaceSymbolConfig) -> Self {
Self {
workspace_index: WorkspaceIndex::new(),
config,
symbol_cache: HashMap::new(),
}
}
pub fn with_index(workspace_index: WorkspaceIndex) -> Self {
Self {
workspace_index,
config: WorkspaceSymbolConfig::default(),
symbol_cache: HashMap::new(),
}
}
pub fn workspace_symbols(&self, params: WorkspaceSymbolParams) -> Option<Vec<SymbolInformation>> {
let query = params.query.trim();
if query.len() < self.config.min_query_length {
return Some(Vec::new());
}
if let Some(cached) = self.symbol_cache.get(query) {
return Some(cached.clone());
}
let mut symbols = Vec::new();
let all_symbols = self.workspace_index.get_all_symbols();
for symbol in all_symbols {
if self.matches_query(&symbol.name, query) {
if let Some(symbol_info) = self.convert_to_symbol_information(&symbol) {
symbols.push(symbol_info);
}
}
}
symbols.sort_by(|a, b| {
let a_score = self.calculate_relevance_score(&a.name, query);
let b_score = self.calculate_relevance_score(&b.name, query);
b_score.cmp(&a_score)
});
symbols.truncate(self.config.max_results);
self.symbol_cache.insert(query.to_string(), symbols.clone());
Some(symbols)
}
fn matches_query(&self, symbol_name: &str, query: &str) -> bool {
if !self.config.include_private_symbols && symbol_name.starts_with('_') {
return false;
}
if self.config.enable_fuzzy_matching {
self.fuzzy_match(symbol_name, query)
} else {
symbol_name.to_lowercase().contains(&query.to_lowercase())
}
}
fn fuzzy_match(&self, symbol_name: &str, query: &str) -> bool {
let symbol_lower = symbol_name.to_lowercase();
let query_lower = query.to_lowercase();
let mut query_chars = query_lower.chars().peekable();
let mut symbol_chars = symbol_lower.chars();
while let Some(query_char) = query_chars.next() {
let mut found = false;
while let Some(symbol_char) = symbol_chars.next() {
if symbol_char == query_char {
found = true;
break;
}
}
if !found {
return false;
}
}
true
}
fn calculate_relevance_score(&self, symbol_name: &str, query: &str) -> u32 {
let symbol_lower = symbol_name.to_lowercase();
let query_lower = query.to_lowercase();
let mut score = 0u32;
if symbol_lower == query_lower {
score += 1000;
}
else if symbol_lower.starts_with(&query_lower) {
score += 500;
}
else if symbol_lower.contains(&query_lower) {
score += 250;
}
else if self.fuzzy_match(symbol_name, query) {
score += 100;
}
score += (20 - symbol_name.len().min(20)) as u32 * 5;
score
}
fn convert_to_symbol_information(&self, symbol: &crate::workspace::workspace_index::Symbol) -> Option<SymbolInformation> {
let kind = self.determine_symbol_kind(symbol);
let location = Location {
uri: symbol.uri.clone(),
range: symbol.range,
};
Some(SymbolInformation {
name: symbol.name.clone(),
kind,
tags: None,
location,
container_name: symbol.container_name.clone(),
})
}
fn determine_symbol_kind(&self, symbol: &crate::workspace::workspace_index::Symbol) -> SymbolKind {
match symbol.category {
SymbolCategory::Function => SymbolKind::FUNCTION,
SymbolCategory::Variable => SymbolKind::VARIABLE,
SymbolCategory::Package => SymbolKind::MODULE,
SymbolCategory::Constant => SymbolKind::CONSTANT,
SymbolCategory::Type => SymbolKind::CLASS,
SymbolCategory::Method => SymbolKind::METHOD,
SymbolCategory::Import => SymbolKind::NAMESPACE,
SymbolCategory::Pragma => SymbolKind::INTERFACE,
}
}
pub fn update_workspace_index(&mut self, workspace_index: WorkspaceIndex) {
self.workspace_index = workspace_index;
self.symbol_cache.clear();
}
pub fn clear_cache(&mut self) {
self.symbol_cache.clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
let query_count = self.symbol_cache.len();
let total_symbols: usize = self.symbol_cache.values()
.map(|symbols| symbols.len())
.sum();
(query_count, total_symbols)
}
}
impl Default for WorkspaceSymbolProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workspace_symbol_provider_creation() {
let provider = WorkspaceSymbolProvider::new();
assert!(provider.config.enable_fuzzy_matching);
assert_eq!(provider.config.max_results, 100);
assert!(provider.config.include_test_symbols);
assert!(!provider.config.include_private_symbols);
assert_eq!(provider.config.min_query_length, 2);
}
#[test]
fn test_custom_config() {
let config = WorkspaceSymbolConfig {
enable_fuzzy_matching: false,
max_results: 50,
include_test_symbols: false,
include_private_symbols: true,
min_query_length: 3,
};
let provider = WorkspaceSymbolProvider::with_config(config);
assert!(!provider.config.enable_fuzzy_matching);
assert_eq!(provider.config.max_results, 50);
assert!(!provider.config.include_test_symbols);
assert!(provider.config.include_private_symbols);
assert_eq!(provider.config.min_query_length, 3);
}
#[test]
fn test_fuzzy_matching() {
let provider = WorkspaceSymbolProvider::new();
assert!(provider.fuzzy_match("process_data", "process_data"));
assert!(provider.fuzzy_match("process_data", "process"));
assert!(provider.fuzzy_match("process_data", "data"));
assert!(provider.fuzzy_match("process_data", "pd"));
assert!(!provider.fuzzy_match("process_data", "xyz"));
}
#[test]
fn test_relevance_scoring() {
let provider = WorkspaceSymbolProvider::new();
let exact_score = provider.calculate_relevance_score("process_data", "process_data");
let prefix_score = provider.calculate_relevance_score("process_data", "process");
let contains_score = provider.calculate_relevance_score("process_data", "data");
assert!(exact_score > prefix_score);
assert!(prefix_score > contains_score);
}
#[test]
fn test_cache_operations() {
let mut provider = WorkspaceSymbolProvider::new();
let (queries, symbols) = provider.cache_stats();
assert_eq!(queries, 0);
assert_eq!(symbols, 0);
provider.clear_cache();
let (queries, symbols) = provider.cache_stats();
assert_eq!(queries, 0);
assert_eq!(symbols, 0);
}
#[test]
fn test_workspace_symbols_query() {
let provider = WorkspaceSymbolProvider::new();
let params = WorkspaceSymbolParams {
query: "test".to_string(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let symbols = provider.workspace_symbols(params);
assert!(symbols.is_some());
}
#[test]
fn test_min_query_length() {
let provider = WorkspaceSymbolProvider::new();
let params = WorkspaceSymbolParams {
query: "x".to_string(), work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let symbols = provider.workspace_symbols(params);
use perl_tdd_support::must_some;
assert!(must_some(symbols).is_empty());
}
#[test]
fn test_workspace_index_update() {
let mut provider = WorkspaceSymbolProvider::new();
let new_index = WorkspaceIndex::new();
provider.update_workspace_index(new_index);
let (queries, symbols) = provider.cache_stats();
assert_eq!(queries, 0);
assert_eq!(symbols, 0);
}
}