From 420610a06075f8a70d67579208aadcd235597507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maria=20Jos=C3=A9=20Solano?= Date: Mon, 24 Jul 2023 14:08:09 -0700 Subject: [PATCH] Display parts for types Parenthesized types Baseline update Parts for keyword types Fill up the visitor switch Handling a bunch of other easy cases (part 2) --- src/services/inlayHints.ts | 305 +++++++++++++++++- .../reference/inlayHintsDisplayParts.baseline | 47 +++ .../reference/inlayHintsShouldWork27.baseline | 2 +- .../reference/inlayHintsShouldWork29.baseline | 2 +- .../reference/inlayHintsShouldWork52.baseline | 9 +- .../cases/fourslash/inlayHintsDisplayParts.ts | 8 + 6 files changed, 355 insertions(+), 18 deletions(-) create mode 100644 tests/baselines/reference/inlayHintsDisplayParts.baseline create mode 100644 tests/cases/fourslash/inlayHintsDisplayParts.ts diff --git a/src/services/inlayHints.ts b/src/services/inlayHints.ts index 9eb3d7aa4ea56..6ec1a67e34e24 100644 --- a/src/services/inlayHints.ts +++ b/src/services/inlayHints.ts @@ -1,7 +1,10 @@ import { __String, + ArrayTypeNode, ArrowFunction, CallExpression, + ConditionalTypeNode, + ConstructorTypeNode, createPrinterWithRemoveComments, createTextSpanFromNode, Debug, @@ -23,10 +26,15 @@ import { getLeadingCommentRanges, hasContextSensitiveParameters, Identifier, + idText, + ImportTypeNode, + IndexedAccessTypeNode, + InferTypeNode, InlayHint, InlayHintDisplayPart, InlayHintKind, InlayHintsContext, + IntersectionTypeNode, isArrowFunction, isAssertionExpression, isBindingPattern, @@ -52,13 +60,21 @@ import { isTypeNode, isVarConst, isVariableDeclaration, + LiteralTypeNode, + MappedTypeNode, MethodDeclaration, + NamedTupleMember, NewExpression, Node, + NodeArray, NodeBuilderFlags, + OptionalTypeNode, ParameterDeclaration, + ParenthesizedTypeNode, PrefixUnaryExpression, PropertyDeclaration, + QualifiedName, + RestTypeNode, Signature, skipParentheses, some, @@ -67,17 +83,24 @@ import { SymbolFlags, SyntaxKind, textSpanIntersectsWith, + tokenToString, + TupleTypeNode, TupleTypeReference, Type, TypeFormatFlags, + TypeNode, + TypeOperatorNode, + TypeParameterDeclaration, + TypePredicateNode, + TypeQueryNode, + TypeReferenceNode, unescapeLeadingUnderscores, + UnionTypeNode, UserPreferences, usingSingleLineStringWriter, VariableDeclaration, } from "./_namespaces/ts"; -const maxTypeHintLength = 30; - const leadingParameterNameCommentRegexFactory = (name: string) => { return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`); }; @@ -161,7 +184,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { function addParameterHints(text: string, parameter: Identifier, position: number, isFirstVariadicArgument: boolean, sourceFile: SourceFile | undefined) { let hintText: string | InlayHintDisplayPart[] = `${isFirstVariadicArgument ? "..." : ""}${text}`; if (shouldUseInteractiveInlayHints(preferences)) { - hintText = [getNodeDisplayPart(hintText, parameter, sourceFile!), { text: ":" }]; + hintText = [getNodeDisplayPart(hintText, parameter, sourceFile), { text: ":" }]; } else { hintText += ":"; @@ -175,9 +198,10 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { }); } - function addTypeHints(text: string, position: number) { + function addTypeHints(hintText: string | InlayHintDisplayPart[], position: number) { + const text = typeof hintText === "string" ? `: ${hintText}` : [{ text: ": " }, ...hintText]; result.push({ - text: `: ${text.length > maxTypeHintLength ? text.substr(0, maxTypeHintLength - "...".length) + "..." : text}`, + text, position, kind: InlayHintKind.Type, whitespaceBefore: true, @@ -223,13 +247,14 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { return; } - const typeDisplayString = printTypeInSingleLine(declarationType); - if (typeDisplayString) { - const isVariableNameMatchesType = preferences.includeInlayVariableTypeHintsWhenTypeMatchesName === false && equateStringsCaseInsensitive(decl.name.getText(), typeDisplayString); + const hint = typeToInlayHintParts(declarationType); + if (hint) { + const hintText = typeof hint === "string" ? hint : hint.map(part => part.text).join(""); + const isVariableNameMatchesType = preferences.includeInlayVariableTypeHintsWhenTypeMatchesName === false && equateStringsCaseInsensitive(decl.name.getText(), hintText); if (isVariableNameMatchesType) { return; } - addTypeHints(typeDisplayString, decl.name.end); + addTypeHints(hint, decl.name.end); } } @@ -354,12 +379,10 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { return; } - const typeDisplayString = printTypeInSingleLine(returnType); - if (!typeDisplayString) { - return; + const hint = typeToInlayHintParts(returnType); + if (hint) { + addTypeHints(hint, getTypeAnnotationPosition(decl)); } - - addTypeHints(typeDisplayString, getTypeAnnotationPosition(decl)); } function getTypeAnnotationPosition(decl: FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration) { @@ -421,6 +444,258 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { }); } + function typeToInlayHintParts(type: Type): InlayHintDisplayPart[] | string { + if (!shouldUseInteractiveInlayHints(preferences)) { + return printTypeInSingleLine(type); + } + + const flags = NodeBuilderFlags.IgnoreErrors | TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope; + const typeNode = checker.typeToTypeNode(type, /*enclosingDeclaration*/ undefined, flags); + Debug.assertIsDefined(typeNode, "should always get typenode"); + + const parts: InlayHintDisplayPart[] = []; + visitDisplayPart(typeNode); + function visitDisplayPart(node: Node) { + if (!node) { + return; + } + + if (preferences.includeInlayFunctionParameterTypeHints && isFunctionLikeDeclaration(node) && hasContextSensitiveParameters(node)) { + visitFunctionLikeForParameterType(node); + } + if (preferences.includeInlayFunctionLikeReturnTypeHints && isSignatureSupportingReturnAnnotation(node)) { + visitFunctionDeclarationLikeForReturnType(node); + } + + switch (node.kind) { + case SyntaxKind.AnyKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.IntrinsicKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.UndefinedKeyword: + case SyntaxKind.UnknownKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.ThisType: + parts.push({ text: tokenToString(node.kind)! }); + break; + case SyntaxKind.Identifier: + const identifier = node as Identifier; + parts.push(getNodeDisplayPart(idText(identifier), identifier)); + break; + case SyntaxKind.QualifiedName: + const qualifiedName = node as QualifiedName; + visitDisplayPart(qualifiedName.left); + parts.push({ text: "." }); + visitDisplayPart(qualifiedName.right); + break; + case SyntaxKind.TypePredicate: + const predicate = node as TypePredicateNode; + if (predicate.assertsModifier) { + parts.push({ text: "asserts " }); + } + visitDisplayPart(predicate.parameterName); + if (predicate.type) { + parts.push({ text: " is " }); + visitDisplayPart(predicate.type); + } + break; + case SyntaxKind.TypeReference: + const typeReference = node as TypeReferenceNode; + visitDisplayPart(typeReference.typeName); + if (typeReference.typeArguments) { + parts.push({ text: "<" }); + visitDisplayPartList(typeReference.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + case SyntaxKind.TypeParameter: + // TODO: Modifier list. + const typeParameter = node as TypeParameterDeclaration; + visitDisplayPart(typeParameter.name); + if (typeParameter.constraint) { + parts.push({ text: " extends " }); + visitDisplayPart(typeParameter.constraint); + } + if (typeParameter.default) { + parts.push({ text: " = " }); + visitDisplayPart(typeParameter.default); + } + break; + case SyntaxKind.ConstructorType: + const constructorType = node as ConstructorTypeNode; + parts.push({ text: "new " }); + if (constructorType.typeParameters) { + parts.push({ text: "<" }); + visitDisplayPartList(constructorType.typeParameters, ","); + parts.push({ text: ">" }); + } + // TODO: Parameters. + parts.push({ text: " => " }); + visitDisplayPart(constructorType.type); + break; + case SyntaxKind.TypeQuery: + const typeQuery = node as TypeQueryNode; + parts.push({ text: "typeof " }); + visitDisplayPart(typeQuery.exprName); + if (typeQuery.typeArguments) { + parts.push({ text: "<" }); + visitDisplayPartList(typeQuery.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + case SyntaxKind.TypeLiteral: + parts.push({ text: "{" }); + // TODO: Members. + parts.push({ text: "}" }); + break; + case SyntaxKind.ArrayType: + visitDisplayPart((node as ArrayTypeNode).elementType); + parts.push({ text: "[]" }); + break; + case SyntaxKind.TupleType: + parts.push({ text: "[" }); + visitDisplayPartList((node as TupleTypeNode).elements, ","); + parts.push({ text: "]" }); + break; + case SyntaxKind.NamedTupleMember: + const member = node as NamedTupleMember; + if (member.dotDotDotToken) { + parts.push({ text: "..." }); + } + visitDisplayPart(member.name); + if (member.questionToken) { + parts.push({ text: "?" }); + } + parts.push({ text: ": " }); + visitDisplayPart(member.type); + break; + case SyntaxKind.OptionalType: + visitDisplayPart((node as OptionalTypeNode).type); + parts.push({ text: "?" }); + break; + case SyntaxKind.RestType: + parts.push({ text: "..." }); + visitDisplayPart((node as RestTypeNode).type); + break; + case SyntaxKind.UnionType: + visitDisplayPartList((node as UnionTypeNode).types, "|"); + break; + case SyntaxKind.IntersectionType: + visitDisplayPartList((node as IntersectionTypeNode).types, "&"); + break; + case SyntaxKind.ConditionalType: + const conditionalType = node as ConditionalTypeNode; + visitDisplayPart(conditionalType.checkType); + parts.push({ text: " extends " }); + visitDisplayPart(conditionalType.extendsType); + parts.push({ text: " ? " }); + visitDisplayPart(conditionalType.trueType); + parts.push({ text: " : " }); + visitDisplayPart(conditionalType.falseType); + break; + case SyntaxKind.InferType: + parts.push({ text: "infer " }); + visitDisplayPart((node as InferTypeNode).typeParameter); + break; + case SyntaxKind.ParenthesizedType: + parts.push({ text: "(" }); + visitDisplayPart((node as ParenthesizedTypeNode).type); + parts.push({ text: ")" }); + break; + case SyntaxKind.TypeOperator: + const typeOperator = node as TypeOperatorNode; + parts.push({ text: `${tokenToString(typeOperator.operator)} ` }); + visitDisplayPart(typeOperator.type); + break; + case SyntaxKind.IndexedAccessType: + const indexedAccess = node as IndexedAccessTypeNode; + visitDisplayPart(indexedAccess.objectType); + parts.push({ text: "[" }); + visitDisplayPart(indexedAccess.indexType); + parts.push({ text: "]" }); + break; + case SyntaxKind.MappedType: + const mappedType = node as MappedTypeNode; + parts.push({ text: "{ " }); + if (mappedType.readonlyToken) { + if (mappedType.readonlyToken.kind === SyntaxKind.PlusToken) { + parts.push({ text: "+" }); + } + else if (mappedType.readonlyToken.kind === SyntaxKind.MinusToken) { + parts.push({ text: "-" }); + } + parts.push({ text: "readonly " }); + } + parts.push({ text: "[" }); + visitDisplayPart(mappedType.typeParameter); + if (mappedType.nameType) { + parts.push({ text: " as " }); + visitDisplayPart(mappedType.nameType); + } + parts.push({ text: "]" }); + if (mappedType.questionToken) { + if (mappedType.questionToken.kind === SyntaxKind.PlusToken) { + parts.push({ text: "+" }); + } + else if (mappedType.questionToken.kind === SyntaxKind.MinusToken) { + parts.push({ text: "-" }); + } + parts.push({ text: "?" }); + } + parts.push({ text: ": " }); + if (mappedType.type) { + visitDisplayPart(mappedType.type); + } + parts.push({ text: "; " }); + // TODO: Members + parts.push({ text: "}" }); + break; + case SyntaxKind.LiteralType: + visitDisplayPart((node as LiteralTypeNode).literal); + break; + case SyntaxKind.ImportType: + const importType = node as ImportTypeNode; + if (importType.isTypeOf) { + parts.push({ text: "typeof " }); + } + parts.push({ text: "import(" }); + visitDisplayPart(importType.argument); + if (importType.assertions) { + parts.push({ text: ", { assert: " }); + // TODO: Visit assert clause entries. + parts.push({ text: " }" }); + } + parts.push({ text: ")" }); + if (importType.qualifier) { + parts.push({ text: "." }); + visitDisplayPart(importType.qualifier); + } + if (importType.typeArguments) { + parts.push({ text: "<" }); + visitDisplayPartList(importType.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + } + } + + function visitDisplayPartList(nodes: NodeArray, separator: string) { + nodes.forEach((node, index) => { + if (index > 0) { + parts.push({ text: `${separator} ` }); + } + visitDisplayPart(node); + }); + } + + return parts; + } + function isUndefined(name: __String) { return name === "undefined"; } @@ -433,7 +708,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { return true; } - function getNodeDisplayPart(text: string, node: Node, sourceFile: SourceFile): InlayHintDisplayPart { + function getNodeDisplayPart(text: string, node: Node, sourceFile: SourceFile = node.getSourceFile()): InlayHintDisplayPart { return { text, span: createTextSpanFromNode(node, sourceFile), diff --git a/tests/baselines/reference/inlayHintsDisplayParts.baseline b/tests/baselines/reference/inlayHintsDisplayParts.baseline new file mode 100644 index 0000000000000..246ed65caf92b --- /dev/null +++ b/tests/baselines/reference/inlayHintsDisplayParts.baseline @@ -0,0 +1,47 @@ +function foo1() { return 1; } + ^ +{ + "text": [ + { + "text": ": " + }, + { + "text": "number" + } + ], + "position": 15, + "kind": "Type", + "whitespaceBefore": true +} + +function foo2() { return "foo"; } + ^ +{ + "text": [ + { + "text": ": " + }, + { + "text": "string" + } + ], + "position": 45, + "kind": "Type", + "whitespaceBefore": true +} + +function foo3() { } + ^ +{ + "text": [ + { + "text": ": " + }, + { + "text": "void" + } + ], + "position": 79, + "kind": "Type", + "whitespaceBefore": true +} \ No newline at end of file diff --git a/tests/baselines/reference/inlayHintsShouldWork27.baseline b/tests/baselines/reference/inlayHintsShouldWork27.baseline index f9dc800b3837e..69e718f4d0e11 100644 --- a/tests/baselines/reference/inlayHintsShouldWork27.baseline +++ b/tests/baselines/reference/inlayHintsShouldWork27.baseline @@ -1,7 +1,7 @@ foo(a => { ^ { - "text": ": (c: (d: 2 | 3) => void) => ...", + "text": ": (c: (d: 2 | 3) => void) => void", "position": 87, "kind": "Type", "whitespaceBefore": true diff --git a/tests/baselines/reference/inlayHintsShouldWork29.baseline b/tests/baselines/reference/inlayHintsShouldWork29.baseline index 8e2ad45030f9a..676c012877799 100644 --- a/tests/baselines/reference/inlayHintsShouldWork29.baseline +++ b/tests/baselines/reference/inlayHintsShouldWork29.baseline @@ -10,7 +10,7 @@ foo(a => { foo(a => { ^ { - "text": ": (c: (d: 2 | 3) => void) => ...", + "text": ": (c: (d: 2 | 3) => void) => void", "position": 87, "kind": "Type", "whitespaceBefore": true diff --git a/tests/baselines/reference/inlayHintsShouldWork52.baseline b/tests/baselines/reference/inlayHintsShouldWork52.baseline index 7568d4fe52f65..bbfd31b3c7db1 100644 --- a/tests/baselines/reference/inlayHintsShouldWork52.baseline +++ b/tests/baselines/reference/inlayHintsShouldWork52.baseline @@ -1,7 +1,14 @@ function foo (aParameter: number, bParameter: number, cParameter: number) { } ^ { - "text": ": void", + "text": [ + { + "text": ": " + }, + { + "text": "void" + } + ], "position": 73, "kind": "Type", "whitespaceBefore": true diff --git a/tests/cases/fourslash/inlayHintsDisplayParts.ts b/tests/cases/fourslash/inlayHintsDisplayParts.ts new file mode 100644 index 0000000000000..0b850e22451ca --- /dev/null +++ b/tests/cases/fourslash/inlayHintsDisplayParts.ts @@ -0,0 +1,8 @@ +/// + +// Keyword types: +////function foo1() { return 1; } +////function foo2() { return "foo"; } +////function foo3() { } + +verify.baselineInlayHints(undefined, { interactiveInlayHints: true, includeInlayFunctionLikeReturnTypeHints: true })