use crate::doc_render::render_doc_comment;
use crate::type_inference::{
ParamReferenceMode, infer_function_signatures, infer_program_types, type_annotation_to_string,
unified_metadata,
};
use shape_ast::ast::{Item, Program, TypeName};
use shape_ast::parser::parse_program;
use tower_lsp_server::ls_types::{
ParameterInformation, ParameterLabel, Position, SignatureHelp, SignatureInformation,
};
pub fn get_signature_help(text: &str, position: Position) -> Option<SignatureHelp> {
if let Some(join_sig) = get_join_signature_help(text, position) {
return Some(join_sig);
}
let (function_name, active_param) = get_function_call_context(text, position)?;
get_signature_for_function(text, position, &function_name, active_param)
}
fn get_function_call_context(text: &str, position: Position) -> Option<(String, u32)> {
let lines: Vec<&str> = text.lines().collect();
if position.line as usize >= lines.len() {
return None;
}
let line = lines[position.line as usize];
let char_pos = position.character as usize;
if char_pos > line.len() {
return None;
}
let text_before = &line[..char_pos];
let paren_pos = text_before.rfind('(')?;
let before_paren = &text_before[..paren_pos];
let func_name = extract_function_name(before_paren)?;
let params_text = &text_before[paren_pos + 1..];
let active_param = params_text.matches(',').count() as u32;
Some((func_name, active_param))
}
fn extract_function_name(text: &str) -> Option<String> {
let trimmed = text.trim_end();
let mut start = trimmed.len();
for (i, ch) in trimmed.char_indices().rev() {
if ch.is_alphanumeric() || ch == '_' || ch == '.' {
if i == 0 {
start = 0;
}
} else {
start = i + ch.len_utf8();
break;
}
}
let name = &trimmed[start..];
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn get_signature_for_function(
text: &str,
position: Position,
function_name: &str,
active_param: u32,
) -> Option<SignatureHelp> {
if let Some(dot) = function_name.rfind('.') {
let receiver = &function_name[..dot];
let func = &function_name[dot + 1..];
if let Some(sig_help) =
get_module_function_signature(receiver, func, active_param, Some(text))
{
return Some(sig_help);
}
if let Some(sig_help) =
get_user_method_signature(text, position, receiver, func, active_param)
{
return Some(sig_help);
}
}
if let Some(sig_help) = get_builtin_signature(function_name, active_param) {
return Some(sig_help);
}
if let Some(sig_help) = get_user_function_signature(text, function_name, active_param) {
return Some(sig_help);
}
None
}
fn get_module_function_signature(
module_name: &str,
func_name: &str,
active_param: u32,
current_source: Option<&str>,
) -> Option<SignatureHelp> {
let module_schema = crate::completion::imports::get_registry()
.get(module_name)
.and_then(|module| module.get_schema(func_name).cloned());
if let Some(schema) = module_schema {
let parameters: Vec<ParameterInformation> = schema
.params
.iter()
.map(|p| {
let label = if p.required {
format!("{}: {}", p.name, p.type_name)
} else {
format!("{}?: {}", p.name, p.type_name)
};
ParameterInformation {
label: ParameterLabel::Simple(label),
documentation: Some(tower_lsp_server::ls_types::Documentation::String(
p.description.clone(),
)),
}
})
.collect();
let params_sig: Vec<String> = schema
.params
.iter()
.map(|p| format!("{}: {}", p.name, p.type_name))
.collect();
let sig_label = format!(
"{}.{}({}){}",
module_name,
func_name,
params_sig.join(", "),
schema
.return_type
.as_ref()
.map(|r| format!(" -> {}", r))
.unwrap_or_default()
);
let signature = SignatureInformation {
label: sig_label,
documentation: Some(tower_lsp_server::ls_types::Documentation::MarkupContent(
tower_lsp_server::ls_types::MarkupContent {
kind: tower_lsp_server::ls_types::MarkupKind::Markdown,
value: schema.description.clone(),
},
)),
parameters: Some(parameters),
active_parameter: Some(active_param),
};
return Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(active_param),
});
}
let local_schema = crate::completion::imports::local_module_function_schema_from_source(
module_name,
func_name,
current_source,
)?;
let parameters: Vec<ParameterInformation> = local_schema
.params
.iter()
.map(|p| {
let label = if p.required {
format!("{}: {}", p.name, p.type_name)
} else {
format!("{}?: {}", p.name, p.type_name)
};
ParameterInformation {
label: ParameterLabel::Simple(label),
documentation: None,
}
})
.collect();
let params_sig: Vec<String> = local_schema
.params
.iter()
.map(|p| format!("{}: {}", p.name, p.type_name))
.collect();
let sig_label = format!(
"{}.{}({}){}",
module_name,
func_name,
params_sig.join(", "),
local_schema
.return_type
.as_ref()
.map(|r| format!(" -> {}", r))
.unwrap_or_default()
);
let signature = SignatureInformation {
label: sig_label,
documentation: Some(tower_lsp_server::ls_types::Documentation::MarkupContent(
tower_lsp_server::ls_types::MarkupContent {
kind: tower_lsp_server::ls_types::MarkupKind::Markdown,
value: format!("Local module function: `{}.{}`", module_name, func_name),
},
)),
parameters: Some(parameters),
active_parameter: Some(active_param),
};
Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(active_param),
})
}
fn get_builtin_signature(function_name: &str, active_param: u32) -> Option<SignatureHelp> {
let function = unified_metadata().get_function(function_name)?;
let parameters: Vec<ParameterInformation> = function
.parameters
.iter()
.map(|param| ParameterInformation {
label: ParameterLabel::Simple(format!("{}: {}", param.name, param.param_type)),
documentation: Some(tower_lsp_server::ls_types::Documentation::String(
param.description.clone(),
)),
})
.collect();
let signature = SignatureInformation {
label: function.signature.clone(),
documentation: Some(tower_lsp_server::ls_types::Documentation::MarkupContent(
tower_lsp_server::ls_types::MarkupContent {
kind: tower_lsp_server::ls_types::MarkupKind::Markdown,
value: function.description.clone(),
},
)),
parameters: Some(parameters),
active_parameter: Some(active_param),
};
Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(active_param),
})
}
fn is_primitive_value_type_name(name: &str) -> bool {
let normalized = name.trim().trim_end_matches('?');
matches!(
normalized,
"int"
| "integer"
| "i64"
| "number"
| "float"
| "f64"
| "decimal"
| "bool"
| "boolean"
| "()"
| "void"
| "unit"
| "none"
| "null"
| "undefined"
| "never"
)
}
fn split_top_level_union(type_str: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut start = 0usize;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut brace_depth = 0usize;
let mut angle_depth = 0usize;
for (idx, ch) in type_str.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
'{' => brace_depth += 1,
'}' => brace_depth = brace_depth.saturating_sub(1),
'<' => angle_depth += 1,
'>' => angle_depth = angle_depth.saturating_sub(1),
_ => {}
}
if ch == '|'
&& paren_depth == 0
&& bracket_depth == 0
&& brace_depth == 0
&& angle_depth == 0
{
parts.push(type_str[start..idx].trim().to_string());
start = idx + ch.len_utf8();
}
}
parts.push(type_str[start..].trim().to_string());
parts.into_iter().filter(|part| !part.is_empty()).collect()
}
fn apply_ref_prefix(type_str: &str, mode: &ParamReferenceMode) -> String {
let trimmed = type_str.trim();
if trimmed.starts_with('&') {
trimmed.to_string()
} else {
format!("{}{}", mode.prefix(), trimmed)
}
}
fn format_reference_aware_type(type_str: &str, mode: Option<&ParamReferenceMode>) -> String {
let Some(mode) = mode else {
return type_str.to_string();
};
let union_parts = split_top_level_union(type_str);
if union_parts.len() <= 1 {
return apply_ref_prefix(type_str, mode);
}
union_parts
.into_iter()
.map(|part| {
if is_primitive_value_type_name(&part) {
part
} else {
apply_ref_prefix(&part, mode)
}
})
.collect::<Vec<_>>()
.join(" | ")
}
fn get_user_function_signature(
text: &str,
function_name: &str,
active_param: u32,
) -> Option<SignatureHelp> {
let program = parse_program(text).ok()?;
let function_sigs = infer_function_signatures(&program);
let (params_ref, return_type_ref, doc) = lookup_user_callable(&program, function_name)?;
let sig_info = function_sigs.get(function_name)?;
let mut param_labels = Vec::new();
let mut parameters = Vec::new();
for param in params_ref {
let name = param.simple_name().unwrap_or("_");
let ref_mode = sig_info.param_ref_modes.get(name);
let rendered = if let Some(type_ann) = ¶m.type_annotation {
let type_str = type_annotation_to_string(type_ann).unwrap_or_else(|| "_".to_string());
format!(
"{}: {}",
name,
format_reference_aware_type(&type_str, ref_mode)
)
} else if let Some((_, inferred)) = sig_info.param_types.iter().find(|(n, _)| n == name) {
format!(
"{}: {}",
name,
format_reference_aware_type(inferred, ref_mode)
)
} else if let Some(ref_mode) = ref_mode {
format!("{}: {}unknown", name, ref_mode.prefix())
} else {
name.to_string()
};
param_labels.push(rendered.clone());
parameters.push(ParameterInformation {
label: ParameterLabel::Simple(rendered),
documentation: doc
.and_then(|comment| comment.param_doc(name))
.map(|value| tower_lsp_server::ls_types::Documentation::String(value.to_string())),
});
}
let mut signature_label = format!("fn {}({})", function_name, param_labels.join(", "));
let return_type = if let Some(return_type) = return_type_ref {
type_annotation_to_string(return_type)
} else {
sig_info.return_type.clone()
};
if let Some(return_type) = return_type {
signature_label.push_str(&format!(" -> {}", return_type));
}
let signature = SignatureInformation {
label: signature_label,
documentation: doc.map(|comment| {
tower_lsp_server::ls_types::Documentation::String(render_doc_comment(
&program, comment, None, None, None,
))
}),
parameters: Some(parameters),
active_parameter: Some(active_param),
};
Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(active_param),
})
}
fn sanitize_text_for_recovery(text: &str, position: Position) -> String {
let mut lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
let line_idx = position.line as usize;
if line_idx >= lines.len() {
return text.to_string();
}
let line = &lines[line_idx];
let char_pos = (position.character as usize).min(line.len());
let before = &line[..char_pos];
let blanked = " ".repeat(before.len());
lines[line_idx] = blanked;
lines.join("\n")
}
fn parse_program_recovering(text: &str, position: Position) -> Option<Program> {
if let Ok(program) = parse_program(text) {
return Some(program);
}
let sanitized = sanitize_text_for_recovery(text, position);
parse_program(&sanitized).ok()
}
fn resolve_receiver_type_name(program: &Program, receiver_text: &str) -> Option<String> {
let trimmed = receiver_text.trim();
if trimmed.is_empty() || !is_simple_identifier(trimmed) {
return None;
}
let types = infer_program_types(program);
let raw = types.get(trimmed)?.clone();
Some(strip_to_base_type_name(&raw))
}
fn is_simple_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
fn strip_to_base_type_name(ty: &str) -> String {
let mut s = ty.trim();
while let Some(rest) = s.strip_prefix('&') {
s = rest.trim_start_matches("mut ").trim();
}
let head = s.split(['<', '?', ' ']).next().unwrap_or(s);
head.trim().to_string()
}
fn typename_base(tn: &TypeName) -> &str {
match tn {
TypeName::Simple(path) => path.name(),
TypeName::Generic { name, .. } => name.name(),
}
}
fn lookup_method_def<'a>(
program: &'a Program,
target_type: &str,
method_name: &str,
) -> Option<&'a shape_ast::ast::MethodDef> {
for item in &program.items {
match item {
Item::Impl(impl_block, _) => {
if typename_base(&impl_block.target_type) == target_type {
if let Some(method) =
impl_block.methods.iter().find(|m| m.name == method_name)
{
return Some(method);
}
}
}
Item::Extend(extend_stmt, _) => {
if typename_base(&extend_stmt.type_name) == target_type {
if let Some(method) =
extend_stmt.methods.iter().find(|m| m.name == method_name)
{
return Some(method);
}
}
}
_ => {}
}
}
None
}
fn get_user_method_signature(
text: &str,
position: Position,
receiver_text: &str,
method_name: &str,
active_param: u32,
) -> Option<SignatureHelp> {
let program = parse_program_recovering(text, position)?;
let target_type = resolve_receiver_type_name(&program, receiver_text)?;
let method = lookup_method_def(&program, &target_type, method_name)?;
let mut param_labels: Vec<String> = Vec::new();
let mut parameters: Vec<ParameterInformation> = Vec::new();
for param in &method.params {
let name = param.simple_name().unwrap_or("_").to_string();
if name == "self" {
continue;
}
let rendered = if let Some(type_ann) = ¶m.type_annotation {
let type_str = type_annotation_to_string(type_ann).unwrap_or_else(|| "_".to_string());
format!("{}: {}", name, type_str)
} else {
name.clone()
};
param_labels.push(rendered.clone());
parameters.push(ParameterInformation {
label: ParameterLabel::Simple(rendered),
documentation: method
.doc_comment
.as_ref()
.and_then(|comment| comment.param_doc(&name))
.map(|value| tower_lsp_server::ls_types::Documentation::String(value.to_string())),
});
}
let mut signature_label = format!(
"fn {}.{}({})",
target_type,
method_name,
param_labels.join(", ")
);
if let Some(return_type) = method
.return_type
.as_ref()
.and_then(type_annotation_to_string)
{
signature_label.push_str(&format!(" -> {}", return_type));
}
let signature = SignatureInformation {
label: signature_label,
documentation: method.doc_comment.as_ref().map(|comment| {
tower_lsp_server::ls_types::Documentation::String(render_doc_comment(
&program, comment, None, None, None,
))
}),
parameters: Some(parameters),
active_parameter: Some(active_param),
};
Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(active_param),
})
}
fn lookup_user_callable<'a>(
program: &'a Program,
function_name: &str,
) -> Option<(
&'a [shape_ast::ast::FunctionParameter],
Option<&'a shape_ast::ast::TypeAnnotation>,
Option<&'a shape_ast::ast::DocComment>,
)> {
for item in &program.items {
match item {
Item::Function(func, span) if func.name == function_name => {
return Some((
&func.params,
func.return_type.as_ref(),
program.docs.comment_for_span(*span),
));
}
Item::ForeignFunction(func, span) if func.name == function_name => {
return Some((
&func.params,
func.return_type.as_ref(),
program.docs.comment_for_span(*span),
));
}
Item::Export(export, span) => match &export.item {
shape_ast::ast::ExportItem::Function(func) if func.name == function_name => {
return Some((
&func.params,
func.return_type.as_ref(),
program.docs.comment_for_span(*span),
));
}
shape_ast::ast::ExportItem::ForeignFunction(func) if func.name == function_name => {
return Some((
&func.params,
func.return_type.as_ref(),
program.docs.comment_for_span(*span),
));
}
_ => {}
},
_ => {}
}
}
None
}
fn get_join_signature_help(text: &str, position: Position) -> Option<SignatureHelp> {
let lines: Vec<&str> = text.lines().collect();
let current_line = position.line as usize;
let char_pos = position.character as usize;
let strategies = ["all", "race", "any", "settle"];
let mut brace_depth: i32 = 0;
let mut i = current_line;
loop {
let line = lines.get(i)?;
let effective = if i == current_line {
let end = char_pos.min(line.len());
&line[..end]
} else {
line
};
for ch in effective.chars().rev() {
match ch {
'}' => brace_depth += 1,
'{' => brace_depth -= 1,
_ => {}
}
}
if brace_depth < 0 {
let trimmed = line.trim();
for strategy in &strategies {
let pattern = format!("join {}", strategy);
if trimmed.contains(&pattern) {
let branch_index = count_join_branches(text, i, current_line, char_pos);
return Some(build_join_signature(strategy, branch_index));
}
}
return None;
}
if i == 0 {
break;
}
i -= 1;
}
None
}
fn count_join_branches(
text: &str,
join_line: usize,
cursor_line: usize,
cursor_char: usize,
) -> u32 {
let lines: Vec<&str> = text.lines().collect();
let mut count = 0u32;
let mut brace_depth: i32 = 0;
let mut started = false;
for i in join_line..=cursor_line {
let line = lines.get(i).copied().unwrap_or("");
let effective = if i == cursor_line {
let end = cursor_char.min(line.len());
&line[..end]
} else {
line
};
for ch in effective.chars() {
match ch {
'{' => {
brace_depth += 1;
if !started {
started = true;
}
}
'}' => brace_depth -= 1,
',' if started && brace_depth == 1 => count += 1,
_ => {}
}
}
}
count
}
fn build_join_signature(strategy: &str, active_branch: u32) -> SignatureHelp {
let (label, doc, return_doc) = match strategy {
"all" => (
"await join all { branch1, branch2, ... }",
"Wait for **all** branches to complete concurrently.\nReturns a tuple of all results in branch order.",
"Returns: (T1, T2, ...)",
),
"race" => (
"await join race { branch1, branch2, ... }",
"Race all branches concurrently. The **first** to complete wins; others are cancelled.",
"Returns: T (type of winning branch)",
),
"any" => (
"await join any { branch1, branch2, ... }",
"Race all branches. The **first to succeed** (non-error) wins; others are cancelled.",
"Returns: T (type of first successful branch)",
),
"settle" => (
"await join settle { branch1, branch2, ... }",
"Wait for **all** branches, preserving individual success/error results.",
"Returns: (Result<T1>, Result<T2>, ...)",
),
_ => (
"await join <strategy> { branch1, branch2, ... }",
"Concurrent join expression.",
"Returns: varies by strategy",
),
};
let parameters = vec![ParameterInformation {
label: ParameterLabel::Simple("branch: [label:] expr".to_string()),
documentation: Some(tower_lsp_server::ls_types::Documentation::String(format!(
"A concurrent branch expression.\nOptional label for named access to results.\n\n{}",
return_doc
))),
}];
let signature = SignatureInformation {
label: label.to_string(),
documentation: Some(tower_lsp_server::ls_types::Documentation::MarkupContent(
tower_lsp_server::ls_types::MarkupContent {
kind: tower_lsp_server::ls_types::MarkupKind::Markdown,
value: doc.to_string(),
},
)),
parameters: Some(parameters),
active_parameter: Some(active_branch.min(0)), };
SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter: Some(0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_function_name() {
assert_eq!(extract_function_name("sma"), Some("sma".to_string()));
assert_eq!(
extract_function_name("let x = sma"),
Some("sma".to_string())
);
assert_eq!(extract_function_name(" sma "), Some("sma".to_string()));
}
#[test]
fn test_function_call_context() {
let text = "sma(series, ";
let position = Position {
line: 0,
character: 12,
};
let result = get_function_call_context(text, position);
assert_eq!(result, Some(("sma".to_string(), 1)));
}
#[test]
fn test_builtin_signature() {
let sig_help = get_builtin_signature("abs", 0);
assert!(sig_help.is_some());
let sig_help = sig_help.unwrap();
assert_eq!(sig_help.signatures.len(), 1);
assert!(sig_help.signatures[0].label.contains("abs"));
assert_eq!(sig_help.active_parameter, Some(0));
}
#[test]
fn test_signature_help_integration() {
let text = "let x = abs(";
let position = Position {
line: 0,
character: 12,
};
let sig_help = get_signature_help(text, position);
assert!(sig_help.is_some());
let sig_help = sig_help.unwrap();
assert_eq!(sig_help.signatures.len(), 1);
assert!(sig_help.signatures[0].label.contains("abs"));
}
#[test]
fn test_join_signature_help_all() {
let text = "async fn foo() {\n await join all {\n ";
let position = Position {
line: 2,
character: 4,
};
let sig_help = get_signature_help(text, position);
assert!(
sig_help.is_some(),
"Should provide signature help inside join all block"
);
let sig = &sig_help.unwrap().signatures[0];
assert!(
sig.label.contains("join all"),
"Label should mention 'join all'"
);
}
#[test]
fn test_join_signature_help_race() {
let text = "async fn foo() {\n await join race {\n fetch(),\n ";
let position = Position {
line: 3,
character: 4,
};
let sig_help = get_signature_help(text, position);
assert!(
sig_help.is_some(),
"Should provide signature help inside join race block"
);
let sig = &sig_help.unwrap().signatures[0];
assert!(
sig.label.contains("join race"),
"Label should mention 'join race'"
);
}
#[test]
fn test_join_signature_help_not_outside_block() {
let text = "async fn foo() {\n await join all {\n 1, 2\n }\n let x = ";
let position = Position {
line: 4,
character: 10,
};
let sig_help = get_join_signature_help(text, position);
assert!(
sig_help.is_none(),
"Should NOT provide join signature help outside block"
);
}
#[test]
fn test_user_signature_shows_inferred_mutable_reference_mode() {
let text = r#"
fn mutate(a) {
a = a + "!"
return a
}
let s = "x"
mutate(s)
"#;
let sig_help =
get_user_function_signature(text, "mutate", 0).expect("expected user signature help");
let label = &sig_help.signatures[0].label;
assert!(
label.contains("a: &mut string"),
"expected inferred mutable reference signature, got: {}",
label
);
}
#[test]
fn test_user_signature_shows_memberwise_union_reference_mode() {
let text = r#"
fn foo(a) { return a }
let i = foo(1)
let s = foo("hi")
"#;
let sig_help =
get_user_function_signature(text, "foo", 0).expect("expected user signature help");
let label = &sig_help.signatures[0].label;
assert!(
label.contains("int") && label.contains("&string"),
"expected union signature with primitive/value split, got: {}",
label
);
}
#[test]
fn test_is_primitive_value_type_name() {
assert!(is_primitive_value_type_name("int"));
assert!(is_primitive_value_type_name("integer"));
assert!(is_primitive_value_type_name("i64"));
assert!(is_primitive_value_type_name("number"));
assert!(is_primitive_value_type_name("float"));
assert!(is_primitive_value_type_name("f64"));
assert!(is_primitive_value_type_name("decimal"));
assert!(is_primitive_value_type_name("bool"));
assert!(is_primitive_value_type_name("boolean"));
assert!(is_primitive_value_type_name("()"));
assert!(is_primitive_value_type_name("void"));
assert!(is_primitive_value_type_name("unit"));
assert!(is_primitive_value_type_name("none"));
assert!(is_primitive_value_type_name("null"));
assert!(is_primitive_value_type_name("undefined"));
assert!(is_primitive_value_type_name("never"));
}
#[test]
fn test_is_primitive_value_type_name_strips_optional() {
assert!(is_primitive_value_type_name("int?"));
assert!(is_primitive_value_type_name("number?"));
assert!(is_primitive_value_type_name("bool?"));
}
#[test]
fn test_is_primitive_value_type_name_rejects_non_primitives() {
assert!(!is_primitive_value_type_name("string"));
assert!(!is_primitive_value_type_name("Foo"));
assert!(!is_primitive_value_type_name("Array<int>"));
assert!(!is_primitive_value_type_name(""));
}
#[test]
fn test_split_top_level_union_no_pipe() {
let parts = split_top_level_union("int");
assert_eq!(parts, vec!["int".to_string()]);
}
#[test]
fn test_split_top_level_union_simple_pipe() {
let parts = split_top_level_union("int | string");
assert_eq!(parts, vec!["int".to_string(), "string".to_string()]);
}
#[test]
fn test_split_top_level_union_nested_brackets() {
let parts = split_top_level_union("Array<int | string> | bool");
assert_eq!(
parts,
vec!["Array<int | string>".to_string(), "bool".to_string()]
);
}
#[test]
fn test_split_top_level_union_nested_parens() {
let parts = split_top_level_union("(int | string) | bool");
assert_eq!(
parts,
vec!["(int | string)".to_string(), "bool".to_string()]
);
}
#[test]
fn test_split_top_level_union_filters_empty() {
let parts = split_top_level_union("| int |");
assert_eq!(parts, vec!["int".to_string()]);
}
#[test]
fn test_apply_ref_prefix_adds_when_missing() {
let result = apply_ref_prefix("string", &ParamReferenceMode::Shared);
assert!(
result.starts_with('&') && result.ends_with("string"),
"expected &string-like prefix, got {result:?}"
);
}
#[test]
fn test_apply_ref_prefix_no_double_prefix() {
let result = apply_ref_prefix("&string", &ParamReferenceMode::Shared);
assert_eq!(
result, "&string",
"should not double-prefix already-referenced type"
);
}
#[test]
fn test_format_reference_aware_type_none_mode_passthrough() {
let result = format_reference_aware_type("string", None);
assert_eq!(result, "string");
}
#[test]
fn test_format_reference_aware_type_union_splits_primitives() {
let result =
format_reference_aware_type("int | string", Some(&ParamReferenceMode::Shared));
assert!(
result.contains("int") && result.contains("&string"),
"expected mixed-prefix union, got {result:?}"
);
assert!(
!result.contains("&int"),
"primitives should not be prefixed in {result:?}"
);
}
#[test]
fn test_get_function_call_context_invalid_line() {
let text = "abs(";
let result = get_function_call_context(
text,
Position {
line: 99,
character: 0,
},
);
assert!(result.is_none(), "expected None for out-of-range line");
}
#[test]
fn test_get_function_call_context_no_paren() {
let text = "let x = 5";
let result = get_function_call_context(
text,
Position {
line: 0,
character: 8,
},
);
assert!(result.is_none(), "expected None when no '(' before cursor");
}
#[test]
fn test_extract_function_name_empty() {
assert!(extract_function_name("").is_none());
assert!(extract_function_name(" ").is_none());
}
#[test]
fn test_extract_function_name_qualified() {
assert_eq!(
extract_function_name("csv.load"),
Some("csv.load".to_string())
);
}
#[test]
fn test_get_signature_help_unknown_function_returns_none() {
let text = "neverDefined(";
let position = Position {
line: 0,
character: 13,
};
let result = get_signature_help(text, position);
assert!(
result.is_none(),
"expected None for unknown function call"
);
}
#[test]
fn test_build_join_signature_unknown_strategy_uses_fallback() {
let sig = build_join_signature("nonsense", 0);
let label = &sig.signatures[0].label;
assert!(
label.contains("join"),
"expected fallback label to mention join, got {label:?}"
);
}
#[test]
fn test_count_join_branches_counts_top_level_commas() {
let text = "async fn foo() {\n await join all {\n a,\n b,\n c,\n ";
let count = count_join_branches(text, 1, 5, 4);
assert_eq!(count, 3, "expected 3 commas at top level");
}
#[test]
fn test_join_signature_help_any() {
let text = "async fn foo() {\n await join any {\n ";
let position = Position {
line: 2,
character: 4,
};
let sig_help = get_signature_help(text, position);
assert!(sig_help.is_some());
let sig = &sig_help.unwrap().signatures[0];
assert!(sig.label.contains("join any"));
}
#[test]
fn test_join_signature_help_settle() {
let text = "async fn foo() {\n await join settle {\n ";
let position = Position {
line: 2,
character: 4,
};
let sig_help = get_signature_help(text, position);
assert!(sig_help.is_some());
let sig = &sig_help.unwrap().signatures[0];
assert!(sig.label.contains("join settle"));
}
#[test]
fn test_is_simple_identifier() {
assert!(is_simple_identifier("u"));
assert!(is_simple_identifier("user_name"));
assert!(is_simple_identifier("_x"));
assert!(!is_simple_identifier(""));
assert!(!is_simple_identifier("1u"));
assert!(!is_simple_identifier("u.name"));
assert!(!is_simple_identifier("get_user()"));
}
#[test]
fn test_strip_to_base_type_name_handles_prefixes_and_generics() {
assert_eq!(strip_to_base_type_name("User"), "User");
assert_eq!(strip_to_base_type_name("&User"), "User");
assert_eq!(strip_to_base_type_name("&mut User"), "User");
assert_eq!(strip_to_base_type_name("Array<int>"), "Array");
assert_eq!(strip_to_base_type_name("User?"), "User");
assert_eq!(strip_to_base_type_name(" &Foo<T> "), "Foo");
}
#[test]
fn test_sanitize_text_for_recovery_blanks_cursor_line_prefix() {
let text = "let x = 1\nlet s = u.hello(\n";
let position = Position {
line: 1,
character: 16,
};
let sanitized = sanitize_text_for_recovery(text, position);
assert!(sanitized.contains("let x = 1"));
assert!(!sanitized.contains("u.hello("));
assert_eq!(sanitized.lines().count(), text.lines().count());
}
#[test]
fn test_method_call_signature_via_impl_trait_block() {
let text = "\
trait Greet { fn hello(greeting: string) -> string; }
type User { name: string }
impl Greet for User {
fn hello(greeting: string) -> string { greeting }
}
let u = User { name: \"a\" }
let s = u.hello(
";
let position = Position {
line: 6,
character: 16,
};
let sig_help =
get_signature_help(text, position).expect("expected method-call signature help");
let sig = &sig_help.signatures[0];
assert!(
sig.label.contains("hello"),
"label should mention method name, got {:?}",
sig.label
);
assert!(
sig.label.contains("greeting"),
"label should mention the parameter, got {:?}",
sig.label
);
assert_eq!(sig_help.active_parameter, Some(0));
}
#[test]
fn test_method_call_signature_via_extend_block() {
let text = "\
type User { name: string }
extend User {
method hello(greeting: string) -> string { greeting }
}
let u = User { name: \"a\" }
let s = u.hello(
";
let position = Position {
line: 5,
character: 16,
};
let sig_help =
get_signature_help(text, position).expect("expected extend-method signature help");
let sig = &sig_help.signatures[0];
assert!(sig.label.contains("hello"));
assert!(sig.label.contains("greeting"));
}
#[test]
fn test_method_call_signature_returns_none_for_unknown_method() {
let text = "\
type User { name: string }
let u = User { name: \"a\" }
let s = u.no_such_method(
";
let position = Position {
line: 2,
character: 24,
};
assert!(get_signature_help(text, position).is_none());
}
}