Skip to content

Commit 69c913e

Browse files
committed
Fix some keyword suggestions
1 parent b6ee347 commit 69c913e

3 files changed

Lines changed: 200 additions & 7 deletions

File tree

docs/todo/completion.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,50 @@ This is a small quality-of-life improvement: deprecated classes would
360360
show with a strikethrough in the completion menu across all sources,
361361
not just same-namespace ones.
362362

363-
## C11. Smarter member ordering after `->` / `::`
363+
## C11. PSR-4 namespace completion
364+
365+
**Impact: Medium · Effort: Low**
366+
367+
When the user types `namespace ` at the top of a file, suggest the
368+
fully qualified namespace derived from the file's path and the
369+
project's PSR-4 autoload mapping. For example, typing `namespace T`
370+
in `tests/Unit/Reorder/SuspendScheduleTest.php` should offer
371+
`Tests\Unit\Reorder` as a completion item that inserts the full
372+
namespace followed by a semicolon.
373+
374+
The classmap scanner already knows every PSR-4 prefix-to-directory
375+
mapping, and the class-declaration-name completion already infers a
376+
class name from the filename. The namespace case is the directory
377+
counterpart: strip the filename, walk the remaining path segments
378+
back to the PSR-4 root, and prepend the configured namespace prefix.
379+
380+
**Implementation:**
381+
382+
1. **Detect `namespace` context** — the keyword context already
383+
identifies `ClassNameContext::NamespaceDeclaration`. The handler
384+
currently delegates to `build_namespace_completions` which returns
385+
existing namespace names from the index. Add a new code path (or
386+
augment the existing one) that also computes the PSR-4-derived
387+
namespace for the current file.
388+
389+
2. **Compute the expected namespace** — given the current file's URI
390+
and the project's PSR-4 map (`autoload_map`), find the matching
391+
prefix, strip it from the file path, convert directory separators
392+
to `\`, and drop the filename. This is essentially
393+
`filename_class_name` but for directories.
394+
395+
3. **Build the completion item** — offer the computed namespace with
396+
high sort priority (above existing namespace names from the
397+
index). Insert text should include the trailing `;`. If the
398+
computed namespace matches what the user has already partially
399+
typed, filter normally.
400+
401+
Intelephense already does this, so users migrating from it will
402+
expect the behavior.
403+
404+
---
405+
406+
## C12. Smarter member ordering after `->` / `::`
364407

365408
**Impact: Medium · Effort: needs planning**
366409

src/completion/context/type_hint_completion.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,33 @@ pub(crate) fn detect_type_hint_context(
160160
if partial.is_empty() {
161161
return None;
162162
}
163-
// Make sure the partial is NOT a keyword that forms part of a
164-
// function declaration (e.g. `public function`). If the user
165-
// has typed `function` or `fn` we should not offer type hints.
166-
let partial_lower = partial.to_lowercase();
167-
if partial_lower == "function" || partial_lower == "fn" {
168-
return None;
163+
// If the partial starts with a lowercase letter and is a prefix
164+
// of any member-declaration keyword, defer to keyword completion
165+
// so that e.g. `public fu` offers `function` rather than type
166+
// hints. Partials starting with an uppercase letter (e.g. `Us`,
167+
// `User`) are type names, not keywords, so they skip this check.
168+
if partial.starts_with(|c: char| c.is_ascii_lowercase()) {
169+
const MEMBER_DECLARATION_KEYWORDS: &[&str] = &[
170+
"function",
171+
"fn",
172+
"const",
173+
"static",
174+
"final",
175+
"abstract",
176+
"readonly",
177+
"public",
178+
"protected",
179+
"private",
180+
"use",
181+
"class",
182+
];
183+
let partial_lower = partial.to_lowercase();
184+
let is_keyword_prefix = MEMBER_DECLARATION_KEYWORDS
185+
.iter()
186+
.any(|kw| kw.starts_with(partial_lower.as_str()));
187+
if is_keyword_prefix {
188+
return None;
189+
}
169190
}
170191
return Some(TypeHintContext {
171192
partial,

src/completion/context/type_hint_completion_tests.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,135 @@ fn partial_is_function_keyword_after_modifier() {
276276
assert!(ctx.is_none());
277277
}
278278

279+
// ── Partial keyword prefixes after modifiers ────────────────────
280+
281+
#[test]
282+
fn partial_fu_after_public_is_not_type_hint() {
283+
// `public fu` — "fu" is a prefix of "function", should defer to keyword completion.
284+
let src = "<?php\nclass Foo {\n public fu\n}";
285+
let ctx = detect(src, 2, 13);
286+
assert!(
287+
ctx.is_none(),
288+
"partial 'fu' is a prefix of 'function' and should not trigger type hints"
289+
);
290+
}
291+
292+
#[test]
293+
fn partial_fun_after_public_is_not_type_hint() {
294+
let src = "<?php\nclass Foo {\n public fun\n}";
295+
let ctx = detect(src, 2, 14);
296+
assert!(
297+
ctx.is_none(),
298+
"partial 'fun' is a prefix of 'function' and should not trigger type hints"
299+
);
300+
}
301+
302+
#[test]
303+
fn partial_func_after_public_is_not_type_hint() {
304+
let src = "<?php\nclass Foo {\n public func\n}";
305+
let ctx = detect(src, 2, 15);
306+
assert!(
307+
ctx.is_none(),
308+
"partial 'func' is a prefix of 'function' and should not trigger type hints"
309+
);
310+
}
311+
312+
#[test]
313+
fn partial_con_after_public_is_not_type_hint() {
314+
// `public con` — "con" is a prefix of "const", should defer to keyword completion.
315+
let src = "<?php\nclass Foo {\n public con\n}";
316+
let ctx = detect(src, 2, 14);
317+
assert!(
318+
ctx.is_none(),
319+
"partial 'con' is a prefix of 'const' and should not trigger type hints"
320+
);
321+
}
322+
323+
#[test]
324+
fn partial_st_after_public_is_not_type_hint() {
325+
// `public st` — "st" is a prefix of "static", should defer to keyword completion.
326+
let src = "<?php\nclass Foo {\n public st\n}";
327+
let ctx = detect(src, 2, 13);
328+
assert!(
329+
ctx.is_none(),
330+
"partial 'st' is a prefix of 'static' and should not trigger type hints"
331+
);
332+
}
333+
334+
#[test]
335+
fn partial_f_after_public_is_not_type_hint() {
336+
// `public f` — "f" is a prefix of "function", "fn", "final".
337+
let src = "<?php\nclass Foo {\n public f\n}";
338+
let ctx = detect(src, 2, 12);
339+
assert!(
340+
ctx.is_none(),
341+
"partial 'f' is a prefix of 'function'/'fn'/'final' and should not trigger type hints"
342+
);
343+
}
344+
345+
#[test]
346+
fn partial_cl_after_readonly_at_top_level_is_not_type_hint() {
347+
// `readonly cl` — "cl" is a prefix of "class", should defer to keyword completion.
348+
let src = "<?php\nreadonly cl";
349+
let ctx = detect(src, 1, 12);
350+
assert!(
351+
ctx.is_none(),
352+
"partial 'cl' is a prefix of 'class' and should not trigger type hints"
353+
);
354+
}
355+
356+
#[test]
357+
fn partial_ab_after_public_is_not_type_hint() {
358+
// `public ab` — "ab" is a prefix of "abstract".
359+
let src = "<?php\nclass Foo {\n public ab\n}";
360+
let ctx = detect(src, 2, 13);
361+
assert!(
362+
ctx.is_none(),
363+
"partial 'ab' is a prefix of 'abstract' and should not trigger type hints"
364+
);
365+
}
366+
367+
#[test]
368+
fn partial_str_after_public_is_type_hint() {
369+
// `public str` — "str" is NOT a prefix of any declaration keyword,
370+
// so it should trigger type hints (e.g. offering `string`).
371+
let src = "<?php\nclass Foo {\n public str\n}";
372+
let ctx = detect(src, 2, 14).unwrap();
373+
assert_eq!(ctx.partial, "str");
374+
}
375+
376+
#[test]
377+
fn partial_us_after_public_is_type_hint() {
378+
// `public Us` — "Us" is not a prefix of any declaration keyword,
379+
// so it should trigger type hints (e.g. offering `UserService`).
380+
let src = "<?php\nclass Foo {\n public Us\n}";
381+
let ctx = detect(src, 2, 13).unwrap();
382+
assert_eq!(ctx.partial, "Us");
383+
}
384+
385+
#[test]
386+
fn partial_fn_after_public_is_not_type_hint() {
387+
// `public fn` — "fn" is an exact match for the keyword.
388+
let src = "<?php\nclass Foo {\n public fn\n}";
389+
let ctx = detect(src, 2, 13);
390+
assert!(
391+
ctx.is_none(),
392+
"partial 'fn' matches declaration keyword and should not trigger type hints"
393+
);
394+
}
395+
396+
#[test]
397+
fn partial_use_after_public_is_not_type_hint() {
398+
// Inside a class, `public use` — "use" is a declaration keyword (trait import).
399+
// Note: not valid PHP but the keyword check should still exclude it.
400+
let src = "<?php\nclass Foo {\n public use\n}";
401+
let ctx = detect(src, 2, 14);
402+
assert!(
403+
ctx.is_none(),
404+
"partial 'use' matches declaration keyword and should not trigger type hints"
405+
);
406+
}
407+
279408
// ── Native types constant ───────────────────────────────────────
280409

281410
#[test]

0 commit comments

Comments
 (0)