@@ -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