Skip to content

Commit 85277cd

Browse files
committed
Fix compleatino inside calls
1 parent a3a3e20 commit 85277cd

4 files changed

Lines changed: 295 additions & 25 deletions

File tree

example.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public function demo(): void
147147
{
148148
/** @var Pencil $inlineHinted */
149149
$inlineHinted = getUnknownValue();
150-
$inlineHinted->sketch(); // with explicit variable name
150+
$inlineHinted->sketch();
151151

152152
/** @var Pen */
153153
$hinted = getUnknownValue();

src/completion/handler.rs

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,41 @@ use crate::Backend;
3838
use crate::completion::class_completion::{
3939
ClassCompletionParams, ClassNameContext, detect_class_name_context, is_class_declaration_name,
4040
};
41-
use crate::completion::named_args::{NamedArgContext, parse_existing_args};
41+
use crate::completion::named_args::{
42+
NamedArgContext, cursor_inside_nested_bracket, parse_existing_args,
43+
};
4244
use crate::docblock::types::PHPDOC_TYPE_KEYWORDS;
4345
use crate::php_type::PhpType;
4446
use crate::symbol_map::SymbolKind;
4547
use crate::types::{ClassInfo, ResolvedType};
4648
use crate::types::{CompletionTarget, FileContext};
4749
use crate::util::{find_class_at_offset, position_to_byte_offset, position_to_offset};
4850

51+
/// Append named-argument items into an existing [`CompletionResponse`].
52+
///
53+
/// If `named_arg_items` is empty the response is returned unchanged.
54+
/// Otherwise the items are appended to the response's item list,
55+
/// preserving the `is_incomplete` flag when the response is a
56+
/// [`CompletionList`].
57+
fn merge_named_args_into_response(
58+
response: CompletionResponse,
59+
named_arg_items: Vec<CompletionItem>,
60+
) -> CompletionResponse {
61+
if named_arg_items.is_empty() {
62+
return response;
63+
}
64+
match response {
65+
CompletionResponse::Array(mut items) => {
66+
items.extend(named_arg_items);
67+
CompletionResponse::Array(items)
68+
}
69+
CompletionResponse::List(mut list) => {
70+
list.items.extend(named_arg_items);
71+
CompletionResponse::List(list)
72+
}
73+
}
74+
}
75+
4976
/// Check whether a `(` immediately follows the cursor position (past any
5077
/// partial identifier the user has already typed).
5178
///
@@ -296,10 +323,12 @@ impl Backend {
296323
return Ok(self.complete_type_hint(&content, &th_ctx, &ctx, position, &uri));
297324
}
298325

299-
// ── Named argument completion ───────────────────────────
300-
if let Some(response) = self.try_named_arg_completion(&uri, &content, position, &ctx) {
301-
return Ok(Some(response));
302-
}
326+
// ── Named argument completion (collected, not short-circuited) ──
327+
// Named arg items are always valid alongside normal
328+
// completions, so collect them here and merge them into
329+
// whatever strategy wins below.
330+
let named_arg_items =
331+
self.collect_named_arg_items(&uri, &content, position, &ctx);
303332

304333
// ── String context detection ────────────────────────────
305334
// Classify once and use throughout the remaining pipeline.
@@ -368,7 +397,7 @@ impl Backend {
368397

369398
// ── Smart catch clause completion ───────────────────────
370399
if let Some(response) = self.try_catch_completion(&content, position, &ctx, &uri) {
371-
return Ok(Some(response));
400+
return Ok(Some(merge_named_args_into_response(response, named_arg_items)));
372401
}
373402

374403
// ── `throw new` completion ──────────────────────────────
@@ -388,7 +417,12 @@ impl Backend {
388417
if let Some(response) =
389418
self.try_class_constant_function_completion(&content, position, &ctx, &uri)
390419
{
391-
return Ok(Some(response));
420+
return Ok(Some(merge_named_args_into_response(response, named_arg_items)));
421+
}
422+
423+
// No strategy matched, but we may still have named arg items.
424+
if !named_arg_items.is_empty() {
425+
return Ok(Some(CompletionResponse::Array(named_arg_items)));
392426
}
393427
}
394428

@@ -671,28 +705,33 @@ impl Backend {
671705

672706
// ─── Strategy: named argument completion ─────────────────────────────
673707

674-
/// Try to offer `name:` argument completions inside function/method
708+
/// Collect `name:` argument completion items inside function/method
675709
/// call parentheses.
676710
///
677-
/// Returns `None` when the cursor is not in a named-argument context
678-
/// or when no parameters could be resolved.
679-
fn try_named_arg_completion(
711+
/// Returns an empty `Vec` when the cursor is not in a named-argument
712+
/// context or when no parameters could be resolved. The items are
713+
/// meant to be **merged** into whatever other completion strategy
714+
/// wins — named args are always valid alongside normal completions.
715+
fn collect_named_arg_items(
680716
&self,
681717
uri: &str,
682718
content: &str,
683719
position: Position,
684720
ctx: &FileContext,
685-
) -> Option<CompletionResponse> {
721+
) -> Vec<CompletionItem> {
686722
// ── Primary path: AST-based detection via symbol map ────────
687723
// The symbol map's `CallSite` data handles chains, nesting,
688724
// and strings correctly. Fall back to text scanning when the
689725
// AST has no hit (typically because the parser couldn't recover
690726
// from incomplete code).
691-
let na_ctx = self
727+
let na_ctx = match self
692728
.detect_named_arg_from_symbol_map(uri, content, position)
693729
.or_else(|| {
694730
crate::completion::named_args::detect_named_arg_context(content, position)
695-
})?;
731+
}) {
732+
Some(ctx) => ctx,
733+
None => return Vec::new(),
734+
};
696735

697736
let mut params = self.resolve_named_arg_params(&na_ctx, content, position, ctx);
698737

@@ -719,16 +758,7 @@ impl Backend {
719758
}
720759
}
721760

722-
if params.is_empty() {
723-
return None;
724-
}
725-
726-
let items = crate::completion::named_args::build_named_arg_completions(&na_ctx, &params);
727-
if items.is_empty() {
728-
None
729-
} else {
730-
Some(CompletionResponse::Array(items))
731-
}
761+
crate::completion::named_args::build_named_arg_completions(&na_ctx, &params)
732762
}
733763

734764
/// Detect a named-argument context using precomputed [`CallSite`] data
@@ -749,6 +779,16 @@ impl Backend {
749779
let cursor_byte_offset = position_to_offset(content, position);
750780
let cs = symbol_map.find_enclosing_call_site(cursor_byte_offset)?;
751781

782+
// ── Bail out when cursor is inside a nested `[…]` or `{…}` ─
783+
// If the cursor sits inside an array literal or braced
784+
// expression that is itself an argument, named-arg completion
785+
// for the outer call must not fire — the user wants normal
786+
// value completion, not parameter names.
787+
if cursor_inside_nested_bracket(content, cs.args_start as usize, cursor_byte_offset as usize)
788+
{
789+
return None;
790+
}
791+
752792
// ── Check eligibility at cursor ─────────────────────────────
753793
// Walk backward from cursor through identifier chars to find the
754794
// start of the current "word" in the raw source text.

src/completion/named_args.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,60 @@
2020
2121
use tower_lsp::lsp_types::*;
2222

23+
// ─── Shared helpers ─────────────────────────────────────────────────────────
24+
25+
/// Check whether `cursor` (byte offset) sits inside a `[…]` or `{…}` pair
26+
/// that is nested within the call's argument span starting at `args_start`.
27+
///
28+
/// Scans forward from `args_start` to `cursor`, tracking bracket and brace
29+
/// depth while skipping string literals. Returns `true` when the net depth
30+
/// is > 0, meaning the cursor is inside an array literal or braced
31+
/// expression — not at the top-level argument list of the call.
32+
pub fn cursor_inside_nested_bracket(content: &str, args_start: usize, cursor: usize) -> bool {
33+
let bytes = content.as_bytes();
34+
let end = cursor.min(bytes.len());
35+
let mut i = args_start;
36+
let mut bracket_depth: i32 = 0; // tracks [ ]
37+
let mut brace_depth: i32 = 0; // tracks { }
38+
39+
while i < end {
40+
match bytes[i] {
41+
b'[' => bracket_depth += 1,
42+
b']' => bracket_depth -= 1,
43+
b'{' => brace_depth += 1,
44+
b'}' => brace_depth -= 1,
45+
// Skip single-quoted strings
46+
b'\'' => {
47+
i += 1;
48+
while i < end {
49+
if bytes[i] == b'\\' {
50+
i += 1; // skip escaped char
51+
} else if bytes[i] == b'\'' {
52+
break;
53+
}
54+
i += 1;
55+
}
56+
}
57+
// Skip double-quoted strings
58+
b'"' => {
59+
i += 1;
60+
while i < end {
61+
if bytes[i] == b'\\' {
62+
i += 1; // skip escaped char
63+
} else if bytes[i] == b'"' {
64+
break;
65+
}
66+
i += 1;
67+
}
68+
}
69+
_ => {}
70+
}
71+
i += 1;
72+
}
73+
74+
bracket_depth > 0 || brace_depth > 0
75+
}
76+
2377
// ─── Context ────────────────────────────────────────────────────────────────
2478

2579
/// Information about a named-argument completion context.

0 commit comments

Comments
 (0)