@@ -794,57 +794,86 @@ foam.CLASS({
794794 * back once the grammar path is stable and covered by tests.
795795 */
796796 var cpos = this . embedCursorToPosition_ ( ctx ) ;
797- var clines = ctx . content . split ( '\n' ) ;
798- var line = clines [ cpos . line ] || '' ;
797+ var content = ctx . content ;
798+ var cursorAbs = this . positionToOffset_ ( content , cpos ) ;
799+
799800 var wordRe = / [ \w . $ ] / ;
800- var start = cpos . character ;
801- var end = cpos . character ;
802- while ( start > 0 && wordRe . test ( line . charAt ( start - 1 ) ) ) start -- ;
803- while ( end < line . length && wordRe . test ( line . charAt ( end ) ) ) end ++ ;
804- var dotted = line . substring ( start , end ) . replace ( / ^ \. + | \. + $ / g, '' ) ;
801+ var start = cursorAbs ;
802+ var end = cursorAbs ;
803+ while ( start > 0 && wordRe . test ( content . charAt ( start - 1 ) ) ) start -- ;
804+ while ( end < content . length && wordRe . test ( content . charAt ( end ) ) ) end ++ ;
805+ var dotted = content . substring ( start , end ) . replace ( / ^ \. + | \. + $ / g, '' ) ;
805806
806807 if ( dotted && this . index . classExists ( dotted ) ) {
807808 return this . index . getClassDoc ( dotted ) ;
808809 }
809810 if ( dotted && dotted . indexOf ( '.' ) !== - 1 ) {
810811 for ( var cand = dotted ; cand . indexOf ( '.' ) !== - 1 ; ) {
811- if ( this . index . classExists ( cand ) ) return this . index . getClassDoc ( cand ) ;
812+ if ( this . index . classExists ( cand ) ) {
813+ // If the trimmed suffix is an enum value on this class, show
814+ // the value hover rather than the class doc.
815+ var suffix = dotted . substring ( cand . length + 1 ) ;
816+ if ( suffix && suffix . indexOf ( '.' ) === - 1 ) {
817+ var enumHit = this . buildEnumValueHover_ ( cand , suffix ) ;
818+ if ( enumHit ) return enumHit ;
819+ }
820+ return this . index . getClassDoc ( cand ) ;
821+ }
812822 cand = cand . substring ( 0 , cand . lastIndexOf ( '.' ) ) ;
813823 }
814824 }
815825 if ( dotted && dotted . indexOf ( '.' ) === - 1 ) {
816826 // Effective word start: first non-dot char in the matched region.
817827 var effectiveStart = start ;
818- while ( effectiveStart < line . length && line . charAt ( effectiveStart ) === '.' ) {
828+ while ( effectiveStart < content . length && content . charAt ( effectiveStart ) === '.' ) {
819829 effectiveStart ++ ;
820830 }
821- var receiverType = this . resolveReceiverBefore_ ( line , effectiveStart ) ;
822- if ( receiverType ) return this . buildMemberHover_ ( receiverType , dotted ) ;
831+ var receiverType = this . resolveReceiverBefore_ ( content , effectiveStart ) ;
832+ if ( receiverType ) {
833+ // Check enum first — hovering on ENUM_VALUE inside enum class scope.
834+ var enumHover = this . buildEnumValueHover_ ( receiverType , dotted ) ;
835+ if ( enumHover ) return enumHover ;
836+ return this . buildMemberHover_ ( receiverType , dotted ) ;
837+ }
823838 }
824839 return null ;
825840 } ,
826841
827- function resolveReceiverBefore_ ( line , wordStart ) {
842+ function positionToOffset_ ( content , pos ) {
843+ /** Map {line, character} to absolute offset in content. */
844+ var off = 0 , line = 0 ;
845+ for ( var i = 0 ; i < content . length ; i ++ ) {
846+ if ( line === pos . line ) return i + pos . character ;
847+ if ( content . charCodeAt ( i ) === 10 ) line ++ ;
848+ }
849+ return content . length ;
850+ } ,
851+
852+ function resolveReceiverBefore_ ( content , wordStart ) {
828853 /**
829854 * Return the FOAM class id of the receiver expression that precedes
830- * `line[wordStart]`. Handles arbitrarily long chains:
831- * • `foo.X.something.`
832- * • `foo.X.Builder(x).`
833- * • `foo.X.Builder(x).setA(true).setB(y).` ← chained builder
855+ * `content[wordStart]`. Operates on the FULL embedded content so
856+ * multi-line builder chains work:
857+ *
858+ * return new foo.X.Builder(x)
859+ * .setA(true)
860+ * .setB(y)
861+ * .setC(…) ← cursor on setC resolves receiver across lines
834862 *
835- * Walks backward peeling alternating `word/dot` runs and balanced
836- * `(…)` groups. Stops on whitespace, operators, or line start.
863+ * Walks backward peeling alternating word/dot runs and balanced
864+ * `(…)` groups; treats whitespace (including newlines) as part of
865+ * the chain if it's between a dot and the next token.
837866 */
838- if ( wordStart === 0 || line . charAt ( wordStart - 1 ) !== '.' ) return null ;
867+ if ( wordStart === 0 || content . charAt ( wordStart - 1 ) !== '.' ) return null ;
839868 var pos = wordStart - 2 ; // start before the trailing dot
840869 while ( pos >= 0 ) {
841- var ch = line . charAt ( pos ) ;
870+ var ch = content . charAt ( pos ) ;
842871 if ( / [ \w . $ ] / . test ( ch ) ) { pos -- ; continue ; }
843872 if ( ch === ')' ) {
844873 var depth = 1 ;
845874 pos -- ;
846875 while ( pos >= 0 && depth > 0 ) {
847- var c = line . charAt ( pos ) ;
876+ var c = content . charAt ( pos ) ;
848877 if ( c === ')' ) depth ++ ;
849878 else if ( c === '(' ) depth -- ;
850879 if ( depth === 0 ) break ;
@@ -854,15 +883,52 @@ foam.CLASS({
854883 pos -- ; // consume the '('
855884 continue ;
856885 }
886+ if ( / \s / . test ( ch ) ) {
887+ // Whitespace (including newlines) is part of the chain only if it
888+ // sits between two chain elements — i.e. the next non-ws char
889+ // going backward is `.`, `)`, or `]`. This rejects `new foo.Bar`
890+ // (whitespace between `new` and the chain) while accepting
891+ // foo.Builder(x)
892+ // .setA(…) (ws before `.`)
893+ // and foo
894+ // .bar (ws before `.`)
895+ var skip = pos ;
896+ while ( skip >= 0 && / \s / . test ( content . charAt ( skip ) ) ) skip -- ;
897+ if ( skip < 0 ) break ;
898+ var prev = content . charAt ( skip ) ;
899+ if ( prev !== '.' && prev !== ')' && prev !== ']' ) break ;
900+ pos = skip ;
901+ continue ;
902+ }
857903 break ;
858904 }
859- var start = pos + 1 ;
860- while ( start < wordStart - 1 && line . charAt ( start ) === '.' ) start ++ ;
861- var receiverExpr = line . substring ( start , wordStart - 1 ) ;
905+ var segStart = pos + 1 ;
906+ // Skip leading whitespace and leading dots.
907+ while ( segStart < wordStart - 1 && / [ \s . ] / . test ( content . charAt ( segStart ) ) ) segStart ++ ;
908+ var receiverExpr = content . substring ( segStart , wordStart - 1 ) . replace ( / \s + / g, '' ) ;
862909 if ( ! receiverExpr ) return null ;
863910 return this . resolveReceiverType_ ( receiverExpr ) ;
864911 } ,
865912
913+ function buildEnumValueHover_ ( classId , valueName ) {
914+ /**
915+ * If classId is an enum and valueName is one of its values, build a
916+ * hover showing the value's label and ordinal.
917+ */
918+ var values = this . index . getEnumValues ( classId ) ;
919+ if ( ! values || ! values . length ) return null ;
920+ for ( var i = 0 ; i < values . length ; i ++ ) {
921+ if ( values [ i ] . name !== valueName ) continue ;
922+ var v = values [ i ] ;
923+ var md = '```java\n' + classId + '.' + v . name + '\n```\n' ;
924+ md += '*enum value on `' + classId + '`*' ;
925+ if ( v . label ) md += '\n\nLabel: **' + v . label + '**' ;
926+ if ( v . ordinal != null ) md += '\n\nOrdinal: ' + v . ordinal ;
927+ return md ;
928+ }
929+ return null ;
930+ } ,
931+
866932 function buildMemberHover_ ( classId , memberName ) {
867933 /**
868934 * Build hover markdown for a member access on a known class. Matches
@@ -996,24 +1062,34 @@ foam.CLASS({
9961062 * or the builder's own type) this surfaces EVERY real setter
9971063 * the class actually declares, no hardcoded list.
9981064 */
999- var lines = text . split ( '\n' ) ;
1000- var line = lines [ position . line ] || '' ;
1001- var prefix = line . substring ( 0 , position . character ) ;
1002-
1003- // Member access: `<receiver>.<partial>` — try registry-driven resolution.
1004- var memberMatch = prefix . match ( / ( [ \w . $ ] + ) \s * (?: \( [ ^ ) ] * \) \s * (?: \. \w + \s * \( [ ^ ) ] * \) \s * ) * ) ? \. ( \w * ) $ / ) ;
1005- if ( memberMatch ) {
1006- var receiverExpr = memberMatch [ 1 ] ;
1007- var partialName = memberMatch [ 2 ] || '' ;
1008- var receiverType = this . resolveReceiverType_ ( receiverExpr ) ;
1065+ // Operate on the FULL content up to the cursor so multi-line
1066+ // builder chains resolve correctly (each setter on its own line).
1067+ var cursorAbs = this . positionToOffset_ ( text , position ) ;
1068+ var prefix = text . substring ( 0 , cursorAbs ) ;
1069+
1070+ // Partial word at cursor: letters only — we match an optional leading
1071+ // dot explicitly below.
1072+ var partialMatch = prefix . match ( / \. ( \w * ) $ / ) ;
1073+ if ( partialMatch ) {
1074+ var partialName = partialMatch [ 1 ] || '' ;
1075+ var dotPos = cursorAbs - partialName . length - 1 ;
1076+ var receiverType = this . resolveReceiverBefore_ ( text , dotPos + 1 ) ;
10091077 if ( receiverType ) {
1078+ // Enum values take precedence — they're what you actually want
1079+ // when the receiver is an enum class.
1080+ var enumItems = this . enumValueCompletions_ ( receiverType , partialName ) ;
1081+ if ( enumItems . length > 0 ) return { isIncomplete : false , items : enumItems } ;
1082+
10101083 var items = this . memberCompletions_ ( receiverType , partialName ) ;
10111084 if ( items . length > 0 ) return { isIncomplete : false , items : items } ;
10121085 }
10131086 }
10141087
10151088 // Otherwise: dotted class-id prefix search.
1016- var dotted = prefix . match ( / ( [ \w . $ ] + ) $ / ) ;
1089+ var lines = text . split ( '\n' ) ;
1090+ var line = lines [ position . line ] || '' ;
1091+ var linePrefix = line . substring ( 0 , position . character ) ;
1092+ var dotted = linePrefix . match ( / ( [ \w . $ ] + ) $ / ) ;
10171093 var partial = dotted ? dotted [ 1 ] : '' ;
10181094 var ids = this . index . getAllClassIds ( ) ;
10191095 var out = [ ] ;
@@ -1054,6 +1130,29 @@ foam.CLASS({
10541130 return null ;
10551131 } ,
10561132
1133+ function enumValueCompletions_ ( classId , partial ) {
1134+ /**
1135+ * If classId is a FOAM enum, offer its VALUES as completions.
1136+ * Returns empty array for non-enums.
1137+ */
1138+ var values = this . index . getEnumValues ( classId ) ;
1139+ if ( ! values || ! values . length ) return [ ] ;
1140+ var items = [ ] ;
1141+ var lower = partial ? partial . toLowerCase ( ) : '' ;
1142+ for ( var i = 0 ; i < values . length ; i ++ ) {
1143+ var v = values [ i ] ;
1144+ if ( lower && v . name . toLowerCase ( ) . indexOf ( lower ) === - 1 ) continue ;
1145+ items . push ( {
1146+ label : v . name ,
1147+ kind : 20 , // CompletionItemKind.EnumMember
1148+ detail : classId + ( v . label ? ' — ' + v . label : '' ) ,
1149+ insertText : v . name ,
1150+ sortText : '!' + String ( v . ordinal != null ? v . ordinal : i ) . padStart ( 5 , '0' )
1151+ } ) ;
1152+ }
1153+ return items ;
1154+ } ,
1155+
10571156 function memberCompletions_ ( classId , partial ) {
10581157 /**
10591158 * Build completion items for members of a FOAM class's Java surface:
0 commit comments