use crate::doc_render::render_doc_comment;
use shape_ast::ast::{
DestructurePattern, ExportItem, Item, Program, Span, TypeAnnotation, VarKind,
};
use tower_lsp_server::ls_types::{
CompletionItem, CompletionItemKind, CompletionItemTag, Documentation, MarkupContent, MarkupKind,
};
fn format_type_annotation(annotation: &TypeAnnotation) -> String {
match annotation {
TypeAnnotation::Basic(name) => name.clone(),
TypeAnnotation::Reference(name) => name.to_string(),
TypeAnnotation::Generic { name, args } => {
if args.is_empty() {
name.to_string()
} else {
let arg_list: Vec<String> = args.iter().map(format_type_annotation).collect();
format!("{}<{}>", name, arg_list.join(", "))
}
}
TypeAnnotation::Array(inner) => format!("Array<{}>", format_type_annotation(inner)),
TypeAnnotation::Tuple(types) => {
let type_list: Vec<String> = types.iter().map(format_type_annotation).collect();
format!("({})", type_list.join(", "))
}
TypeAnnotation::Object(_) => "Object".to_string(),
TypeAnnotation::Function {
params, returns, ..
} => {
let param_list: Vec<String> = params
.iter()
.map(|p| format_type_annotation(&p.type_annotation))
.collect();
format!(
"({}) -> {}",
param_list.join(", "),
format_type_annotation(returns)
)
}
TypeAnnotation::Union(types) => {
let type_list: Vec<String> = types.iter().map(format_type_annotation).collect();
type_list.join(" | ")
}
TypeAnnotation::Intersection(types) => {
let type_list: Vec<String> = types.iter().map(format_type_annotation).collect();
type_list.join(" + ")
}
TypeAnnotation::Void => "void".to_string(),
TypeAnnotation::Never => "never".to_string(),
TypeAnnotation::Null => "None".to_string(),
TypeAnnotation::Undefined => "undefined".to_string(),
TypeAnnotation::Dyn(traits) => format!("dyn {}", traits.iter().map(|t| t.as_str()).collect::<Vec<_>>().join(" + ")),
}
}
#[derive(Debug, Clone)]
pub struct SymbolInfo {
pub name: String,
pub kind: SymbolKind,
pub detail: Option<String>,
pub documentation: Option<String>,
pub type_annotation: Option<String>,
pub annotations: Vec<String>,
pub deprecated: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymbolKind {
Variable,
Constant,
Function,
Type,
}
fn rendered_doc_for_span(program: &Program, span: Span) -> Option<String> {
program
.docs
.comment_for_span(span)
.map(|comment| render_doc_comment(program, comment, None, None, None))
}
fn deprecated_for_span(program: &Program, span: Span) -> bool {
program
.docs
.comment_for_span(span)
.is_some_and(|comment| comment.deprecated_doc().is_some())
}
pub fn extract_symbols(program: &Program) -> Vec<SymbolInfo> {
let mut symbols = Vec::new();
for item in &program.items {
match item {
Item::Statement(statement, _) => {
use shape_ast::ast::Statement;
if let Statement::VariableDecl(var_decl, _) = statement {
let kind = match var_decl.kind {
VarKind::Let => SymbolKind::Variable,
VarKind::Var => SymbolKind::Variable,
VarKind::Const => SymbolKind::Constant,
};
let type_str = var_decl
.type_annotation
.as_ref()
.map(format_type_annotation);
for (name, _) in var_decl.pattern.get_bindings() {
symbols.push(SymbolInfo {
name,
kind,
detail: type_str.clone(),
documentation: None,
type_annotation: type_str.clone(),
annotations: vec![],
deprecated: false,
});
}
}
}
Item::VariableDecl(var_decl, _) => {
let kind = match var_decl.kind {
VarKind::Let => SymbolKind::Variable,
VarKind::Var => SymbolKind::Variable,
VarKind::Const => SymbolKind::Constant,
};
let type_str = var_decl
.type_annotation
.as_ref()
.map(format_type_annotation);
for (name, _) in var_decl.pattern.get_bindings() {
symbols.push(SymbolInfo {
name,
kind,
detail: type_str.clone(),
documentation: None,
type_annotation: type_str.clone(),
annotations: vec![],
deprecated: false,
});
}
}
Item::Function(func_def, span) => {
let params: Vec<String> = func_def
.params
.iter()
.flat_map(|p| {
let prefix = if p.is_reference { "&" } else { "" };
p.get_identifiers()
.into_iter()
.map(move |name| format!("{}{}", prefix, name))
})
.collect();
let annotations: Vec<String> = func_def
.annotations
.iter()
.map(|a| a.name.clone())
.collect();
let signature = format!("{}({})", func_def.name, params.join(", "));
let doc = rendered_doc_for_span(program, *span);
symbols.push(SymbolInfo {
name: func_def.name.clone(),
kind: SymbolKind::Function,
detail: Some(signature),
documentation: doc,
type_annotation: None,
annotations,
deprecated: deprecated_for_span(program, *span),
});
}
Item::TypeAlias(type_alias, span) => {
symbols.push(SymbolInfo {
name: type_alias.name.clone(),
kind: SymbolKind::Type,
detail: Some("type alias".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
Item::StructType(struct_def, span) => {
symbols.push(SymbolInfo {
name: struct_def.name.clone(),
kind: SymbolKind::Type,
detail: Some("type".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
Item::Trait(trait_def, span) => {
symbols.push(SymbolInfo {
name: trait_def.name.clone(),
kind: SymbolKind::Type,
detail: Some("trait".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
Item::Enum(enum_def, span) => {
symbols.push(SymbolInfo {
name: enum_def.name.clone(),
kind: SymbolKind::Type,
detail: Some("enum".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
Item::ForeignFunction(foreign_fn, span) => {
let params: Vec<String> = foreign_fn
.params
.iter()
.flat_map(|p| {
let prefix = if p.is_reference { "&" } else { "" };
p.get_identifiers()
.into_iter()
.map(move |name| format!("{}{}", prefix, name))
})
.collect();
let annotations: Vec<String> = foreign_fn
.annotations
.iter()
.map(|a| a.name.clone())
.collect();
let signature = if let Some(ref rt) = foreign_fn.return_type {
format!(
"fn {} {}({}) -> {}",
foreign_fn.language,
foreign_fn.name,
params.join(", "),
format_type_annotation(rt)
)
} else {
format!(
"fn {} {}({})",
foreign_fn.language,
foreign_fn.name,
params.join(", ")
)
};
symbols.push(SymbolInfo {
name: foreign_fn.name.clone(),
kind: SymbolKind::Function,
detail: Some(signature),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations,
deprecated: deprecated_for_span(program, *span),
});
}
Item::Export(export_stmt, span) => {
match &export_stmt.item {
ExportItem::Function(func_def) => {
let params: Vec<String> = func_def
.params
.iter()
.flat_map(|p| p.get_identifiers())
.collect();
let annotations: Vec<String> = func_def
.annotations
.iter()
.map(|a| a.name.clone())
.collect();
let signature = format!("{}({})", func_def.name, params.join(", "));
let doc = rendered_doc_for_span(program, *span);
symbols.push(SymbolInfo {
name: func_def.name.clone(),
kind: SymbolKind::Function,
detail: Some(signature),
documentation: doc,
type_annotation: None,
annotations,
deprecated: deprecated_for_span(program, *span),
});
}
ExportItem::Enum(enum_def) => {
symbols.push(SymbolInfo {
name: enum_def.name.clone(),
kind: SymbolKind::Type,
detail: Some("enum".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
ExportItem::Struct(struct_def) => {
symbols.push(SymbolInfo {
name: struct_def.name.clone(),
kind: SymbolKind::Type,
detail: Some("struct".to_string()),
documentation: rendered_doc_for_span(program, *span),
type_annotation: None,
annotations: vec![],
deprecated: deprecated_for_span(program, *span),
});
}
_ => {
}
}
}
_ => {
}
}
}
symbols
}
pub fn get_pattern_name(pattern: &DestructurePattern) -> Option<String> {
pattern
.get_bindings()
.into_iter()
.next()
.map(|(name, _)| name)
}
pub fn get_pattern_names(pattern: &DestructurePattern) -> Vec<(String, Span)> {
pattern.get_bindings()
}
pub fn symbols_to_completions(symbols: &[SymbolInfo]) -> Vec<CompletionItem> {
symbols
.iter()
.map(|symbol| {
let kind = match symbol.kind {
SymbolKind::Variable | SymbolKind::Constant => CompletionItemKind::VARIABLE,
SymbolKind::Function => CompletionItemKind::FUNCTION,
SymbolKind::Type => CompletionItemKind::STRUCT,
};
let documentation = symbol.documentation.as_ref().map(|doc| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: doc.clone(),
})
});
let tags = if symbol.deprecated {
Some(vec![CompletionItemTag::DEPRECATED])
} else {
None
};
CompletionItem {
label: symbol.name.clone(),
kind: Some(kind),
detail: symbol.detail.clone(),
documentation,
tags,
..CompletionItem::default()
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use shape_ast::parser::parse_program;
#[test]
fn test_extract_variables() {
let code = r#"let x = 5;
const PI = 3.14;
var counter = 0;"#;
let program = parse_program(code).expect("Failed to parse test code");
eprintln!("Program items count: {}", program.items.len());
for (i, item) in program.items.iter().enumerate() {
eprintln!("Item {}: {:?}", i, std::mem::discriminant(item));
}
let symbols = extract_symbols(&program);
assert_eq!(
symbols.len(),
3,
"Expected 3 symbols, found {}: {:?}",
symbols.len(),
symbols
);
assert!(
symbols
.iter()
.any(|s| s.name == "x" && s.kind == SymbolKind::Variable)
);
assert!(
symbols
.iter()
.any(|s| s.name == "PI" && s.kind == SymbolKind::Constant)
);
assert!(
symbols
.iter()
.any(|s| s.name == "counter" && s.kind == SymbolKind::Variable)
);
}
#[test]
fn test_extract_functions() {
let code = r#"
function add(a, b) {
return a + b;
}
function greet(name) {
return "Hello " + name;
}
"#;
let program = parse_program(code).unwrap();
let symbols = extract_symbols(&program);
assert_eq!(symbols.len(), 2);
assert!(
symbols
.iter()
.any(|s| s.name == "add" && s.kind == SymbolKind::Function)
);
assert!(
symbols
.iter()
.any(|s| s.name == "greet" && s.kind == SymbolKind::Function)
);
}
#[test]
fn test_extract_annotated_functions() {
let code = r#"
annotation my_ann() {}
@my_ann
function hammer(candle) {
return candle.close > candle.open;
}
@my_ann
function doji(candle) {
return abs(candle.close - candle.open) < 0.1;
}
"#;
let program = parse_program(code).unwrap();
let symbols = extract_symbols(&program);
assert!(symbols.len() >= 2);
assert!(symbols.iter().any(|s| s.name == "hammer"
&& s.kind == SymbolKind::Function
&& s.annotations.contains(&"my_ann".to_string())));
assert!(symbols.iter().any(|s| s.name == "doji"
&& s.kind == SymbolKind::Function
&& s.annotations.contains(&"my_ann".to_string())));
}
#[test]
fn test_symbols_to_completions() {
let symbols = vec![
SymbolInfo {
name: "myVar".to_string(),
kind: SymbolKind::Variable,
detail: Some("Number".to_string()),
documentation: None,
type_annotation: Some("Number".to_string()),
annotations: vec![],
deprecated: false,
},
SymbolInfo {
name: "myFunc".to_string(),
kind: SymbolKind::Function,
detail: Some("myFunc(a, b)".to_string()),
documentation: Some("A test function".to_string()),
type_annotation: None,
annotations: vec![],
deprecated: false,
},
];
let completions = symbols_to_completions(&symbols);
assert_eq!(completions.len(), 2);
assert_eq!(completions[0].label, "myVar");
assert_eq!(completions[0].kind, Some(CompletionItemKind::VARIABLE));
assert_eq!(completions[1].label, "myFunc");
assert_eq!(completions[1].kind, Some(CompletionItemKind::FUNCTION));
}
#[test]
fn test_annotated_function_completion() {
let symbols = vec![SymbolInfo {
name: "my_strategy".to_string(),
kind: SymbolKind::Function,
detail: Some("my_strategy(row, ctx)".to_string()),
documentation: None,
type_annotation: None,
annotations: vec!["strategy".to_string()],
deprecated: false,
}];
let completions = symbols_to_completions(&symbols);
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].kind, Some(CompletionItemKind::FUNCTION));
}
#[test]
fn test_filter_symbols_by_annotation() {
let symbols = vec![
SymbolInfo {
name: "regular".to_string(),
kind: SymbolKind::Function,
detail: None,
documentation: None,
type_annotation: None,
annotations: vec![],
deprecated: false,
},
SymbolInfo {
name: "my_strategy".to_string(),
kind: SymbolKind::Function,
detail: None,
documentation: None,
type_annotation: None,
annotations: vec!["strategy".to_string()],
deprecated: false,
},
];
let strategies: Vec<_> = symbols
.iter()
.filter(|s| s.annotations.contains(&"strategy".to_string()))
.collect();
assert_eq!(strategies.len(), 1);
assert_eq!(strategies[0].name, "my_strategy");
}
#[test]
fn test_deprecated_doc_tag_flag_extraction() {
let code = r#"
/// Old computation kept for back-compat.
///
/// @deprecated Use `new_compute` instead.
fn old_compute(x: int) -> int {
return x * 2;
}
fn new_compute(x: int) -> int {
return x * 3;
}
"#;
let program = parse_program(code).expect("parse failed");
let symbols = extract_symbols(&program);
let old = symbols
.iter()
.find(|s| s.name == "old_compute")
.expect("old_compute symbol present");
let new = symbols
.iter()
.find(|s| s.name == "new_compute")
.expect("new_compute symbol present");
assert!(
old.deprecated,
"old_compute carries @deprecated doc-tag → SymbolInfo.deprecated should be true"
);
assert!(
!new.deprecated,
"new_compute has no @deprecated → SymbolInfo.deprecated should be false"
);
}
#[test]
fn test_symbols_to_completions_emits_deprecated_tag() {
let symbols = vec![
SymbolInfo {
name: "fresh".to_string(),
kind: SymbolKind::Function,
detail: None,
documentation: None,
type_annotation: None,
annotations: vec![],
deprecated: false,
},
SymbolInfo {
name: "stale".to_string(),
kind: SymbolKind::Function,
detail: None,
documentation: None,
type_annotation: None,
annotations: vec![],
deprecated: true,
},
];
let completions = symbols_to_completions(&symbols);
let fresh = completions.iter().find(|c| c.label == "fresh").unwrap();
let stale = completions.iter().find(|c| c.label == "stale").unwrap();
assert!(fresh.tags.is_none(), "fresh symbol should carry no tags");
let tags = stale.tags.as_ref().expect("stale symbol should carry tags");
assert!(
tags.contains(&CompletionItemTag::DEPRECATED),
"stale symbol should carry CompletionItemTag::DEPRECATED, got {:?}",
tags
);
}
}