use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::completion::named_args::{
extract_call_expression, find_enclosing_open_paren, position_to_char_offset,
split_args_top_level,
};
use crate::php_type::PhpType;
use crate::symbol_map::SymbolMap;
use crate::types::*;
use crate::util::position_to_offset;
struct CallSiteContext {
call_expression: String,
active_parameter: u32,
}
fn detect_call_site_from_map(
symbol_map: &SymbolMap,
content: &str,
position: Position,
) -> Option<CallSiteContext> {
let cursor_byte_offset = position_to_offset(content, position);
let cs = symbol_map.find_enclosing_call_site(cursor_byte_offset)?;
let active = cs
.comma_offsets
.iter()
.filter(|&&comma| comma < cursor_byte_offset)
.count() as u32;
Some(CallSiteContext {
call_expression: cs.call_expression.clone(),
active_parameter: active,
})
}
fn detect_call_site_text_fallback(content: &str, position: Position) -> Option<CallSiteContext> {
let chars: Vec<char> = content.chars().collect();
let cursor = position_to_char_offset(&chars, position)?;
let open_paren = find_enclosing_open_paren(&chars, cursor)?;
if is_function_definition_paren(&chars, open_paren) {
return None;
}
let call_expr = extract_call_expression(&chars, open_paren)?;
if call_expr.is_empty() {
return None;
}
let args_text: String = chars[open_paren + 1..cursor].iter().collect();
let segments = split_args_top_level(&args_text);
let trimmed = args_text.trim_end();
let active = if trimmed.is_empty() {
0
} else if trimmed.ends_with(',') {
segments.len() as u32
} else {
count_top_level_commas(&chars, open_paren + 1, cursor)
};
Some(CallSiteContext {
call_expression: call_expr,
active_parameter: active,
})
}
fn count_top_level_commas(chars: &[char], start: usize, end: usize) -> u32 {
let mut count = 0u32;
let mut depth = 0i32;
let mut i = start;
while i < end {
match chars[i] {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
',' if depth == 0 => count += 1,
'\'' | '"' => {
let q = chars[i];
i += 1;
while i < end {
if chars[i] == q {
let mut backslashes = 0u32;
let mut k = i;
while k > start && chars[k - 1] == '\\' {
backslashes += 1;
k -= 1;
}
if backslashes.is_multiple_of(2) {
break;
}
}
i += 1;
}
}
_ => {}
}
i += 1;
}
count
}
fn format_param_label(param: &ParameterInfo) -> String {
let mut parts = Vec::new();
if let Some(ref th) = param.native_type_hint {
parts.push(th.to_string());
}
if param.is_variadic {
parts.push(format!("...{}", param.name));
} else if param.is_reference {
parts.push(format!("&{}", param.name));
} else {
parts.push(param.name.clone());
}
let base = parts.join(" ");
if !param.is_required
&& !param.is_variadic
&& let Some(ref dv) = param.default_value
{
return format!("{} = {}", base, dv);
}
base
}
fn build_param_documentation(param: &ParameterInfo) -> Option<Documentation> {
let native = param.native_type_hint.as_ref();
let desc = param.description.as_deref();
let show_effective = match (¶m.type_hint, native) {
(Some(eff), Some(nat)) => !eff.equivalent(nat),
(Some(_), None) => true,
_ => false,
};
let shortened = param.type_hint.as_ref().map(crate::hover::shorten_php_type);
let value = match (show_effective, desc) {
(true, Some(d)) => format!("`{}` {}", shortened.as_deref().unwrap_or(""), d),
(true, None) => format!("`{}`", shortened.as_deref().unwrap_or("")),
(false, Some(d)) => d.to_string(),
(false, None) => return None,
};
Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value,
}))
}
fn build_signature(
params: &[ParameterInfo],
return_type: Option<&PhpType>,
) -> SignatureInformation {
let param_labels: Vec<String> = params.iter().map(format_param_label).collect();
let params_str = param_labels.join(", ");
let ret = format!(
": {}",
return_type.map_or_else(
|| PhpType::mixed().to_string(),
crate::hover::shorten_php_type
)
);
let label = format!("({}){}", params_str, ret);
let mut param_infos = Vec::with_capacity(params.len());
let mut offset = 1;
for (idx, (pl, param)) in param_labels.iter().zip(params.iter()).enumerate() {
let start = offset as u32;
let end = (offset + pl.len()) as u32;
param_infos.push(ParameterInformation {
label: ParameterLabel::LabelOffsets([start, end]),
documentation: build_param_documentation(param),
});
offset += pl.len();
if idx < param_labels.len() - 1 {
offset += 2; }
}
SignatureInformation {
label,
documentation: None,
parameters: Some(param_infos),
active_parameter: None,
}
}
struct ResolvedCallable {
parameters: Vec<ParameterInfo>,
return_type: Option<PhpType>,
}
impl From<crate::types::ResolvedCallableTarget> for ResolvedCallable {
fn from(t: crate::types::ResolvedCallableTarget) -> Self {
Self {
parameters: t.parameters,
return_type: t.return_type,
}
}
}
impl Backend {
pub(crate) fn handle_signature_help(
&self,
uri: &str,
content: &str,
position: Position,
) -> Option<SignatureHelp> {
let ctx = self.file_context(uri);
let symbol_map = self.symbol_maps.read().get(uri).cloned();
if let Some(ref sm) = symbol_map {
let cursor_offset = position_to_offset(content, position);
if let Some(call) = sm.find_enclosing_call_site(cursor_offset)
&& sm.is_inside_nested_scope_of_call(cursor_offset, call)
{
return None;
}
}
if let Some(ref sm) = symbol_map
&& let Some(site) = detect_call_site_from_map(sm, content, position)
&& let Some(result) = self.resolve_signature(&site, content, position, &ctx)
{
return Some(result);
}
if let Some(site) = detect_call_site_text_fallback(content, position) {
if let Some(result) = self.resolve_signature(&site, content, position, &ctx) {
return Some(result);
}
let patched = Self::patch_content_for_signature(content, position);
if patched != content {
let patched_classes: Vec<Arc<crate::types::ClassInfo>> =
self.parse_php(&patched).into_iter().map(Arc::new).collect();
if !patched_classes.is_empty() {
let patched_ctx = FileContext {
classes: patched_classes,
use_map: ctx.use_map.clone(),
namespace: ctx.namespace.clone(),
resolved_names: ctx.resolved_names.clone(),
};
if let Some(result) =
self.resolve_signature(&site, &patched, position, &patched_ctx)
{
return Some(result);
}
}
}
}
None
}
fn resolve_signature(
&self,
site: &CallSiteContext,
content: &str,
position: Position,
ctx: &FileContext,
) -> Option<SignatureHelp> {
let resolved = self.resolve_callable(&site.call_expression, content, position, ctx)?;
let sig = build_signature(&resolved.parameters, resolved.return_type.as_ref());
Some(SignatureHelp {
signatures: vec![sig],
active_signature: Some(0),
active_parameter: Some(clamp_active_param(
site.active_parameter,
&resolved.parameters,
)),
})
}
fn resolve_callable(
&self,
expr: &str,
content: &str,
position: Position,
ctx: &FileContext,
) -> Option<ResolvedCallable> {
self.resolve_callable_target(expr, content, position, ctx)
.map(ResolvedCallable::from)
}
pub(crate) fn extract_callable_target_from_variable(
var_name: &str,
content: &str,
cursor_offset: u32,
) -> Option<String> {
let search_area = content.get(..cursor_offset as usize)?;
let assign_prefix = format!("{} = ", var_name);
let assign_pos = search_area.rfind(&assign_prefix)?;
let rhs_start = assign_pos + assign_prefix.len();
let remaining = &content[rhs_start..];
let semi_pos = remaining.find(';')?;
let rhs_text = remaining[..semi_pos].trim();
let callable_text = rhs_text.strip_suffix("(...)")?.trim_end();
if callable_text.is_empty() {
return None;
}
Some(callable_text.to_string())
}
fn patch_content_for_signature(content: &str, position: Position) -> String {
let line_idx = position.line as usize;
let col = position.character as usize;
let mut result = String::with_capacity(content.len() + 2);
for (i, line) in content.lines().enumerate() {
if i == line_idx {
let byte_col = line
.char_indices()
.nth(col)
.map(|(idx, _)| idx)
.unwrap_or(line.len());
result.push_str(&line[..byte_col]);
result.push_str(");");
result.push_str(&line[byte_col..]);
} else {
result.push_str(line);
}
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
}
fn clamp_active_param(active: u32, params: &[ParameterInfo]) -> u32 {
if params.is_empty() {
return 0;
}
let last = (params.len() - 1) as u32;
active.min(last)
}
fn is_function_definition_paren(chars: &[char], paren_pos: usize) -> bool {
let mut i = paren_pos;
while i > 0 && chars[i - 1].is_ascii_whitespace() {
i -= 1;
}
let name_end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
let name: String = chars[i..name_end].iter().collect();
if name == "function" || name == "fn" {
return true;
}
let mut j = i;
while j > 0 && chars[j - 1].is_ascii_whitespace() {
j -= 1;
}
if ends_with_keyword(chars, j, "function") || ends_with_keyword(chars, j, "fn") {
return true;
}
false
}
fn ends_with_keyword(chars: &[char], pos: usize, keyword: &str) -> bool {
let kw_len = keyword.len();
if pos < kw_len {
return false;
}
let start = pos - kw_len;
let candidate: String = chars[start..pos].iter().collect();
if candidate != keyword {
return false;
}
if start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') {
return false;
}
true
}
#[cfg(test)]
#[path = "signature_help_tests.rs"]
mod tests;