Skip to content

Commit e3eac17

Browse files
committed
Be more selective about when suggestions are return in comments
1 parent 66f3310 commit e3eac17

4 files changed

Lines changed: 2351 additions & 0 deletions

File tree

src/completion/handler.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@ use tower_lsp::lsp_types::*;
1616

1717
use crate::Backend;
1818

19+
/// PHP scalar and built-in types offered in docblock type positions.
20+
///
21+
/// These are prepended to class-name results so that typing `@param str`
22+
/// suggests `string` alongside any user-defined classes starting with `str`.
23+
const PHPDOC_SCALAR_TYPES: &[&str] = &[
24+
"string",
25+
"int",
26+
"float",
27+
"bool",
28+
"array",
29+
"object",
30+
"mixed",
31+
"void",
32+
"null",
33+
"callable",
34+
"iterable",
35+
"never",
36+
"self",
37+
"static",
38+
"parent",
39+
"true",
40+
"false",
41+
"resource",
42+
"class-string",
43+
"positive-int",
44+
"negative-int",
45+
"non-empty-string",
46+
"non-empty-array",
47+
"non-empty-list",
48+
"list",
49+
"numeric-string",
50+
];
51+
1952
impl Backend {
2053
/// Main completion handler — called by `LanguageServer::completion`.
2154
///
@@ -46,6 +79,15 @@ impl Backend {
4679
if let Some(content) = content {
4780
let classes = classes.unwrap_or_default();
4881

82+
// ── Suppress completion inside non-doc comments ─────────
83+
// When the cursor is inside a `//` line comment or a `/* … */`
84+
// block comment (but NOT a `/** … */` docblock), return no
85+
// completions — typing inside comments should not trigger
86+
// suggestions.
87+
if crate::completion::phpdoc::is_inside_non_doc_comment(&content, position) {
88+
return Ok(None);
89+
}
90+
4991
// Gather the current file's `use` statement mappings and namespace
5092
// so the class_loader can resolve short names like `Resource` to
5193
// their fully-qualified equivalents like `Klarna\Rest\Resource`.
@@ -85,6 +127,98 @@ impl Backend {
85127
return Ok(Some(CompletionResponse::Array(items)));
86128
}
87129

130+
// ── Docblock type / variable completion ─────────────────
131+
// When the cursor is inside a `/** … */` docblock at a
132+
// recognised tag position (e.g. after `@param `, `@return `,
133+
// `@throws `, `@var `, …), offer class-name or $variable
134+
// completions as appropriate. At all other docblock
135+
// positions (descriptions, unknown tags) suppress the
136+
// remaining strategies so random words don't trigger
137+
// class / variable suggestions.
138+
if crate::completion::phpdoc::is_inside_docblock(&content, position) {
139+
use crate::completion::phpdoc::{
140+
DocblockTypingContext, detect_docblock_typing_position, extract_symbol_info,
141+
};
142+
143+
match detect_docblock_typing_position(&content, position) {
144+
Some(DocblockTypingContext::Type { partial }) => {
145+
// Offer scalar / built-in types first, then class
146+
// / interface / enum names from the project.
147+
let partial_lower = partial.to_lowercase();
148+
let mut items: Vec<CompletionItem> = PHPDOC_SCALAR_TYPES
149+
.iter()
150+
.filter(|t| t.to_lowercase().starts_with(&partial_lower))
151+
.enumerate()
152+
.map(|(idx, t)| CompletionItem {
153+
label: t.to_string(),
154+
kind: Some(CompletionItemKind::KEYWORD),
155+
detail: Some("PHP built-in type".to_string()),
156+
insert_text: Some(t.to_string()),
157+
filter_text: Some(t.to_string()),
158+
sort_text: Some(format!("0_scalar_{:03}", idx)),
159+
..CompletionItem::default()
160+
})
161+
.collect();
162+
163+
let (class_items, class_incomplete) = self.build_class_name_completions(
164+
&file_use_map,
165+
&file_namespace,
166+
&partial,
167+
&content,
168+
false, // not a `new` context
169+
);
170+
items.extend(class_items);
171+
172+
if !items.is_empty() {
173+
return Ok(Some(CompletionResponse::List(CompletionList {
174+
is_incomplete: class_incomplete,
175+
items,
176+
})));
177+
}
178+
return Ok(None);
179+
}
180+
Some(DocblockTypingContext::Variable { partial }) => {
181+
// Offer $parameter names from the function declaration.
182+
let sym = extract_symbol_info(&content, position);
183+
let partial_lower = partial.to_lowercase();
184+
let items: Vec<CompletionItem> = sym
185+
.params
186+
.iter()
187+
.filter(|(_, name)| {
188+
partial_lower.is_empty()
189+
|| name.to_lowercase().starts_with(&partial_lower)
190+
})
191+
.map(|(type_hint, name)| {
192+
let detail = type_hint.as_deref().unwrap_or("mixed").to_string();
193+
// Always use the full `$name` as insert_text
194+
// — the LSP client replaces the typed prefix
195+
// (whether `$`, `$na`, or empty) with whatever
196+
// we provide, matching how regular variable
197+
// completion works in variable_completion.rs.
198+
CompletionItem {
199+
label: name.clone(),
200+
kind: Some(CompletionItemKind::VARIABLE),
201+
detail: Some(detail),
202+
insert_text: Some(name.clone()),
203+
filter_text: Some(name.clone()),
204+
sort_text: Some(format!("0_{}", name.to_lowercase())),
205+
..CompletionItem::default()
206+
}
207+
})
208+
.collect();
209+
if !items.is_empty() {
210+
return Ok(Some(CompletionResponse::Array(items)));
211+
}
212+
return Ok(None);
213+
}
214+
None => {
215+
// Description text or unrecognised position — no
216+
// completions.
217+
return Ok(None);
218+
}
219+
}
220+
}
221+
88222
// ── Named argument completion ───────────────────────────
89223
// When the cursor is inside the parentheses of a function or
90224
// method call, offer parameter names as `name:` completions.

0 commit comments

Comments
 (0)