@@ -1225,8 +1225,6 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
1225
1225
const importRE = / \b i m p o r t \s * \( \s * [ ' " ` ] ( ( [ \w @ . / : - ] + \/ ) ? (?: [ \w @ . / : - ] * ) ) (? ! [ ^ ' " ` ] ) $ / ;
1226
1226
const requireRE = / \b r e q u i r e \s * \( \s * [ ' " ` ] ( ( [ \w @ . / : - ] + \/ ) ? (?: [ \w @ . / : - ] * ) ) (? ! [ ^ ' " ` ] ) $ / ;
1227
1227
const fsAutoCompleteRE = / f s (?: \. p r o m i s e s ) ? \. \s * [ a - z ] [ a - z A - Z ] + \( \s * [ " ' ] ( .* ) / ;
1228
- const simpleExpressionRE =
1229
- / (?: [ \w $ ' " ` [ { ( ] (?: ( \w | | \t ) * ?[ ' " ` ] | \$ | [ ' " ` \] } ) ] ) * \? ? (?: \. | ] ) ? ) * ?(?: [ a - z A - Z _ $ ] ) ? (?: \w | \$ ) * \? ? \. ? $ / ;
1230
1228
const versionedFileNamesRe = / - \d + \. \d + / ;
1231
1229
1232
1230
function isIdentifier ( str ) {
@@ -1480,29 +1478,20 @@ function complete(line, callback) {
1480
1478
} else if ( ( match = RegExpPrototypeExec ( fsAutoCompleteRE , line ) ) !== null &&
1481
1479
this . allowBlockingCompletions ) {
1482
1480
( { 0 : completionGroups , 1 : completeOn } = completeFSFunctions ( match ) ) ;
1483
- // Handle variable member lookup.
1484
- // We support simple chained expressions like the following (no function
1485
- // calls, etc.). That is for simplicity and also because we *eval* that
1486
- // leading expression so for safety (see WARNING above) don't want to
1487
- // eval function calls.
1488
- //
1489
- // foo.bar<|> # completions for 'foo' with filter 'bar'
1490
- // spam.eggs.<|> # completions for 'spam.eggs' with filter ''
1491
- // foo<|> # all scope vars with filter 'foo'
1492
- // foo.<|> # completions for 'foo' with filter ''
1493
1481
} else if ( line . length === 0 ||
1494
1482
RegExpPrototypeExec ( / \w | \. | \$ / , line [ line . length - 1 ] ) !== null ) {
1495
- const { 0 : match } = RegExpPrototypeExec ( simpleExpressionRE , line ) || [ '' ] ;
1496
- if ( line . length !== 0 && ! match ) {
1483
+ const completeTarget = line . length === 0 ? line : findPotentialExpressionCompleteTarget ( line ) ;
1484
+
1485
+ if ( line . length !== 0 && ! completeTarget ) {
1497
1486
completionGroupsLoaded ( ) ;
1498
1487
return ;
1499
1488
}
1500
1489
let expr = '' ;
1501
- completeOn = match ;
1490
+ completeOn = completeTarget ;
1502
1491
if ( StringPrototypeEndsWith ( line , '.' ) ) {
1503
- expr = StringPrototypeSlice ( match , 0 , - 1 ) ;
1492
+ expr = StringPrototypeSlice ( completeTarget , 0 , - 1 ) ;
1504
1493
} else if ( line . length !== 0 ) {
1505
- const bits = StringPrototypeSplit ( match , '.' ) ;
1494
+ const bits = StringPrototypeSplit ( completeTarget , '.' ) ;
1506
1495
filter = ArrayPrototypePop ( bits ) ;
1507
1496
expr = ArrayPrototypeJoin ( bits , '.' ) ;
1508
1497
}
@@ -1531,7 +1520,7 @@ function complete(line, callback) {
1531
1520
}
1532
1521
1533
1522
return includesProxiesOrGetters (
1534
- StringPrototypeSplit ( match , '.' ) ,
1523
+ StringPrototypeSplit ( completeTarget , '.' ) ,
1535
1524
this . eval ,
1536
1525
this . context ,
1537
1526
( includes ) => {
@@ -1642,6 +1631,100 @@ function complete(line, callback) {
1642
1631
}
1643
1632
}
1644
1633
1634
+ /**
1635
+ * This function tries to extract a target for tab completion from code representing an expression.
1636
+ *
1637
+ * Such target is basically the last piece of the expression that can be evaluated for the potential
1638
+ * tab completion.
1639
+ *
1640
+ * Some examples:
1641
+ * - The complete target for `const a = obj.b` is `obj.b`
1642
+ * (because tab completion will evaluate and check the `obj.b` object)
1643
+ * - The complete target for `tru` is `tru`
1644
+ * (since we'd ideally want to complete that to `true`)
1645
+ * - The complete target for `{ a: tru` is `tru`
1646
+ * (like the last example, we'd ideally want that to complete to true)
1647
+ * - There is no complete target for `{ a: true }`
1648
+ * (there is nothing to complete)
1649
+ * @param {string } code the code representing the expression to analyze
1650
+ * @returns {string|null } a substring of the code representing the complete target is there was one, `null` otherwise
1651
+ */
1652
+ function findPotentialExpressionCompleteTarget ( code ) {
1653
+ if ( ! code ) {
1654
+ return null ;
1655
+ }
1656
+
1657
+ if ( code . at ( - 1 ) === '.' ) {
1658
+ if ( code . at ( - 2 ) === '?' ) {
1659
+ // The code ends with the optional chaining operator (`?.`),
1660
+ // such code can't generate a valid AST so we need to strip
1661
+ // the suffix, run this function's logic and add back the
1662
+ // optional chaining operator to the result if present
1663
+ const result = findPotentialExpressionCompleteTarget ( code . slice ( 0 , - 2 ) ) ;
1664
+ return ! result ? result : `${ result } ?.` ;
1665
+ }
1666
+
1667
+ // The code ends with a dot, such code can't generate a valid AST
1668
+ // so we need to strip the suffix, run this function's logic and
1669
+ // add back the dot to the result if present
1670
+ const result = findPotentialExpressionCompleteTarget ( code . slice ( 0 , - 1 ) ) ;
1671
+ return ! result ? result : `${ result } .` ;
1672
+ }
1673
+
1674
+ let ast ;
1675
+ try {
1676
+ ast = acornParse ( code , { __proto__ : null , sourceType : 'module' , ecmaVersion : 'latest' } ) ;
1677
+ } catch {
1678
+ const keywords = code . split ( ' ' ) ;
1679
+
1680
+ if ( keywords . length > 1 ) {
1681
+ // Something went wrong with the parsing, however this can be due to incomplete code
1682
+ // (that is for example missing a closing bracket, as for example `{ a: obj.te`), in
1683
+ // this case we take the last code keyword and try again
1684
+ // TODO(dario-piotrowicz): make this more robust, right now we only split by spaces
1685
+ // but that's not always enough, for example it doesn't handle
1686
+ // this code: `{ a: obj['hello world'].te`
1687
+ return findPotentialExpressionCompleteTarget ( keywords . at ( - 1 ) ) ;
1688
+ }
1689
+
1690
+ // The ast parsing has legitimately failed so we return null
1691
+ return null ;
1692
+ }
1693
+
1694
+ const lastBodyStatement = ast . body [ ast . body . length - 1 ] ;
1695
+
1696
+ if ( ! lastBodyStatement ) {
1697
+ return null ;
1698
+ }
1699
+
1700
+ // If the last statement is a block we know there is not going to be a potential
1701
+ // completion target (e.g. in `{ a: true }` there is no completion to be done)
1702
+ if ( lastBodyStatement . type === 'BlockStatement' ) {
1703
+ return null ;
1704
+ }
1705
+
1706
+ // If the last statement is an expression and it has a right side, that's what we
1707
+ // want to potentially complete on, so let's re-run the function's logic on that
1708
+ if ( lastBodyStatement . type === 'ExpressionStatement' && lastBodyStatement . expression . right ) {
1709
+ const exprRight = lastBodyStatement . expression . right ;
1710
+ const exprRightCode = code . slice ( exprRight . start , exprRight . end ) ;
1711
+ return findPotentialExpressionCompleteTarget ( exprRightCode ) ;
1712
+ }
1713
+
1714
+ // If the last statement is a variable declaration statement the last declaration is
1715
+ // what we can potentially complete on, so let's re-run the function's logic on that
1716
+ if ( lastBodyStatement . type === 'VariableDeclaration' ) {
1717
+ const lastDeclarationInit = lastBodyStatement . declarations . at ( - 1 ) . init ;
1718
+ const lastDeclarationInitCode = code . slice ( lastDeclarationInit . start , lastDeclarationInit . end ) ;
1719
+ return findPotentialExpressionCompleteTarget ( lastDeclarationInitCode ) ;
1720
+ }
1721
+
1722
+ // If any of the above early returns haven't activated then it means that
1723
+ // the potential complete target is the full code (e.g. the code represents
1724
+ // a simple partial identifier, a member expression, etc...)
1725
+ return code ;
1726
+ }
1727
+
1645
1728
function includesProxiesOrGetters ( exprSegments , evalFn , context , callback , currentExpr = '' , idx = 0 ) {
1646
1729
const currentSegment = exprSegments [ idx ] ;
1647
1730
currentExpr += `${ currentExpr . length === 0 ? '' : '.' } ${ currentSegment } ` ;
0 commit comments