From fb19edeee8025cf4462596192bba2f329c0b52c7 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 | 261 +++++++++++++++++- .../reference/inlayHintsDisplayParts.baseline | 47 ++++ .../reference/inlayHintsShouldWork27.baseline | 2 +- .../reference/inlayHintsShouldWork29.baseline | 2 +- .../reference/inlayHintsShouldWork52.baseline | 9 +- .../cases/fourslash/inlayHintsDisplayParts.ts | 8 + 6 files changed, 311 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..883a2848f3771 100644 --- a/src/services/inlayHints.ts +++ b/src/services/inlayHints.ts @@ -1,7 +1,9 @@ import { __String, + ArrayTypeNode, ArrowFunction, CallExpression, + ConditionalTypeNode, createPrinterWithRemoveComments, createTextSpanFromNode, Debug, @@ -23,10 +25,14 @@ import { getLeadingCommentRanges, hasContextSensitiveParameters, Identifier, + idText, + ImportTypeNode, + IndexedAccessTypeNode, InlayHint, InlayHintDisplayPart, InlayHintKind, InlayHintsContext, + IntersectionTypeNode, isArrowFunction, isAssertionExpression, isBindingPattern, @@ -53,12 +59,18 @@ import { isVarConst, isVariableDeclaration, MethodDeclaration, + NamedTupleMember, NewExpression, Node, + NodeArray, NodeBuilderFlags, + OptionalTypeNode, ParameterDeclaration, + ParenthesizedTypeNode, PrefixUnaryExpression, PropertyDeclaration, + QualifiedName, + RestTypeNode, Signature, skipParentheses, some, @@ -67,17 +79,23 @@ import { SymbolFlags, SyntaxKind, textSpanIntersectsWith, + tokenToString, + TupleTypeNode, TupleTypeReference, Type, TypeFormatFlags, + TypeNode, + TypeOperatorNode, + 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 +179,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 +193,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 +242,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 = typeToInlayHint(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 +374,10 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { return; } - const typeDisplayString = printTypeInSingleLine(returnType); - if (!typeDisplayString) { - return; + const hint = typeToInlayHint(returnType); + if (hint) { + addTypeHints(hint, getTypeAnnotationPosition(decl)); } - - addTypeHints(typeDisplayString, getTypeAnnotationPosition(decl)); } function getTypeAnnotationPosition(decl: FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration) { @@ -421,6 +439,219 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { }); } + function typeToInlayHint(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[] = []; + visitor(typeNode); + function visitor(node: Node) { + if (!node) { + return; + } + + 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; + visitor(qualifiedName.left); + parts.push({ text: "." }); + visitor(qualifiedName.right); + break; + case SyntaxKind.TypePredicate: + const predicate = node as TypePredicateNode; + if (predicate.assertsModifier) { + parts.push({ text: "asserts " }); + } + visitor(predicate.parameterName); + if (predicate.type) { + parts.push({ text: " is " }); + visitor(predicate.type); + } + break; + case SyntaxKind.TypeReference: + const typeReference = node as TypeReferenceNode; + visitor(typeReference.typeName); + if (typeReference.typeArguments) { + parts.push({ text: "<" }); + visitList(typeReference.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + case SyntaxKind.FunctionType: + // TODO: Handle this case. + break; + case SyntaxKind.ConstructorType: + // TODO: Handle this case. + break; + case SyntaxKind.TypeQuery: + const typeQuery = node as TypeQueryNode; + parts.push({ text: "typeof " }); + visitor(typeQuery.exprName); + if (typeQuery.typeArguments) { + parts.push({ text: "<" }); + visitList(typeQuery.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + case SyntaxKind.TypeLiteral: + // TODO: Handle this case. + break; + case SyntaxKind.ArrayType: + visitor((node as ArrayTypeNode).elementType); + parts.push({ text: "[]" }); + break; + case SyntaxKind.TupleType: + parts.push({ text: "[" }); + visitList((node as TupleTypeNode).elements, ","); + parts.push({ text: "]" }); + break; + case SyntaxKind.NamedTupleMember: + const member = node as NamedTupleMember; + if (member.dotDotDotToken) { + parts.push({ text: "..." }); + } + visitor(member.name); + if (member.questionToken) { + parts.push({ text: "?" }); + } + parts.push({ text: ": " }); + visitor(member.type); + break; + case SyntaxKind.OptionalType: + visitor((node as OptionalTypeNode).type); + parts.push({ text: "?" }); + break; + case SyntaxKind.RestType: + parts.push({ text: "..." }); + visitor((node as RestTypeNode).type); + break; + case SyntaxKind.UnionType: + visitList((node as UnionTypeNode).types, "|"); + break; + case SyntaxKind.IntersectionType: + visitList((node as IntersectionTypeNode).types, "&"); + break; + case SyntaxKind.ConditionalType: + const conditionalType = node as ConditionalTypeNode; + visitor(conditionalType.checkType); + parts.push({ text: " extends " }); + visitor(conditionalType.extendsType); + parts.push({ text: " ? " }); + visitor(conditionalType.trueType); + parts.push({ text: " : " }); + visitor(conditionalType.falseType); + break; + case SyntaxKind.InferType: + // TODO: Handle this case. + break; + case SyntaxKind.ParenthesizedType: + parts.push({ text: "(" }); + visitor((node as ParenthesizedTypeNode).type); + parts.push({ text: ")" }); + break; + case SyntaxKind.TypeOperator: + const typeOperator = node as TypeOperatorNode; + parts.push({ text: `${tokenToString(typeOperator.operator)} ` }); + visitor(typeOperator.type); + break; + case SyntaxKind.IndexedAccessType: + const indexedAccess = node as IndexedAccessTypeNode; + visitor(indexedAccess.objectType); + parts.push({ text: "[" }); + visitor(indexedAccess.indexType); + parts.push({ text: "]" }); + break; + case SyntaxKind.MappedType: + // TODO: Handle this case. + break; + case SyntaxKind.LiteralType: + // TODO: Handle this case. + break; + case SyntaxKind.TemplateLiteralType: + // TODO: Handle this case. + break; + case SyntaxKind.TemplateLiteralTypeSpan: + // TODO: Handle this case. + break; + case SyntaxKind.ImportType: + const importType = node as ImportTypeNode; + if (importType.isTypeOf) { + parts.push({ text: "typeof " }); + } + parts.push({ text: "import(" }); + visitor(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: "." }); + visitor(importType.qualifier); + } + if (importType.typeArguments) { + parts.push({ text: "<" }); + visitList(importType.typeArguments, ","); + parts.push({ text: ">" }); + } + break; + case SyntaxKind.ExpressionWithTypeArguments: + // TODO: Handle this case. + break; + // TODO: I _think_ that we don't display inlay hints in JSDocs, + // so I shouldn't worry about these cases (?). + // case SyntaxKind.JSDocTypeExpression: + // case SyntaxKind.JSDocAllType: + // case SyntaxKind.JSDocUnknownType: + // case SyntaxKind.JSDocNonNullableType: + // case SyntaxKind.JSDocNullableType: + // case SyntaxKind.JSDocOptionalType: + // case SyntaxKind.JSDocFunctionType: + // case SyntaxKind.JSDocVariadicType: + // case SyntaxKind.JSDocNamepathType: + // case SyntaxKind.JSDocSignature: + // case SyntaxKind.JSDocTypeLiteral: + default: + Debug.fail("Type node does not support inlay hints."); + } + } + function visitList(nodes: NodeArray, separator: string) { + nodes.forEach((node, index) => { + if (index > 0) { + parts.push({ text: `${separator} ` }); + } + visitor(node); + }); + } + + return parts; + } + function isUndefined(name: __String) { return name === "undefined"; } @@ -433,7 +664,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 })