@@ -722,24 +722,50 @@ fn preceded_by_use_keyword_bytes(bytes: &[u8], keyword_start: usize) -> bool {
722722 check_keyword_ending_at_bytes ( bytes, before, b"use" )
723723}
724724
725- fn has_const_keyword_before_name ( bytes : & [ u8 ] , pos : usize ) -> bool {
725+ pub ( super ) fn has_const_keyword_before_name ( bytes : & [ u8 ] , pos : usize ) -> bool {
726726 let mut line_start = pos;
727727 while line_start > 0 && bytes[ line_start - 1 ] != b'\n' {
728728 line_start -= 1 ;
729729 }
730- bytes[ line_start..pos]
730+ // Find the standalone `const` keyword governing this line (skipping
731+ // `use const` imports).
732+ let Some ( const_end) = bytes[ line_start..pos]
731733 . windows ( b"const" . len ( ) )
732734 . enumerate ( )
733- . any ( |( idx, window) | {
735+ . find_map ( |( idx, window) | {
734736 if window != b"const" {
735- return false ;
737+ return None ;
736738 }
737739 let start = line_start + idx;
738740 let end = start + b"const" . len ( ) ;
739- ( start == 0 || !is_ident_byte ( bytes[ start - 1 ] ) )
740- && ( end >= bytes. len ( ) || !is_ident_byte ( bytes[ end] ) )
741- && !preceded_by_use_keyword_bytes ( bytes, start)
741+ let is_word = ( start == 0 || !is_ident_byte ( bytes[ start - 1 ] ) )
742+ && ( end >= bytes. len ( ) || !is_ident_byte ( bytes[ end] ) ) ;
743+ ( is_word && !preceded_by_use_keyword_bytes ( bytes, start) ) . then_some ( end )
742744 } )
745+ else {
746+ return false ;
747+ } ;
748+ // A declarator *name* follows `const` or a top-level `,`; the cursor is
749+ // in the initializer once a top-level `=` is seen (until the next
750+ // comma). `const MAX = PHP_INT_M|` is a value position, not a name.
751+ !in_const_initializer ( bytes, const_end, pos)
752+ }
753+
754+ /// Whether the byte range `from..pos` (starting just after a `const`
755+ /// keyword) ends inside a declarator initializer rather than a name slot.
756+ fn in_const_initializer ( bytes : & [ u8 ] , from : usize , pos : usize ) -> bool {
757+ let mut depth = 0i32 ;
758+ let mut in_value = false ;
759+ for & b in & bytes[ from..pos] {
760+ match b {
761+ b'(' | b'[' | b'{' => depth += 1 ,
762+ b')' | b']' | b'}' => depth = depth. saturating_sub ( 1 ) ,
763+ b',' if depth == 0 => in_value = false ,
764+ b'=' if depth == 0 => in_value = true ,
765+ _ => { }
766+ }
767+ }
768+ in_value
743769}
744770
745771/// Property name after `$` on a property declaration line (not a parameter).
@@ -773,11 +799,14 @@ fn is_function_or_const_name_position_at_offset(bytes: &[u8], cursor: usize) ->
773799 return false ;
774800 }
775801
776- if check_keyword_ending_at_bytes ( bytes, i, b"fn" )
777- || check_keyword_ending_at_bytes ( bytes, i, b"case" )
778- {
802+ if check_keyword_ending_at_bytes ( bytes, i, b"fn" ) {
779803 return true ;
780804 }
805+ // `case` names an enum case (a member) but also labels a `switch`
806+ // branch, where a class/const/enum name completion is wanted.
807+ if check_keyword_ending_at_bytes ( bytes, i, b"case" ) {
808+ return case_is_enum_declaration ( bytes, i) ;
809+ }
781810 if check_keyword_ending_at_bytes ( bytes, i, b"function" ) {
782811 return !preceded_by_use_keyword_bytes ( bytes, i - "function" . len ( ) ) ;
783812 }
@@ -787,6 +816,40 @@ fn is_function_or_const_name_position_at_offset(bytes: &[u8], cursor: usize) ->
787816 has_const_keyword_before_name ( bytes, i)
788817}
789818
819+ /// Whether the `case` keyword ending at `keyword_end` declares an enum case
820+ /// (directly inside an `enum` body) rather than a `switch` branch label.
821+ /// Scans back to the nearest enclosing unmatched `{` and checks whether it
822+ /// opens an enum.
823+ pub ( super ) fn case_is_enum_declaration ( bytes : & [ u8 ] , keyword_end : usize ) -> bool {
824+ let mut depth = 0i32 ;
825+ let mut k = keyword_end - "case" . len ( ) ;
826+ while k > 0 {
827+ k -= 1 ;
828+ match bytes[ k] {
829+ b'}' => depth += 1 ,
830+ b'{' => {
831+ if depth == 0 {
832+ return brace_opens_enum ( bytes, k) ;
833+ }
834+ depth -= 1 ;
835+ }
836+ _ => { }
837+ }
838+ }
839+ false
840+ }
841+
842+ /// Whether the block opened by the `{` at `brace_pos` is an `enum` body,
843+ /// determined from the block header (the text back to the previous
844+ /// statement boundary).
845+ fn brace_opens_enum ( bytes : & [ u8 ] , brace_pos : usize ) -> bool {
846+ let mut start = brace_pos;
847+ while start > 0 && !matches ! ( bytes[ start - 1 ] , b';' | b'{' | b'}' ) {
848+ start -= 1 ;
849+ }
850+ contains_ascii_word ( & bytes[ start..brace_pos] , b"enum" )
851+ }
852+
790853fn is_property_declaration_name_position_at_offset ( bytes : & [ u8 ] , cursor : usize ) -> bool {
791854 // Skip partial name.
792855 let mut i = cursor;
0 commit comments