@@ -1225,8 +1225,6 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
12251225const importRE = / \b i m p o r t \s * \( \s * [ ' " ` ] ( ( [ \w @ . / : - ] + \/ ) ? (?: [ \w @ . / : - ] * ) ) (? ! [ ^ ' " ` ] ) $ / ;
12261226const requireRE = / \b r e q u i r e \s * \( \s * [ ' " ` ] ( ( [ \w @ . / : - ] + \/ ) ? (?: [ \w @ . / : - ] * ) ) (? ! [ ^ ' " ` ] ) $ / ;
12271227const 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 | \$ ) * \? ? \. ? $ / ;
12301228const versionedFileNamesRe = / - \d + \. \d + / ;
12311229
12321230function isIdentifier ( str ) {
@@ -1480,29 +1478,20 @@ function complete(line, callback) {
14801478 } else if ( ( match = RegExpPrototypeExec ( fsAutoCompleteRE , line ) ) !== null &&
14811479 this . allowBlockingCompletions ) {
14821480 ( { 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 ''
14931481 } else if ( line . length === 0 ||
14941482 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 ) {
14971486 completionGroupsLoaded ( ) ;
14981487 return ;
14991488 }
15001489 let expr = '' ;
1501- completeOn = match ;
1490+ completeOn = completeTarget ;
15021491 if ( StringPrototypeEndsWith ( line , '.' ) ) {
1503- expr = StringPrototypeSlice ( match , 0 , - 1 ) ;
1492+ expr = StringPrototypeSlice ( completeTarget , 0 , - 1 ) ;
15041493 } else if ( line . length !== 0 ) {
1505- const bits = StringPrototypeSplit ( match , '.' ) ;
1494+ const bits = StringPrototypeSplit ( completeTarget , '.' ) ;
15061495 filter = ArrayPrototypePop ( bits ) ;
15071496 expr = ArrayPrototypeJoin ( bits , '.' ) ;
15081497 }
@@ -1531,7 +1520,7 @@ function complete(line, callback) {
15311520 }
15321521
15331522 return includesProxiesOrGetters (
1534- StringPrototypeSplit ( match , '.' ) ,
1523+ StringPrototypeSplit ( completeTarget , '.' ) ,
15351524 this . eval ,
15361525 this . context ,
15371526 ( includes ) => {
@@ -1642,6 +1631,100 @@ function complete(line, callback) {
16421631 }
16431632}
16441633
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+
16451728function includesProxiesOrGetters ( exprSegments , evalFn , context , callback , currentExpr = '' , idx = 0 ) {
16461729 const currentSegment = exprSegments [ idx ] ;
16471730 currentExpr += `${ currentExpr . length === 0 ? '' : '.' } ${ currentSegment } ` ;
0 commit comments