diff --git a/src/services/inlayHints.ts b/src/services/inlayHints.ts index 43dad7ea1793d..2279de76f378d 100644 --- a/src/services/inlayHints.ts +++ b/src/services/inlayHints.ts @@ -109,12 +109,14 @@ import { PrefixUnaryExpression, PropertyDeclaration, QuotePreference, - SignatureDeclarationBase, - skipParentheses, - some, - Symbol, - SymbolFlags, - SyntaxKind, + SignatureDeclarationBase, + Signature, + signatureHasRestParameter, + skipParentheses, + some, + Symbol, + SymbolFlags, + SyntaxKind, TemplateLiteralLikeNode, textSpanIntersectsWith, tokenToString, @@ -292,44 +294,43 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { } } - function visitCallOrNewExpression(expr: CallExpression | NewExpression) { - const args = expr.arguments; - if (!args || !args.length) { - return; - } - - const signature = checker.getResolvedSignature(expr); - if (signature === undefined) return; - - let signatureParamPos = 0; - for (const originalArg of args) { - const arg = skipParentheses(originalArg); - if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { - signatureParamPos++; - continue; - } - - let spreadArgs = 0; - if (isSpreadElement(arg)) { - const spreadType = checker.getTypeAtLocation(arg.expression); - if (checker.isTupleType(spreadType)) { - const { elementFlags, fixedLength } = (spreadType as TupleTypeReference).target; - if (fixedLength === 0) { - continue; - } - const firstOptionalIndex = findIndex(elementFlags, f => !(f & ElementFlags.Required)); - const requiredArgs = firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; - if (requiredArgs > 0) { - spreadArgs = firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; - } - } - } - - const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, signatureParamPos); - signatureParamPos = signatureParamPos + (spreadArgs || 1); - if (identifierInfo) { - const { parameter, parameterName, isRestParameter: isFirstVariadicArgument } = identifierInfo; - const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); + function visitCallOrNewExpression(expr: CallExpression | NewExpression) { + const args = expr.arguments; + if (!args || !args.length) { + return; + } + + const signature = checker.getResolvedSignature(expr); + if (signature === undefined) return; + + const argumentSpans = args.map(arg => getArgumentSpan(skipParentheses(arg))); + let totalArgumentPositions = 0; + for (const span of argumentSpans) { + totalArgumentPositions += span; + } + + const nonRestParamCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + const restTupleInfo = getRestTupleInfo(signature, nonRestParamCount, totalArgumentPositions); + + let signatureParamPos = 0; + for (let argIndex = 0; argIndex < args.length; argIndex++) { + const originalArg = args[argIndex]; + const arg = skipParentheses(originalArg); + const spreadArgs = argumentSpans[argIndex]; + if (spreadArgs === 0) { + continue; + } + if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { + signatureParamPos += spreadArgs; + continue; + } + + const parameterPos = getAdjustedParameterPosition(signatureParamPos, restTupleInfo); + const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, parameterPos); + signatureParamPos = signatureParamPos + (spreadArgs || 1); + if (identifierInfo) { + const { parameter, parameterName, isRestParameter: isFirstVariadicArgument } = identifierInfo; + const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { continue; } @@ -339,10 +340,68 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { continue; } - addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument); - } - } - } + addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument); + } + } + + function getArgumentSpan(arg: Expression) { + if (isSpreadElement(arg)) { + const spreadType = checker.getTypeAtLocation(arg.expression); + if (checker.isTupleType(spreadType)) { + const { elementFlags, fixedLength } = (spreadType as TupleTypeReference).target; + if (fixedLength === 0) { + return 0; + } + const firstOptionalIndex = findIndex(elementFlags, f => !(f & ElementFlags.Required)); + const requiredArgs = firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; + if (requiredArgs > 0) { + return requiredArgs; + } + } + } + return 1; + } + + function getRestTupleInfo(signature: Signature, paramCount: number, totalPositions: number) { + if (!signatureHasRestParameter(signature)) { + return undefined; + } + const restParameter = signature.parameters[paramCount]; + if (!restParameter) { + return undefined; + } + const restType = checker.getTypeOfSymbol(restParameter); + if (!checker.isTupleType(restType)) { + return undefined; + } + const elementFlags = (restType as TupleTypeReference).target.elementFlags; + const restStartIndex = findIndex(elementFlags, f => !!(f & ElementFlags.Variable)); + if (restStartIndex < 0) { + return undefined; + } + const restTailCount = elementFlags.length - restStartIndex - 1; + const restPositionsTotal = Math.max(0, totalPositions - paramCount); + return { restStartIndex, restTailCount, restPositionsTotal, paramCount }; + } + + function getAdjustedParameterPosition(signatureParamPos: number, restInfo?: { restStartIndex: number; restTailCount: number; restPositionsTotal: number; paramCount: number; }) { + if (!restInfo || signatureParamPos < restInfo.paramCount) { + return signatureParamPos; + } + const restPosition = signatureParamPos - restInfo.paramCount; + if (restPosition < restInfo.restStartIndex) { + return signatureParamPos; + } + if (restInfo.restTailCount > 0 && restInfo.restPositionsTotal >= restInfo.restTailCount) { + const tailStart = restInfo.restPositionsTotal - restInfo.restTailCount; + if (restPosition >= tailStart) { + const tailIndex = restPosition - tailStart; + return restInfo.paramCount + restInfo.restStartIndex + 1 + tailIndex; + } + } + return restInfo.paramCount + restInfo.restStartIndex; + } + } function identifierOrAccessExpressionPostfixMatchesParameterName(expr: Expression, parameterName: __String) { if (isIdentifier(expr)) { diff --git a/tests/baselines/reference/inlayHintsRestTupleTail.baseline b/tests/baselines/reference/inlayHintsRestTupleTail.baseline new file mode 100644 index 0000000000000..d37587619a821 --- /dev/null +++ b/tests/baselines/reference/inlayHintsRestTupleTail.baseline @@ -0,0 +1,63 @@ +// === Inlay Hints === +test(10, 'a', 'b', 'c') + ^ +{ + "text": "first:", + "position": 83, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "...middle:", + "position": 87, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "...middle:", + "position": 92, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "last:", + "position": 97, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "first:", + "position": 107, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "...middle:", + "position": 111, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "last:", + "position": 116, + "kind": "Parameter", + "whitespaceAfter": true +} \ No newline at end of file diff --git a/tests/cases/fourslash/inlayHintsRestTupleTail.ts b/tests/cases/fourslash/inlayHintsRestTupleTail.ts new file mode 100644 index 0000000000000..8899e8df9666b --- /dev/null +++ b/tests/cases/fourslash/inlayHintsRestTupleTail.ts @@ -0,0 +1,8 @@ +//// function test(...rest: [first: number, ...middle: string[], last: string]) {} +//// test(10, 'a', 'b', 'c') +//// test(10, 'a', 'c') + +verify.baselineInlayHints(undefined, { + includeInlayParameterNameHints: "all", + includeInlayFunctionParameterTypeHints: true, +});