use async_lsp::lsp_types::{DocumentSymbol, SymbolKind};
use ropey::Rope;
use wcl_lang::lang::ast::*;
use crate::convert::span_to_lsp_range;
#[allow(deprecated)] pub fn document_symbols(doc: &Document, rope: &Rope) -> Vec<DocumentSymbol> {
let mut symbols = Vec::new();
for item in &doc.items {
if let Some(sym) = doc_item_symbol(item, rope) {
symbols.push(sym);
}
}
symbols
}
#[allow(deprecated)]
fn doc_item_symbol(item: &DocItem, rope: &Rope) -> Option<DocumentSymbol> {
match item {
DocItem::Import(import) => {
let path = import
.path
.parts
.iter()
.filter_map(|p| match p {
StringPart::Literal(s) => Some(s.as_str()),
_ => None,
})
.collect::<String>();
let lazy_suffix = import
.lazy_namespace
.as_ref()
.map(|ns| format!(" lazy({})", wcl_lang::ast::join_path(ns)))
.unwrap_or_default();
Some(DocumentSymbol {
name: format!("import \"{}\"{}", path, lazy_suffix),
detail: None,
kind: SymbolKind::PACKAGE,
tags: None,
deprecated: None,
range: span_to_lsp_range(import.span, rope),
selection_range: span_to_lsp_range(import.path.span, rope),
children: None,
})
}
DocItem::ExportLet(el) => Some(DocumentSymbol {
name: el.name.name.clone(),
detail: Some("export let".to_string()),
kind: SymbolKind::VARIABLE,
tags: None,
deprecated: None,
range: span_to_lsp_range(el.span, rope),
selection_range: span_to_lsp_range(el.name.span, rope),
children: None,
}),
DocItem::ExportMacro(m) => Some(DocumentSymbol {
name: m.name.name.clone(),
detail: Some("export macro".to_string()),
kind: SymbolKind::FUNCTION,
tags: None,
deprecated: None,
range: span_to_lsp_range(m.span, rope),
selection_range: span_to_lsp_range(m.name.span, rope),
children: None,
}),
DocItem::ReExport(re) => Some(DocumentSymbol {
name: re.name.name.clone(),
detail: Some("export".to_string()),
kind: SymbolKind::VARIABLE,
tags: None,
deprecated: None,
range: span_to_lsp_range(re.span, rope),
selection_range: span_to_lsp_range(re.name.span, rope),
children: None,
}),
DocItem::Body(body_item) => body_item_symbol(body_item, rope),
#[allow(deprecated)]
DocItem::FunctionDecl(decl) => Some(DocumentSymbol {
name: decl.name.name.clone(),
detail: Some("declare".to_string()),
kind: SymbolKind::FUNCTION,
tags: None,
deprecated: None,
range: span_to_lsp_range(decl.span, rope),
selection_range: span_to_lsp_range(decl.name.span, rope),
children: None,
}),
DocItem::Namespace(ns) => {
let children: Vec<_> = ns
.items
.iter()
.filter_map(|i| doc_item_symbol(i, rope))
.collect();
Some(DocumentSymbol {
name: join_path(&ns.path),
detail: Some("namespace".to_string()),
kind: SymbolKind::NAMESPACE,
tags: None,
deprecated: None,
range: span_to_lsp_range(ns.span, rope),
selection_range: span_to_lsp_range(ns.path.first().unwrap().span, rope),
children: if children.is_empty() {
None
} else {
Some(children)
},
})
}
DocItem::Use(use_decl) => {
let target_names: Vec<_> = use_decl
.targets
.iter()
.map(|t| {
if let Some(ref alias) = t.alias {
format!("{} -> {}", t.name.name, alias.name)
} else {
t.name.name.clone()
}
})
.collect();
let ns_path = join_path(&use_decl.namespace_path);
let display = if target_names.len() == 1 {
format!("use {}::{}", ns_path, target_names[0])
} else {
format!("use {}::{{{}}}", ns_path, target_names.join(", "))
};
Some(DocumentSymbol {
name: display,
detail: Some("use".to_string()),
kind: SymbolKind::PACKAGE,
tags: None,
deprecated: None,
range: span_to_lsp_range(use_decl.span, rope),
selection_range: span_to_lsp_range(
use_decl.namespace_path.first().unwrap().span,
rope,
),
children: None,
})
}
}
}
#[allow(deprecated)]
fn body_item_symbol(item: &BodyItem, rope: &Rope) -> Option<DocumentSymbol> {
match item {
BodyItem::Attribute(attr) => Some(DocumentSymbol {
name: attr.name.name.clone(),
detail: None,
kind: SymbolKind::PROPERTY,
tags: None,
deprecated: None,
range: span_to_lsp_range(attr.span, rope),
selection_range: span_to_lsp_range(attr.name.span, rope),
children: None,
}),
BodyItem::Block(block) => {
let name = block_display_name(block);
let children: Vec<DocumentSymbol> = block
.body
.iter()
.filter_map(|child| body_item_symbol(child, rope))
.collect();
Some(DocumentSymbol {
name,
detail: None,
kind: SymbolKind::CLASS,
tags: None,
deprecated: None,
range: span_to_lsp_range(block.span, rope),
selection_range: span_to_lsp_range(block.kind.span, rope),
children: if children.is_empty() {
None
} else {
Some(children)
},
})
}
BodyItem::LetBinding(lb) => Some(DocumentSymbol {
name: lb.name.name.clone(),
detail: Some("let".to_string()),
kind: SymbolKind::VARIABLE,
tags: None,
deprecated: None,
range: span_to_lsp_range(lb.span, rope),
selection_range: span_to_lsp_range(lb.name.span, rope),
children: None,
}),
BodyItem::MacroDef(md) => Some(DocumentSymbol {
name: md.name.name.clone(),
detail: Some("macro".to_string()),
kind: SymbolKind::FUNCTION,
tags: None,
deprecated: None,
range: span_to_lsp_range(md.span, rope),
selection_range: span_to_lsp_range(md.name.span, rope),
children: None,
}),
BodyItem::Schema(schema) => {
let name = schema
.name
.parts
.iter()
.filter_map(|p| match p {
StringPart::Literal(s) => Some(s.as_str()),
_ => None,
})
.collect::<String>();
Some(DocumentSymbol {
name,
detail: Some("schema".to_string()),
kind: SymbolKind::INTERFACE,
tags: None,
deprecated: None,
range: span_to_lsp_range(schema.span, rope),
selection_range: span_to_lsp_range(schema.name.span, rope),
children: None,
})
}
BodyItem::Table(table) => {
let name = table
.inline_id
.as_ref()
.map(|id| match id {
InlineId::Literal(lit) => format!("table {}", lit.value),
InlineId::Interpolated(_) => "table <interpolated>".to_string(),
})
.unwrap_or_else(|| "table".to_string());
Some(DocumentSymbol {
name,
detail: None,
kind: SymbolKind::STRUCT,
tags: None,
deprecated: None,
range: span_to_lsp_range(table.span, rope),
selection_range: span_to_lsp_range(table.span, rope),
children: None,
})
}
BodyItem::Validation(val) => {
let name = val
.name
.parts
.iter()
.filter_map(|p| match p {
StringPart::Literal(s) => Some(s.as_str()),
_ => None,
})
.collect::<String>();
Some(DocumentSymbol {
name,
detail: Some("validation".to_string()),
kind: SymbolKind::EVENT,
tags: None,
deprecated: None,
range: span_to_lsp_range(val.span, rope),
selection_range: span_to_lsp_range(val.name.span, rope),
children: None,
})
}
BodyItem::MacroCall(_)
| BodyItem::ForLoop(_)
| BodyItem::Conditional(_)
| BodyItem::DecoratorSchema(_)
| BodyItem::SymbolSetDecl(_)
| BodyItem::StructDef(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use wcl_lang::lang::span::FileId;
fn get_symbols(source: &str) -> Vec<DocumentSymbol> {
let file_id = FileId(0);
let (doc, _) = wcl_lang::lang::parse(source, file_id);
let rope = Rope::from_str(source);
document_symbols(&doc, &rope)
}
#[test]
fn test_block_symbol() {
let syms = get_symbols("config { port = 8080 }");
assert_eq!(syms.len(), 1);
assert_eq!(syms[0].name, "config");
assert_eq!(syms[0].kind, SymbolKind::CLASS);
}
#[test]
fn test_block_with_id() {
let syms = get_symbols("server main { port = 80 }");
assert_eq!(syms[0].name, "server #main");
}
#[test]
fn test_let_binding_symbol() {
let syms = get_symbols("let x = 42");
assert_eq!(syms.len(), 1);
assert_eq!(syms[0].name, "x");
assert_eq!(syms[0].kind, SymbolKind::VARIABLE);
}
#[test]
fn test_nested_children() {
let syms = get_symbols("server { inner { x = 1 } }");
assert_eq!(syms.len(), 1);
let children = syms[0].children.as_ref().unwrap();
assert!(children.iter().any(|c| c.name == "inner"));
}
#[test]
fn test_attribute_symbol() {
let syms = get_symbols("config { port = 8080\nhost = \"localhost\" }");
let children = syms[0].children.as_ref().unwrap();
assert_eq!(children.len(), 2);
assert!(children.iter().all(|c| c.kind == SymbolKind::PROPERTY));
}
#[test]
fn test_multiple_top_level() {
let syms = get_symbols("let x = 1\nserver { port = 80 }\nclient { timeout = 30 }");
assert_eq!(syms.len(), 3);
}
}
fn block_display_name(block: &Block) -> String {
let mut name = block.kind.name.clone();
if let Some(id) = &block.inline_id {
match id {
InlineId::Literal(lit) => {
name.push_str(&format!(" #{}", lit.value));
}
InlineId::Interpolated(_) => {
name.push_str(" #<interpolated>");
}
}
}
name
}