diff --git a/src/services/inlayHints.ts b/src/services/inlayHints.ts index 5bdcb560113d8..a7e6f78c878e3 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,13 +59,18 @@ import { isVarConst, isVariableDeclaration, MethodDeclaration, + NamedTupleMember, NewExpression, Node, + NodeArray, NodeBuilderFlags, + OptionalTypeNode, ParameterDeclaration, ParenthesizedTypeNode, PrefixUnaryExpression, PropertyDeclaration, + QualifiedName, + RestTypeNode, Signature, skipParentheses, some, @@ -69,11 +80,17 @@ import { SyntaxKind, textSpanIntersectsWith, tokenToString, + TupleTypeNode, TupleTypeReference, Type, TypeFormatFlags, TypeNode, + TypeOperatorNode, + TypePredicateNode, + TypeQueryNode, + TypeReferenceNode, unescapeLeadingUnderscores, + UnionTypeNode, UserPreferences, usingSingleLineStringWriter, VariableDeclaration, @@ -162,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 += ":"; @@ -446,13 +463,12 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { const parts: InlayHintDisplayPart[] = []; visitor(typeNode); - function visitor(node: TypeNode): true | undefined { + function visitor(node: Node) { if (!node) { return; } switch (node.kind) { - // Keyword types: case SyntaxKind.AnyKeyword: case SyntaxKind.BigIntKeyword: case SyntaxKind.BooleanKeyword: @@ -465,18 +481,186 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { case SyntaxKind.UndefinedKeyword: case SyntaxKind.UnknownKeyword: case SyntaxKind.VoidKeyword: - parts.push({ text: tokenToString(node.kind) }); + 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: - // TODO: Make this unreachable when I consider all cases. - return undefined; + 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; } @@ -493,7 +677,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),