Skip to content

Commit 984b3be

Browse files
fix(lsp): JRL chained-builder setter hover walks past intermediate .method(args) calls
Before: resolveReceiverBefore_ peeled ONE trailing (…) then stopped. For a chain like foo.X.Builder(x).setPm(true).setOf(…) a hover on setOf yielded receiver `.setPm`, which didn't resolve, so the hover returned NULL. Now: walk backward alternating between word/dot runs and balanced (…) groups, so the full chain `foo.X.Builder(x).setPm(true)` is captured. resolveReceiverType_ then iteratively strips trailing .method(args) (FOAM builders return `this`) — peels `.setPm(true)`, tries `foo.X.Builder`, then `foo.X`, resolves to foam.dao.EasyDAO. Added regression test: hovers on both setPm and setOf in a chained builder line. 497 tests total.
1 parent 7b3c0be commit 984b3be

2 files changed

Lines changed: 63 additions & 30 deletions

File tree

tools/lsp/handlers/JrlHandler.js

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -827,33 +827,38 @@ foam.CLASS({
827827
function resolveReceiverBefore_(line, wordStart) {
828828
/**
829829
* Return the FOAM class id of the receiver expression that precedes
830-
* `line[wordStart]`. Handles:
831-
* • `<dotted>.` — `foo.X.something.`
832-
* • `<dotted>(args).` — `foo.X.Builder(x).`
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
834+
*
835+
* Walks backward peeling alternating `word/dot` runs and balanced
836+
* `(…)` groups. Stops on whitespace, operators, or line start.
833837
*/
834-
var i = wordStart;
835-
if ( i === 0 || line.charAt(i - 1) !== '.' ) return null;
836-
var callEnd = i - 1;
837-
var parseEnd = callEnd;
838-
839-
// Optional balanced `(…)` right before the dot.
840-
if ( callEnd > 0 && line.charAt(callEnd - 1) === ')' ) {
841-
var depth = 1;
842-
var j = callEnd - 2;
843-
while ( j >= 0 && depth > 0 ) {
844-
var ch = line.charAt(j);
845-
if ( ch === ')' ) depth++;
846-
else if ( ch === '(' ) depth--;
847-
if ( depth === 0 ) break;
848-
j--;
838+
if ( wordStart === 0 || line.charAt(wordStart - 1) !== '.' ) return null;
839+
var pos = wordStart - 2; // start before the trailing dot
840+
while ( pos >= 0 ) {
841+
var ch = line.charAt(pos);
842+
if ( /[\w.$]/.test(ch) ) { pos--; continue; }
843+
if ( ch === ')' ) {
844+
var depth = 1;
845+
pos--;
846+
while ( pos >= 0 && depth > 0 ) {
847+
var c = line.charAt(pos);
848+
if ( c === ')' ) depth++;
849+
else if ( c === '(' ) depth--;
850+
if ( depth === 0 ) break;
851+
pos--;
852+
}
853+
if ( depth !== 0 ) return null;
854+
pos--; // consume the '('
855+
continue;
849856
}
850-
if ( depth !== 0 ) return null;
851-
parseEnd = j;
857+
break;
852858
}
853-
854-
var k = parseEnd - 1;
855-
while ( k >= 0 && /[\w.$]/.test(line.charAt(k)) ) k--;
856-
var receiverExpr = line.substring(k + 1, parseEnd).replace(/\.+$/, '');
859+
var start = pos + 1;
860+
while ( start < wordStart - 1 && line.charAt(start) === '.' ) start++;
861+
var receiverExpr = line.substring(start, wordStart - 1);
857862
if ( ! receiverExpr ) return null;
858863
return this.resolveReceiverType_(receiverExpr);
859864
},
@@ -1032,14 +1037,20 @@ foam.CLASS({
10321037
* • Fully-qualified / short class id (e.g. `foam.dao.EasyDAO`)
10331038
* • `X.Builder(...)` — type is `X.Builder` if registered, else `X`
10341039
* • `X.getOwnClassInfo()` — the class's own ClassInfo; treat as X
1040+
* • Chained builder calls `X.Builder(x).setA(true).setB(y)` —
1041+
* iteratively strip trailing `.method(args)` (FOAM builders
1042+
* return `this`, so the chain's type is the head's type)
10351043
*/
10361044
var e = expr.replace(/\s+/g, '');
1037-
if ( this.index.classExists(e) ) return e;
1038-
var builderBase = e.match(/^([\w.$]+?)\.Builder$/);
1039-
if ( builderBase && this.index.classExists(builderBase[1]) ) return builderBase[1];
1040-
// Strip trailing method invocations
1041-
var stripped = e.replace(/\.\w+\s*\([^)]*\)\s*$/, '');
1042-
if ( stripped !== e && this.index.classExists(stripped) ) return stripped;
1045+
var callRe = /\.\w+\s*\([^)]*\)\s*$/;
1046+
while ( e ) {
1047+
if ( this.index.classExists(e) ) return e;
1048+
var builderBase = e.match(/^([\w.$]+?)\.Builder$/);
1049+
if ( builderBase && this.index.classExists(builderBase[1]) ) return builderBase[1];
1050+
var next = e.replace(callRe, '');
1051+
if ( next === e ) break;
1052+
e = next;
1053+
}
10431054
return null;
10441055
},
10451056

tools/tests/testFoamLSP.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2799,6 +2799,28 @@ var vscodeJrl = JSON.parse(fs_.readFileSync(path_.join(__dirname, '../lsp/editor
27992799
test(!! vscodeJrl.repository['json-block-triple'] && !! vscodeJrl.repository['json-block-backtick'],
28002800
'VS Code foam-jrl grammar has JSON injections for client triple/backtick');
28012801

2802+
// === CHAINED BUILDER SETTER HOVERS ===
2803+
section('Chained builder setter hovers — walk back through .a(x).b(y) chains');
2804+
var chained = [
2805+
'p({',
2806+
' "class":"foam.core.boot.CSpec",',
2807+
' "id":"myDAO",',
2808+
' "serviceScript":"""',
2809+
' return new foam.dao.EasyDAO.Builder(x).setPm(true).setOf(foam.lang.FObject.getOwnClassInfo()).build();',
2810+
' """',
2811+
'})'
2812+
].join('\n');
2813+
var chainedLine = chained.split('\n')[4];
2814+
// setOf comes AFTER setPm(true). in the chain. Walk-back must skip the
2815+
// intermediate call to resolve to foam.dao.EasyDAO.
2816+
['setPm', 'setOf'].forEach(function(name) {
2817+
var idx = chainedLine.indexOf(name);
2818+
var hover = jrlH3.handleHover(chained, { line: 4, character: idx + 2 });
2819+
test(hover && hover.contents && hover.contents.value &&
2820+
/void ' + name + '|property/i.test(hover.contents.value.replace(/[|]/g, '')),
2821+
'Hover on .' + name + '( in chained builder — resolves to EasyDAO setter');
2822+
});
2823+
28022824
// === SUMMARY ===
28032825

28042826
section('SUMMARY');

0 commit comments

Comments
 (0)