Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/completion/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,18 @@ impl Backend {
/// (same-class access, e.g. `$this->`).
/// - `Some(name)` where `name != target_class.name`: **public** and
/// **protected** members are shown (the caller might be in a subclass).
///
/// `is_self_or_ancestor` should be `true` when the cursor is inside the
/// target class itself or inside a class that (transitively) extends the
/// target. When `true`, `__construct` is offered for `::` access
/// (e.g. `self::__construct()`, `parent::__construct()`,
/// `ClassName::__construct()` from within a subclass). When `false`,
/// magic methods are suppressed entirely.
pub(crate) fn build_completion_items(
target_class: &ClassInfo,
access_kind: AccessKind,
current_class_name: Option<&str>,
is_self_or_ancestor: bool,
) -> Vec<CompletionItem> {
// Determine whether we are inside the same class as the target.
let same_class = current_class_name.is_some_and(|name| name == target_class.name);
Expand All @@ -108,12 +116,13 @@ impl Backend {
// Methods — filtered by static / instance, excluding magic methods
for method in &target_class.methods {
// `__construct` is meaningful to call explicitly via `::` or
// `parent::` (e.g. `parent::__construct(...)` in a child class),
// so we only suppress it for `->` access. All other magic
// methods are always suppressed.
// `parent::` when inside the same class or a subclass
// (e.g. `parent::__construct(...)`, `self::__construct()`).
// Outside of that relationship, magic methods are suppressed.
let is_constructor = method.name.eq_ignore_ascii_case("__construct");
if Self::is_magic_method(&method.name) {
let allow = is_constructor
&& is_self_or_ancestor
&& matches!(
access_kind,
AccessKind::DoubleColon | AccessKind::ParentDoubleColon
Expand Down Expand Up @@ -235,6 +244,19 @@ impl Backend {
}
}

// `::class` keyword — returns the fully qualified class name as a string.
// Available on any class, interface, or enum via `::` access.
if access_kind == AccessKind::DoubleColon || access_kind == AccessKind::ParentDoubleColon {
items.push(CompletionItem {
label: "class".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("class-string".to_string()),
insert_text: Some("class".to_string()),
filter_text: Some("class".to_string()),
..CompletionItem::default()
});
}

// Sort all items alphabetically (case-insensitive) and assign
// sort_text so the editor preserves this ordering.
items.sort_by(|a, b| {
Expand Down
33 changes: 33 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,43 @@ impl LanguageServer for Backend {
for target_class in &candidates {
let merged =
Self::resolve_class_with_inheritance(target_class, &class_loader);

// Determine whether the cursor is inside the target
// class itself or inside a (transitive) subclass.
// This controls whether `__construct` is offered
// via `::` access.
let is_self_or_ancestor = if let Some(cc) = current_class {
if cc.name == target_class.name {
true
} else {
// Walk the parent chain of the current class
// to see if the target is an ancestor.
let mut ancestor_name = cc.parent_class.clone();
let mut found = false;
let mut depth = 0u32;
while let Some(ref name) = ancestor_name {
depth += 1;
if depth > 20 {
break;
}
if *name == target_class.name {
found = true;
break;
}
ancestor_name =
class_loader(name).and_then(|ci| ci.parent_class.clone());
}
found
}
} else {
false
};

let items = Self::build_completion_items(
&merged,
effective_access,
current_class_name,
is_self_or_ancestor,
);
for item in items {
if !all_items
Expand Down
Loading