use std::collections::HashSet;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::types::{ClassLikeKind, DefineInfo, FunctionInfo};
use crate::util::offset_to_position;
const MAX_RESULTS: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum MatchTier {
Exact = 0,
Prefix = 1,
Substring = 2,
}
struct RankedSymbol {
symbol: SymbolInformation,
tier: MatchTier,
}
fn match_tier(name: &str, query_lower: &str) -> Option<MatchTier> {
if query_lower.is_empty() {
return Some(MatchTier::Substring);
}
let name_lower = name.to_lowercase();
if name_lower == query_lower {
Some(MatchTier::Exact)
} else if name_lower.starts_with(query_lower) {
Some(MatchTier::Prefix)
} else if name_lower.contains(query_lower) {
Some(MatchTier::Substring)
} else {
None
}
}
fn short_name(full_name: &str) -> &str {
if let Some(idx) = full_name.rfind("::") {
return &full_name[idx + 2..];
}
if let Some(idx) = full_name.rfind('\\') {
return &full_name[idx + 1..];
}
full_name
}
impl Backend {
#[allow(deprecated)] pub fn handle_workspace_symbol(&self, query: &str) -> Option<Vec<SymbolInformation>> {
let query_lower = query.to_lowercase();
let mut ranked: Vec<RankedSymbol> = Vec::new();
let mut seen_fqns: HashSet<String> = HashSet::new();
{
let ast_map = self.ast_map.read();
for (file_uri, classes) in ast_map.iter() {
for class in classes {
if class.name.is_empty() || class.name.starts_with("anonymous@") {
continue;
}
let fqn = class.fqn();
let content = match self.get_file_content_arc(file_uri) {
Some(c) => c,
None => continue,
};
let class_tier = match_tier(&fqn, &query_lower)
.or_else(|| match_tier(&class.name, &query_lower));
if let Some(tier) = class_tier
&& class.keyword_offset != 0
{
let pos = offset_to_position(&content, class.keyword_offset as usize);
let kind = match class.kind {
ClassLikeKind::Class => SymbolKind::CLASS,
ClassLikeKind::Interface => SymbolKind::INTERFACE,
ClassLikeKind::Trait => SymbolKind::CLASS,
ClassLikeKind::Enum => SymbolKind::ENUM,
};
let tags = class
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
seen_fqns.insert(fqn.clone());
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: fqn.clone(),
kind,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: class.file_namespace.clone(),
},
tier,
});
}
for method in &class.methods {
if method.is_virtual {
continue;
}
if method.name_offset == 0 {
continue;
}
let tier = match match_tier(&method.name, &query_lower) {
Some(t) => t,
None => continue,
};
let pos = offset_to_position(&content, method.name_offset as usize);
let tags = method
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: format!("{}::{}", fqn, method.name),
kind: SymbolKind::METHOD,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: Some(fqn.clone()),
},
tier,
});
}
for prop in &class.properties {
if prop.is_virtual {
continue;
}
if prop.name_offset == 0 {
continue;
}
let match_name = format!("${}", prop.name);
let tier = match_tier(&prop.name, &query_lower)
.or_else(|| match_tier(&match_name, &query_lower));
let tier = match tier {
Some(t) => t,
None => continue,
};
let pos = offset_to_position(&content, prop.name_offset as usize);
let tags = prop
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: format!("{}::${}", fqn, prop.name),
kind: SymbolKind::PROPERTY,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: Some(fqn.clone()),
},
tier,
});
}
for constant in &class.constants {
if constant.is_virtual {
continue;
}
if constant.name_offset == 0 {
continue;
}
let tier = match match_tier(&constant.name, &query_lower) {
Some(t) => t,
None => continue,
};
let pos = offset_to_position(&content, constant.name_offset as usize);
let tags = constant
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
let kind = if constant.is_enum_case {
SymbolKind::ENUM_MEMBER
} else {
SymbolKind::CONSTANT
};
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: format!("{}::{}", fqn, constant.name),
kind,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: Some(fqn.clone()),
},
tier,
});
}
}
}
}
{
let fmap = self.global_functions.read();
for (_name, (file_uri, func)) in fmap.iter() {
let display_name = function_display_name(func);
let func_short = short_name(&display_name);
let tier = match match_tier(&display_name, &query_lower)
.or_else(|| match_tier(func_short, &query_lower))
{
Some(t) => t,
None => continue,
};
if func.name_offset == 0 {
continue;
}
let content = match self.get_file_content_arc(file_uri) {
Some(c) => c,
None => continue,
};
let pos = offset_to_position(&content, func.name_offset as usize);
let tags = func
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: display_name,
kind: SymbolKind::FUNCTION,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: func.namespace.clone(),
},
tier,
});
}
}
{
let dmap = self.global_defines.read();
for (name, info) in dmap.iter() {
let tier = match match_tier(name, &query_lower) {
Some(t) => t,
None => continue,
};
if info.name_offset == 0 {
continue;
}
let content = match self.get_file_content_arc(&info.file_uri) {
Some(c) => c,
None => continue,
};
let pos = offset_to_position(&content, info.name_offset as usize);
ranked.push(RankedSymbol {
symbol: make_constant_symbol(name, info, pos),
tier,
});
}
}
if !query_lower.is_empty() {
let fqn_idx = self.fqn_index.read();
let idx = self.class_index.read();
for (fqn, file_uri) in idx.iter() {
if seen_fqns.contains(fqn) {
continue;
}
let fqn_short = short_name(fqn);
let tier = match match_tier(fqn, &query_lower)
.or_else(|| match_tier(fqn_short, &query_lower))
{
Some(t) => t,
None => continue,
};
let (kind, tags, container_name) =
if let Some(class_info) = fqn_idx.get(fqn.as_str()) {
let k = match class_info.kind {
ClassLikeKind::Class => SymbolKind::CLASS,
ClassLikeKind::Interface => SymbolKind::INTERFACE,
ClassLikeKind::Trait => SymbolKind::CLASS,
ClassLikeKind::Enum => SymbolKind::ENUM,
};
let t = class_info
.deprecation_message
.as_ref()
.map(|_| vec![SymbolTag::DEPRECATED]);
(k, t, class_info.file_namespace.clone())
} else {
(SymbolKind::CLASS, None, namespace_from_fqn(fqn))
};
let pos = if let Some(class_info) = fqn_idx.get(fqn.as_str()) {
if class_info.keyword_offset > 0 {
if let Some(content) = self.get_file_content_arc(file_uri) {
offset_to_position(&content, class_info.keyword_offset as usize)
} else {
Position::new(0, 0)
}
} else {
Position::new(0, 0)
}
} else {
Position::new(0, 0)
};
seen_fqns.insert(fqn.clone());
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: fqn.clone(),
kind,
tags,
deprecated: None,
location: Location {
uri: Url::parse(file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name,
},
tier,
});
}
}
if !query_lower.is_empty() {
let cmap = self.classmap.read();
for (fqn, file_path) in cmap.iter() {
if seen_fqns.contains(fqn) {
continue;
}
let fqn_short = short_name(fqn);
let tier = match match_tier(fqn, &query_lower)
.or_else(|| match_tier(fqn_short, &query_lower))
{
Some(t) => t,
None => continue,
};
let uri = match Url::from_file_path(file_path) {
Ok(u) => u,
Err(()) => continue,
};
seen_fqns.insert(fqn.clone());
ranked.push(RankedSymbol {
symbol: SymbolInformation {
name: fqn.clone(),
kind: SymbolKind::CLASS,
tags: None,
deprecated: None,
location: Location {
uri,
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
},
container_name: namespace_from_fqn(fqn),
},
tier,
});
}
}
ranked.sort_by(|a, b| {
a.tier
.cmp(&b.tier)
.then_with(|| a.symbol.name.cmp(&b.symbol.name))
});
ranked.truncate(MAX_RESULTS);
let symbols: Vec<SymbolInformation> = ranked.into_iter().map(|r| r.symbol).collect();
if symbols.is_empty() {
None
} else {
Some(symbols)
}
}
}
fn function_display_name(func: &FunctionInfo) -> String {
match &func.namespace {
Some(ns) if !ns.is_empty() => format!("{}\\{}", ns, func.name),
_ => func.name.clone(),
}
}
fn namespace_from_fqn(fqn: &str) -> Option<String> {
fqn.rfind('\\').map(|i| fqn[..i].to_string())
}
#[allow(deprecated)] fn make_constant_symbol(name: &str, info: &DefineInfo, pos: Position) -> SymbolInformation {
SymbolInformation {
name: name.to_string(),
kind: SymbolKind::CONSTANT,
tags: None,
deprecated: None,
location: Location {
uri: Url::parse(&info.file_uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range::new(pos, pos),
},
container_name: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_name_no_separator() {
assert_eq!(short_name("Foo"), "Foo");
}
#[test]
fn short_name_with_namespace() {
assert_eq!(short_name("App\\Models\\User"), "User");
}
#[test]
fn short_name_with_member() {
assert_eq!(short_name("App\\Models\\User::findByEmail"), "findByEmail");
}
#[test]
fn short_name_member_takes_precedence() {
assert_eq!(short_name("Ns\\Cls::method"), "method");
}
#[test]
fn match_tier_exact() {
assert_eq!(match_tier("Foo", "foo"), Some(MatchTier::Exact));
}
#[test]
fn match_tier_prefix() {
assert_eq!(match_tier("FooBar", "foo"), Some(MatchTier::Prefix));
}
#[test]
fn match_tier_substring() {
assert_eq!(match_tier("MyFooBar", "foo"), Some(MatchTier::Substring));
}
#[test]
fn match_tier_no_match() {
assert_eq!(match_tier("Bar", "foo"), None);
}
#[test]
fn match_tier_empty_query() {
assert_eq!(match_tier("Anything", ""), Some(MatchTier::Substring));
}
#[test]
fn tier_ordering() {
assert!(MatchTier::Exact < MatchTier::Prefix);
assert!(MatchTier::Prefix < MatchTier::Substring);
}
}