use tower_lsp::lsp_types::{
ParameterInformation, ParameterLabel, Position, SignatureHelp, SignatureInformation,
};
use logicaffeine_language::token::TokenType;
use crate::document::DocumentState;
use crate::index::DefinitionKind;
pub fn signature_help(doc: &DocumentState, position: Position) -> Option<SignatureHelp> {
let offset = doc.line_index.offset(position);
let call_token = doc.tokens.iter().rev().find(|t| {
t.span.end <= offset && matches!(t.kind, TokenType::Call)
})?;
let call_idx = doc.tokens.iter().position(|t| {
t.span == call_token.span && t.kind == call_token.kind
})?;
let func_name_token = doc.tokens.get(call_idx + 1)?;
let func_name = doc.source.get(func_name_token.span.start..func_name_token.span.end)?;
let defs = doc.symbol_index.definitions_of(func_name);
let func_def = defs.iter().find(|d| d.kind == DefinitionKind::Function)?;
let detail = func_def.detail.as_ref()?;
let active_param = doc.tokens[call_idx..]
.iter()
.take_while(|t| t.span.start < offset)
.filter(|t| {
matches!(t.kind, TokenType::Comma)
|| doc
.source
.get(t.span.start..t.span.end)
.map(|s| s == "and")
.unwrap_or(false)
})
.count();
let params: Vec<ParameterInformation> = extract_params_from_signature(detail)
.into_iter()
.map(|(name, ty)| ParameterInformation {
label: ParameterLabel::Simple(name.clone()),
documentation: Some(tower_lsp::lsp_types::Documentation::String(
format!("{}: {}", name, ty),
)),
})
.collect();
Some(SignatureHelp {
signatures: vec![SignatureInformation {
label: detail.clone(),
documentation: None,
parameters: if params.is_empty() {
None
} else {
Some(params)
},
active_parameter: Some(active_param as u32),
}],
active_signature: Some(0),
active_parameter: Some(active_param as u32),
})
}
fn extract_params_from_signature(detail: &str) -> Vec<(String, String)> {
let open = match detail.find('(') {
Some(i) => i,
None => return vec![],
};
let close = match detail.find(')') {
Some(i) => i,
None => return vec![],
};
if close <= open + 1 {
return vec![];
}
let params_str = &detail[open + 1..close];
params_str
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let mut split = part.splitn(2, ':');
let name = split.next()?.trim().to_string();
let ty = split.next().map(|s| s.trim().to_string()).unwrap_or_else(|| "auto".to_string());
Some((name, ty))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::DocumentState;
fn make_doc(source: &str) -> DocumentState {
DocumentState::new(source.to_string(), 1)
}
#[test]
fn signature_help_returns_none_without_call() {
let doc = make_doc("## Main\n Let x be 5.\n");
let pos = Position { line: 1, character: 10 };
let result = signature_help(&doc, pos);
assert!(result.is_none(), "Should return None when not in a Call expression");
}
#[test]
fn signature_help_no_crash_empty_doc() {
let doc = make_doc("");
let pos = Position { line: 0, character: 0 };
let result = signature_help(&doc, pos);
assert!(result.is_none());
}
#[test]
fn signature_help_no_crash_on_out_of_bounds() {
let doc = make_doc("## Main\n Let x be 5.\n");
let pos = Position { line: 5, character: 0 };
let result = signature_help(&doc, pos);
assert!(result.is_none(), "OOB should return None");
}
#[test]
fn extract_params_basic() {
let params = extract_params_from_signature("To add(a: Int, b: Int) -> Int");
assert_eq!(params.len(), 2);
assert_eq!(params[0], ("a".to_string(), "Int".to_string()));
assert_eq!(params[1], ("b".to_string(), "Int".to_string()));
}
#[test]
fn extract_params_multiple() {
let params = extract_params_from_signature("To greet(name: Text, age: Int, loud: Bool) -> Text");
assert_eq!(params.len(), 3);
assert_eq!(params[0].0, "name");
assert_eq!(params[1].0, "age");
assert_eq!(params[2].0, "loud");
}
#[test]
fn extract_params_empty() {
let params = extract_params_from_signature("To noop() -> Unit");
assert!(params.is_empty());
}
#[test]
fn extract_params_no_parens() {
let params = extract_params_from_signature("something without parens");
assert!(params.is_empty());
}
#[test]
fn signature_help_returns_signature_for_defined_function() {
let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
let doc = make_doc(source);
let pos = Position { line: 4, character: 30 };
let result = signature_help(&doc, pos);
if let Some(help) = &result {
assert!(!help.signatures.is_empty(), "Should have a signature");
let sig = &help.signatures[0];
if let Some(params) = &sig.parameters {
let names: Vec<&str> = params
.iter()
.map(|p| match &p.label {
ParameterLabel::Simple(s) => s.as_str(),
_ => "",
})
.collect();
assert!(names.contains(&"a"), "Should include param 'a': {:?}", names);
assert!(names.contains(&"b"), "Should include param 'b': {:?}", names);
}
}
}
#[test]
fn active_parameter_tracking() {
let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
let doc = make_doc(source);
let pos = Position { line: 4, character: 35 };
if let Some(help) = signature_help(&doc, pos) {
let active = help.active_parameter.unwrap_or(0);
assert!(active >= 1, "After 'and' separator, active_parameter should be >= 1, got {}", active);
}
}
#[test]
fn call_not_found_returns_none() {
let doc = make_doc("## Main\n Let x be 5.\n");
let pos = Position { line: 1, character: 4 };
let result = signature_help(&doc, pos);
assert!(result.is_none(), "Should return None when no Call precedes position");
}
#[test]
fn with_not_counted_as_separator() {
let source = "## To add with a: Int and b: Int\n Show a.\n\n## Main\n Let r be Call add with 1 and 2.\n";
let doc = make_doc(source);
let pos = Position { line: 4, character: 27 };
if let Some(help) = signature_help(&doc, pos) {
let active = help.active_parameter.unwrap_or(99);
assert_eq!(active, 0, "Before 'and', active_parameter should be 0 (with not counted), got {}", active);
}
}
#[test]
fn signature_help_finds_function_via_span_not_pointer() {
let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
let doc = make_doc(source);
let pos = Position { line: 4, character: 30 };
let result = signature_help(&doc, pos);
if let Some(help) = &result {
assert!(!help.signatures.is_empty(), "Should have at least one signature");
}
}
}