Skip to content

Commit ec58614

Browse files
committed
Add completing of static members
1 parent ef571e4 commit ec58614

2 files changed

Lines changed: 661 additions & 3 deletions

File tree

src/lib.rs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ pub struct PropertyInfo {
4848
pub is_static: bool,
4949
}
5050

51+
/// Stores extracted constant information from a parsed PHP class.
52+
#[derive(Debug, Clone)]
53+
pub struct ConstantInfo {
54+
/// The constant name (e.g. "MAX_SIZE", "STATUS_ACTIVE").
55+
pub name: String,
56+
/// Optional type hint string (e.g. "string", "int").
57+
pub type_hint: Option<String>,
58+
}
59+
60+
/// Describes the access operator that triggered completion.
61+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62+
pub enum AccessKind {
63+
/// Completion triggered after `->` (instance access).
64+
Arrow,
65+
/// Completion triggered after `::` (static access).
66+
DoubleColon,
67+
/// No specific access operator detected (e.g. inside class body).
68+
Other,
69+
}
70+
5171
/// Stores extracted class information from a parsed PHP file.
5272
/// All data is owned so we don't depend on the parser's arena lifetime.
5373
#[derive(Debug, Clone)]
@@ -58,6 +78,8 @@ pub struct ClassInfo {
5878
pub methods: Vec<MethodInfo>,
5979
/// The properties defined directly in this class.
6080
pub properties: Vec<PropertyInfo>,
81+
/// The constants defined directly in this class.
82+
pub constants: Vec<ConstantInfo>,
6183
/// Byte offset where the class body starts (left brace).
6284
pub start_offset: u32,
6385
/// Byte offset where the class body ends (right brace).
@@ -249,6 +271,7 @@ impl Backend {
249271

250272
let mut methods = Vec::new();
251273
let mut properties = Vec::new();
274+
let mut constants = Vec::new();
252275

253276
for member in class.members.iter() {
254277
match member {
@@ -272,6 +295,16 @@ impl Backend {
272295
let mut prop_infos = Self::extract_property_info(property);
273296
properties.append(&mut prop_infos);
274297
}
298+
ClassLikeMember::Constant(constant) => {
299+
let type_hint =
300+
constant.hint.as_ref().map(|h| Self::extract_hint_string(h));
301+
for item in constant.items.iter() {
302+
constants.push(ConstantInfo {
303+
name: item.name.value.to_string(),
304+
type_hint: type_hint.clone(),
305+
});
306+
}
307+
}
275308
_ => {}
276309
}
277310
}
@@ -283,6 +316,7 @@ impl Backend {
283316
name: class_name,
284317
methods,
285318
properties,
319+
constants,
286320
start_offset,
287321
end_offset,
288322
});
@@ -333,6 +367,34 @@ impl Backend {
333367
.find(|c| offset >= c.start_offset && offset <= c.end_offset)
334368
}
335369

370+
/// Detect the access operator before the cursor position by scanning
371+
/// backwards past any partial identifier the user may have typed.
372+
pub fn detect_access_kind(content: &str, position: Position) -> AccessKind {
373+
let lines: Vec<&str> = content.lines().collect();
374+
if position.line as usize >= lines.len() {
375+
return AccessKind::Other;
376+
}
377+
378+
let line = lines[position.line as usize];
379+
let chars: Vec<char> = line.chars().collect();
380+
let col = (position.character as usize).min(chars.len());
381+
382+
// Walk backwards past any identifier characters the user may have typed
383+
let mut i = col;
384+
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
385+
i -= 1;
386+
}
387+
388+
// Now check for `->` or `::`
389+
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
390+
AccessKind::Arrow
391+
} else if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' {
392+
AccessKind::DoubleColon
393+
} else {
394+
AccessKind::Other
395+
}
396+
}
397+
336398
/// Build the label showing the full method signature.
337399
///
338400
/// Example: `regularCode(string $text, $frogs = false): string`
@@ -525,9 +587,19 @@ impl LanguageServer for Backend {
525587
&& let Some(class_info) = Self::find_class_at_offset(&classes, offset)
526588
{
527589
let mut items: Vec<CompletionItem> = Vec::new();
590+
let access_kind = Self::detect_access_kind(&content, position);
528591

529-
// Add method completions
592+
// Add method completions (filtered by access kind)
530593
for method in &class_info.methods {
594+
let include = match access_kind {
595+
AccessKind::Arrow => !method.is_static,
596+
AccessKind::DoubleColon => method.is_static,
597+
AccessKind::Other => true,
598+
};
599+
if !include {
600+
continue;
601+
}
602+
531603
let label = Self::build_method_label(method);
532604

533605
items.push(CompletionItem {
@@ -540,8 +612,17 @@ impl LanguageServer for Backend {
540612
});
541613
}
542614

543-
// Add property completions
615+
// Add property completions (filtered by access kind)
544616
for property in &class_info.properties {
617+
let include = match access_kind {
618+
AccessKind::Arrow => !property.is_static,
619+
AccessKind::DoubleColon => property.is_static,
620+
AccessKind::Other => true,
621+
};
622+
if !include {
623+
continue;
624+
}
625+
545626
let detail = if let Some(ref th) = property.type_hint {
546627
format!("Class: {} — {}", class_info.name, th)
547628
} else {
@@ -557,6 +638,26 @@ impl LanguageServer for Backend {
557638
});
558639
}
559640

641+
// Add constant completions (only for `::` or unqualified access)
642+
if access_kind == AccessKind::DoubleColon || access_kind == AccessKind::Other {
643+
for constant in &class_info.constants {
644+
let detail = if let Some(ref th) = constant.type_hint {
645+
format!("Class: {} — {}", class_info.name, th)
646+
} else {
647+
format!("Class: {}", class_info.name)
648+
};
649+
650+
items.push(CompletionItem {
651+
label: constant.name.clone(),
652+
kind: Some(CompletionItemKind::CONSTANT),
653+
detail: Some(detail),
654+
insert_text: Some(constant.name.clone()),
655+
filter_text: Some(constant.name.clone()),
656+
..CompletionItem::default()
657+
});
658+
}
659+
}
660+
560661
if !items.is_empty() {
561662
return Ok(Some(CompletionResponse::Array(items)));
562663
}

0 commit comments

Comments
 (0)