use async_lsp::lsp_types::{
Documentation, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
};
pub fn signature_help(
source: &str,
offset: usize,
analysis: Option<&crate::state::AnalysisResult>,
) -> Option<SignatureHelp> {
let before = &source[..offset.min(source.len())];
let (fn_name, active_param) = find_call_context(before)?;
let builtin_sigs;
let sig = if let Some(analysis) = analysis {
find_signature(analysis, &fn_name)
} else {
builtin_sigs = wcl_lang::eval::builtin_signatures();
find_signature_in_slice(&builtin_sigs, &fn_name)
};
if let Some(sig) = sig {
let params: Vec<ParameterInformation> = sig
.params
.iter()
.map(|p| ParameterInformation {
label: ParameterLabel::Simple(p.to_string()),
documentation: None,
})
.collect();
let label = format!("{}({})", sig.name, sig.params.join(", "));
return Some(SignatureHelp {
signatures: vec![SignatureInformation {
label,
documentation: Some(Documentation::String(sig.doc.to_string())),
parameters: Some(params),
active_parameter: Some(active_param),
}],
active_signature: Some(0),
active_parameter: Some(active_param),
});
}
if let Some(analysis) = analysis {
if let Some(md) = analysis.macro_registry.function_macros.get(&fn_name) {
let params: Vec<ParameterInformation> = md
.params
.iter()
.map(|p| {
let label = if let Some(te) = &p.type_constraint {
format!("{}: {}", p.name.name, type_expr_label(te))
} else {
p.name.name.clone()
};
ParameterInformation {
label: ParameterLabel::Simple(label),
documentation: None,
}
})
.collect();
let param_strs: Vec<String> = md
.params
.iter()
.map(|p| {
if let Some(te) = &p.type_constraint {
format!("{}: {}", p.name.name, type_expr_label(te))
} else {
p.name.name.clone()
}
})
.collect();
let label = format!("{}({})", md.name.name, param_strs.join(", "));
return Some(SignatureHelp {
signatures: vec![SignatureInformation {
label,
documentation: Some(Documentation::String("user-defined macro".to_string())),
parameters: Some(params),
active_parameter: Some(active_param),
}],
active_signature: Some(0),
active_parameter: Some(active_param),
});
}
}
None
}
fn find_signature<'a>(
analysis: &'a crate::state::AnalysisResult,
fn_name: &str,
) -> Option<&'a wcl_lang::eval::FunctionSignature> {
if let Some(qualified) = analysis.namespace_aliases.get(fn_name) {
if let Some(sig) = analysis
.function_signatures
.iter()
.find(|s| s.name == *qualified)
{
return Some(sig);
}
}
find_signature_in_slice(&analysis.function_signatures, fn_name)
}
fn find_signature_in_slice<'a>(
sigs: &'a [wcl_lang::eval::FunctionSignature],
fn_name: &str,
) -> Option<&'a wcl_lang::eval::FunctionSignature> {
if let Some(sig) = sigs.iter().find(|s| s.name == fn_name) {
return Some(sig);
}
if fn_name.contains("::") {
return None;
}
let mut matches = sigs.iter().filter(|s| {
s.name
.rsplit_once("::")
.map(|(_, tail)| tail == fn_name)
.unwrap_or(false)
});
let first = matches.next()?;
if matches.next().is_none() {
Some(first)
} else {
None
}
}
fn type_expr_label(te: &wcl_lang::lang::ast::TypeExpr) -> String {
match te {
wcl_lang::lang::ast::TypeExpr::String(_) => "string".to_string(),
wcl_lang::lang::ast::TypeExpr::I8(_) => "i8".to_string(),
wcl_lang::lang::ast::TypeExpr::U8(_) => "u8".to_string(),
wcl_lang::lang::ast::TypeExpr::I16(_) => "i16".to_string(),
wcl_lang::lang::ast::TypeExpr::U16(_) => "u16".to_string(),
wcl_lang::lang::ast::TypeExpr::I32(_) => "i32".to_string(),
wcl_lang::lang::ast::TypeExpr::U32(_) => "u32".to_string(),
wcl_lang::lang::ast::TypeExpr::I64(_) => "i64".to_string(),
wcl_lang::lang::ast::TypeExpr::U64(_) => "u64".to_string(),
wcl_lang::lang::ast::TypeExpr::I128(_) => "i128".to_string(),
wcl_lang::lang::ast::TypeExpr::U128(_) => "u128".to_string(),
wcl_lang::lang::ast::TypeExpr::F32(_) => "f32".to_string(),
wcl_lang::lang::ast::TypeExpr::F64(_) => "f64".to_string(),
wcl_lang::lang::ast::TypeExpr::Date(_) => "date".to_string(),
wcl_lang::lang::ast::TypeExpr::Duration(_) => "duration".to_string(),
wcl_lang::lang::ast::TypeExpr::Bool(_) => "bool".to_string(),
wcl_lang::lang::ast::TypeExpr::Any(_) => "any".to_string(),
_ => "any".to_string(),
}
}
fn find_call_context(before: &str) -> Option<(String, u32)> {
let mut depth = 0i32;
let mut commas = 0u32;
for (i, ch) in before.char_indices().rev() {
match ch {
')' => depth += 1,
'(' => {
if depth == 0 {
let prefix = before[..i].trim_end();
let name = prefix
.rsplit(|c: char| !c.is_alphanumeric() && c != '_' && c != ':')
.next()?;
if name.is_empty() || name.ends_with(':') {
return None;
}
return Some((name.to_string(), commas));
}
depth -= 1;
}
',' if depth == 0 => commas += 1,
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_call_context_simple() {
let (name, param) = find_call_context("upper(").unwrap();
assert_eq!(name, "upper");
assert_eq!(param, 0);
}
#[test]
fn test_find_call_context_namespaced() {
let (name, param) = find_call_context("wdoc::icon(").unwrap();
assert_eq!(name, "wdoc::icon");
assert_eq!(param, 0);
}
#[test]
fn test_find_call_context_second_param() {
let (name, param) = find_call_context("replace(s, ").unwrap();
assert_eq!(name, "replace");
assert_eq!(param, 1);
}
#[test]
fn test_signature_help_upper() {
let help = signature_help("upper(", 6, None).unwrap();
assert_eq!(help.signatures[0].label, "upper(s: string)");
assert_eq!(help.active_parameter, Some(0));
}
#[test]
fn test_signature_help_unknown_returns_none() {
let result = signature_help("unknown_fn(", 11, None);
assert!(result.is_none());
}
#[test]
fn test_find_call_context_nested() {
let (name, param) = find_call_context("join(split(x, \".\"), ").unwrap();
assert_eq!(name, "join");
assert_eq!(param, 1);
}
#[test]
fn test_signature_help_builtin_with_analysis() {
use crate::analysis::analyze;
let source = "let x = upper(";
let analysis = analyze(source, &wcl_lang::ParseOptions::default());
let offset = source.len();
let help = signature_help(source, offset, Some(&analysis)).unwrap();
assert!(help.signatures[0].label.contains("upper"));
}
#[test]
fn test_signature_help_with_macro() {
use crate::analysis::analyze;
let source = "macro greet(name, greeting) { msg = greeting }\nlet x = greet(";
let analysis = analyze(source, &wcl_lang::ParseOptions::default());
let offset = source.len();
let help = signature_help(source, offset, Some(&analysis));
assert!(help.is_some(), "should find macro signature");
let h = help.unwrap();
assert!(h.signatures[0].label.contains("greet"));
assert!(h.signatures[0].label.contains("name"));
assert!(h.signatures[0].label.contains("greeting"));
}
#[test]
fn test_signature_help_namespaced_custom_function() {
use crate::analysis::analyze;
use std::sync::Arc;
let mut functions = wcl_lang::FunctionRegistry::new();
let dummy: wcl_lang::BuiltinFn =
Arc::new(|_: &[wcl_lang::Value]| Ok(wcl_lang::Value::Null));
functions.register(
"wdoc::icon",
dummy,
wcl_lang::FunctionSignature {
name: "wdoc::icon".into(),
params: vec!["name: string".into()],
return_type: "string".into(),
doc: "Render a named SVG icon".into(),
},
);
let options = wcl_lang::ParseOptions {
functions,
..Default::default()
};
let source = "let x = wdoc::icon(";
let analysis = analyze(source, &options);
let help = signature_help(source, source.len(), Some(&analysis)).unwrap();
assert_eq!(help.signatures[0].label, "wdoc::icon(name: string)");
}
#[test]
fn test_signature_help_use_alias_custom_function() {
use crate::analysis::analyze;
use std::sync::Arc;
let mut functions = wcl_lang::FunctionRegistry::new();
let dummy: wcl_lang::BuiltinFn =
Arc::new(|_: &[wcl_lang::Value]| Ok(wcl_lang::Value::Null));
functions.register(
"wdoc::bold",
dummy,
wcl_lang::FunctionSignature {
name: "wdoc::bold".into(),
params: vec!["text: string".into()],
return_type: "string".into(),
doc: "Render bold text".into(),
},
);
let options = wcl_lang::ParseOptions {
functions,
..Default::default()
};
let source = "namespace wdoc { declare bold(text: string) -> string }\nuse wdoc::{bold}\nlet x = bold(";
let analysis = analyze(source, &options);
let help = signature_help(source, source.len(), Some(&analysis)).unwrap();
assert_eq!(help.signatures[0].label, "wdoc::bold(text: string)");
}
}