Skip to content

Commit 1724da7

Browse files
committed
Add support for parent::
1 parent c75f02d commit 1724da7

5 files changed

Lines changed: 652 additions & 8 deletions

File tree

src/completion/builder.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
/// Completion item building.
22
///
33
/// This module contains the logic for constructing LSP `CompletionItem`s from
4-
/// resolved `ClassInfo`, filtered by the `AccessKind` (arrow vs double-colon).
4+
/// resolved `ClassInfo`, filtered by the `AccessKind` (arrow, double-colon,
5+
/// or parent double-colon).
56
use tower_lsp::lsp_types::*;
67

78
use crate::Backend;
9+
use crate::types::Visibility;
810
use crate::types::*;
911

1012
/// PHP magic methods that should not appear in completion results.
@@ -78,6 +80,8 @@ impl Backend {
7880
///
7981
/// - `Arrow` access: returns only non-static methods and properties.
8082
/// - `DoubleColon` access: returns only static methods, static properties, and constants.
83+
/// - `ParentDoubleColon` access: returns both static and non-static methods,
84+
/// static properties, and constants — but excludes private members.
8185
/// - `Other` access: returns all members.
8286
pub(crate) fn build_completion_items(
8387
target_class: &ClassInfo,
@@ -91,9 +95,18 @@ impl Backend {
9195
continue;
9296
}
9397

98+
// parent:: excludes private members
99+
if access_kind == AccessKind::ParentDoubleColon
100+
&& method.visibility == Visibility::Private
101+
{
102+
continue;
103+
}
104+
94105
let include = match access_kind {
95106
AccessKind::Arrow => !method.is_static,
96107
AccessKind::DoubleColon => method.is_static,
108+
// parent:: shows both static and non-static methods
109+
AccessKind::ParentDoubleColon => true,
97110
AccessKind::Other => true,
98111
};
99112
if !include {
@@ -113,19 +126,28 @@ impl Backend {
113126

114127
// Properties — filtered by static / instance
115128
for property in &target_class.properties {
129+
// parent:: excludes private members
130+
if access_kind == AccessKind::ParentDoubleColon
131+
&& property.visibility == Visibility::Private
132+
{
133+
continue;
134+
}
135+
116136
let include = match access_kind {
117137
AccessKind::Arrow => !property.is_static,
118-
AccessKind::DoubleColon => property.is_static,
138+
AccessKind::DoubleColon | AccessKind::ParentDoubleColon => property.is_static,
119139
AccessKind::Other => true,
120140
};
121141
if !include {
122142
continue;
123143
}
124144

125-
// Static properties accessed via `::` need the `$` prefix
126-
// (e.g. `self::$path`), while instance properties via `->`
145+
// Static properties accessed via `::` or `parent::` need the `$`
146+
// prefix (e.g. `self::$path`), while instance properties via `->`
127147
// use the bare name (e.g. `$this->path`).
128-
let display_name = if access_kind == AccessKind::DoubleColon {
148+
let display_name = if access_kind == AccessKind::DoubleColon
149+
|| access_kind == AccessKind::ParentDoubleColon
150+
{
129151
format!("${}", property.name)
130152
} else {
131153
property.name.clone()
@@ -147,9 +169,19 @@ impl Backend {
147169
});
148170
}
149171

150-
// Constants — only for `::` or unqualified access
151-
if access_kind == AccessKind::DoubleColon || access_kind == AccessKind::Other {
172+
// Constants — only for `::`, `parent::`, or unqualified access
173+
if access_kind == AccessKind::DoubleColon
174+
|| access_kind == AccessKind::ParentDoubleColon
175+
|| access_kind == AccessKind::Other
176+
{
152177
for constant in &target_class.constants {
178+
// parent:: excludes private members
179+
if access_kind == AccessKind::ParentDoubleColon
180+
&& constant.visibility == Visibility::Private
181+
{
182+
continue;
183+
}
184+
153185
let detail = if let Some(ref th) = constant.type_hint {
154186
format!("Class: {} — {}", target_class.name, th)
155187
} else {

src/completion/resolver.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ impl Backend {
4949
return current_class.cloned();
5050
}
5151

52+
// ── `parent::` — resolve to the current class's parent ──
53+
if subject == "parent" {
54+
if let Some(cc) = current_class
55+
&& let Some(ref parent_name) = cc.parent_class
56+
{
57+
// Try local lookup first
58+
let lookup = parent_name.rsplit('\\').next().unwrap_or(parent_name);
59+
if let Some(cls) = all_classes.iter().find(|c| c.name == lookup) {
60+
return Some(cls.clone());
61+
}
62+
// Fall back to cross-file / PSR-4
63+
return class_loader(parent_name);
64+
}
65+
return None;
66+
}
67+
5268
// ── Bare class name (for `::`) ──
5369
if access_kind == AccessKind::DoubleColon && !subject.starts_with('$') {
5470
let lookup = subject.rsplit('\\').next().unwrap_or(subject);

src/server.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tower_lsp::lsp_types::*;
1111

1212
use crate::Backend;
1313
use crate::composer;
14+
use crate::types::AccessKind;
1415

1516
#[tower_lsp::async_trait]
1617
impl LanguageServer for Backend {
@@ -231,7 +232,15 @@ impl LanguageServer for Backend {
231232

232233
if let Some(target_class) = resolved {
233234
let merged = Self::resolve_class_with_inheritance(&target_class, &class_loader);
234-
let items = Self::build_completion_items(&merged, target.access_kind);
235+
// `parent::` is syntactically `::` but semantically
236+
// different: it shows both static and instance members
237+
// while excluding private ones.
238+
let effective_access = if target.subject == "parent" {
239+
AccessKind::ParentDoubleColon
240+
} else {
241+
target.access_kind
242+
};
243+
let items = Self::build_completion_items(&merged, effective_access);
235244
if !items.is_empty() {
236245
return Ok(Some(CompletionResponse::Array(items)));
237246
}

src/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ pub enum AccessKind {
7676
Arrow,
7777
/// Completion triggered after `::` (static access).
7878
DoubleColon,
79+
/// Completion triggered after `parent::`.
80+
///
81+
/// This is an oddball: it shows both static **and** instance methods
82+
/// (since PHP allows `parent::nonStaticMethod()` from a child class),
83+
/// plus constants and static properties — but excludes private members.
84+
ParentDoubleColon,
7985
/// No specific access operator detected (e.g. inside class body).
8086
Other,
8187
}

0 commit comments

Comments
 (0)