use tower_lsp::lsp_types::*;
#[derive(Debug, Clone)]
pub struct NamedArgContext {
pub call_expression: String,
pub existing_named_args: Vec<String>,
pub positional_count: usize,
pub prefix: String,
}
pub fn detect_named_arg_context(content: &str, position: Position) -> Option<NamedArgContext> {
let chars: Vec<char> = content.chars().collect();
let cursor = position_to_char_offset(&chars, position)?;
let mut word_start = cursor;
while word_start > 0
&& (chars[word_start - 1].is_alphanumeric() || chars[word_start - 1] == '_')
{
word_start -= 1;
}
if word_start > 0 && chars[word_start - 1] == '$' {
return None;
}
if word_start >= 2 && chars[word_start - 2] == '-' && chars[word_start - 1] == '>' {
return None;
}
if word_start >= 2 && chars[word_start - 2] == ':' && chars[word_start - 1] == ':' {
return None;
}
let prefix: String = chars[word_start..cursor].iter().collect();
let open_paren = find_enclosing_open_paren(&chars, word_start)?;
let call_expr = extract_call_expression(&chars, open_paren)?;
if call_expr.is_empty() {
return None;
}
let args_text: String = chars[open_paren + 1..word_start].iter().collect();
let (existing_named, positional_count) = parse_existing_args(&args_text);
Some(NamedArgContext {
call_expression: call_expr,
existing_named_args: existing_named,
positional_count,
prefix,
})
}
pub use crate::util::position_to_char_offset;
pub fn find_enclosing_open_paren(chars: &[char], start: usize) -> Option<usize> {
let mut i = start;
let mut depth: i32 = 0;
while i > 0 {
i -= 1;
match chars[i] {
')' => depth += 1,
'(' => {
if depth > 0 {
depth -= 1;
} else {
return Some(i);
}
}
'\'' => {
i = skip_string_backward(chars, i, '\'');
}
'"' => {
i = skip_string_backward(chars, i, '"');
}
'{' | '[' => return None,
';' => return None,
_ => {}
}
}
None
}
pub fn skip_string_backward(chars: &[char], end: usize, q: char) -> usize {
if end == 0 {
return 0;
}
let mut j = end - 1;
while j > 0 {
if chars[j] == q {
let mut backslashes = 0u32;
let mut k = j;
while k > 0 && chars[k - 1] == '\\' {
backslashes += 1;
k -= 1;
}
if backslashes.is_multiple_of(2) {
return j;
}
}
j -= 1;
}
0
}
pub fn extract_call_expression(chars: &[char], open: usize) -> Option<String> {
if open == 0 {
return None;
}
let mut i = open;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
if i == 0 {
return None;
}
if chars[i - 1] == ')' {
return None;
}
let ident_end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\') {
i -= 1;
}
if i == ident_end {
return None;
}
let ident: String = chars[i..ident_end].iter().collect();
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
let subject = extract_subject_before_arrow(chars, i - 2);
if !subject.is_empty() {
return Some(format!("{}->{}", subject, ident));
}
return None;
}
if i >= 3 && chars[i - 3] == '?' && chars[i - 2] == '-' && chars[i - 1] == '>' {
let subject = extract_subject_before_arrow(chars, i - 3);
if !subject.is_empty() {
return Some(format!("{}->{}", subject, ident));
}
return None;
}
if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' {
let class_name = extract_class_name_backward(chars, i - 2);
if !class_name.is_empty() {
return Some(format!("{}::{}", class_name, ident));
}
return None;
}
let mut j = i;
while j > 0 && chars[j - 1] == ' ' {
j -= 1;
}
if j >= 3 && chars[j - 3] == 'n' && chars[j - 2] == 'e' && chars[j - 1] == 'w' {
let before_ok = j == 3 || { !chars[j - 4].is_alphanumeric() && chars[j - 4] != '_' };
if before_ok {
return Some(format!("new {}", ident));
}
}
Some(ident)
}
pub fn extract_subject_before_arrow(chars: &[char], arrow_pos: usize) -> String {
let mut i = arrow_pos;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
if i > 0 && chars[i - 1] == ')' {
return String::new();
}
let end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
return chars[i..end].iter().collect();
}
chars[i..end].iter().collect()
}
pub fn extract_class_name_backward(chars: &[char], colon_pos: usize) -> String {
let mut i = colon_pos;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
let end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\') {
i -= 1;
}
chars[i..end].iter().collect()
}
pub fn parse_existing_args(args_text: &str) -> (Vec<String>, usize) {
let mut named = Vec::new();
let mut positional = 0usize;
let args = split_args_top_level(args_text);
for arg in &args {
let trimmed = arg.trim();
if trimmed.is_empty() {
continue;
}
if let Some(name) = extract_named_arg_name(trimmed) {
named.push(name);
} else {
positional += 1;
}
}
(named, positional)
}
pub fn split_args_top_level(text: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut depth = 0i32;
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'(' | '[' => {
depth += 1;
current.push(chars[i]);
}
')' | ']' => {
depth -= 1;
current.push(chars[i]);
}
',' if depth == 0 => {
args.push(std::mem::take(&mut current));
}
'\'' | '"' => {
let q = chars[i];
current.push(q);
i += 1;
while i < chars.len() {
current.push(chars[i]);
if chars[i] == q {
let mut backslashes = 0u32;
let mut k = current.len() - 1;
while k > 0 && current.as_bytes()[k - 1] == b'\\' {
backslashes += 1;
k -= 1;
}
if backslashes.is_multiple_of(2) {
break;
}
}
i += 1;
}
}
_ => current.push(chars[i]),
}
i += 1;
}
if !current.trim().is_empty() {
args.push(current);
}
args
}
pub fn extract_named_arg_name(arg: &str) -> Option<String> {
let chars: Vec<char> = arg.chars().collect();
let mut i = 0;
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
if i == start {
return None;
}
if i < chars.len() && chars[i] == ':' {
if i + 1 < chars.len() && chars[i + 1] == ':' {
return None;
}
let name: String = chars[start..i].iter().collect();
return Some(name);
}
None
}
pub fn build_named_arg_completions(
ctx: &NamedArgContext,
parameters: &[crate::types::ParameterInfo],
) -> Vec<CompletionItem> {
let mut items = Vec::new();
let prefix_lower = ctx.prefix.to_lowercase();
for (idx, param) in parameters.iter().enumerate() {
let bare_name = param.name.strip_prefix('$').unwrap_or(¶m.name);
if idx < ctx.positional_count {
continue;
}
if ctx.existing_named_args.iter().any(|n| n == bare_name) {
continue;
}
if !bare_name.to_lowercase().starts_with(&prefix_lower) {
continue;
}
let label = if let Some(ref th) = param.type_hint {
format!("{}: {}", bare_name, th)
} else {
format!("{}:", bare_name)
};
let insert = format!("{}: ", bare_name);
let detail = if param.is_variadic {
Some("Named argument (variadic)".to_string())
} else if !param.is_required {
Some("Named argument (optional)".to_string())
} else {
Some("Named argument".to_string())
};
items.push(CompletionItem {
label,
kind: Some(CompletionItemKind::VARIABLE),
detail,
insert_text: Some(insert),
filter_text: Some(bare_name.to_string()),
sort_text: Some(format!("0_{:03}", idx)),
..CompletionItem::default()
});
}
items
}