Skip to content

Commit 069de74

Browse files
authored
Expandable hover (#61492)
1 parent eef7c14 commit 069de74

File tree

70 files changed

+32173
-289
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+32173
-289
lines changed

src/compiler/checker.ts

+557-112
Large diffs are not rendered by default.

src/compiler/emitter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,7 @@ export const notImplementedResolver: EmitResolver = {
11681168
isImportRequiredByAugmentation: notImplemented,
11691169
isDefinitelyReferenceToGlobalSymbolObject: notImplemented,
11701170
createLateBoundIndexSignatures: notImplemented,
1171+
symbolToDeclarations: notImplemented,
11711172
};
11721173

11731174
const enum PipelinePhase {

src/compiler/types.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -5046,6 +5046,15 @@ export interface TypeCheckerHost extends ModuleSpecifierResolutionHost, SourceFi
50465046

50475047
typesPackageExists(packageName: string): boolean;
50485048
packageBundlesTypes(packageName: string): boolean;
5049+
5050+
isSourceFileDefaultLibrary(file: SourceFile): boolean;
5051+
}
5052+
5053+
/** @internal */
5054+
export interface WriterContextOut {
5055+
/** Whether increasing the expansion depth will cause us to expand more types. */
5056+
canIncreaseExpansionDepth: boolean;
5057+
truncated: boolean;
50495058
}
50505059

50515060
export interface TypeChecker {
@@ -5134,6 +5143,7 @@ export interface TypeChecker {
51345143
symbolToParameterDeclaration(symbol: Symbol, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): ParameterDeclaration | undefined;
51355144
/** Note that the resulting nodes cannot be checked. */
51365145
typeParameterToDeclaration(parameter: TypeParameter, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): TypeParameterDeclaration | undefined;
5146+
/** @internal */ typeParameterToDeclaration(parameter: TypeParameter, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker, verbosityLevel?: number, out?: WriterContextOut): TypeParameterDeclaration | undefined; // eslint-disable-line @typescript-eslint/unified-signatures
51375147

51385148
getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[];
51395149
getSymbolAtLocation(node: Node): Symbol | undefined;
@@ -5165,8 +5175,8 @@ export interface TypeChecker {
51655175
symbolToString(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags?: SymbolFormatFlags): string;
51665176
typePredicateToString(predicate: TypePredicate, enclosingDeclaration?: Node, flags?: TypeFormatFlags): string;
51675177

5168-
/** @internal */ writeSignature(signature: Signature, enclosingDeclaration?: Node, flags?: TypeFormatFlags, kind?: SignatureKind, writer?: EmitTextWriter): string;
5169-
/** @internal */ writeType(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter): string;
5178+
/** @internal */ writeSignature(signature: Signature, enclosingDeclaration?: Node, flags?: TypeFormatFlags, kind?: SignatureKind, writer?: EmitTextWriter, verbosityLevel?: number, out?: WriterContextOut): string;
5179+
/** @internal */ writeType(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter, verbosityLevel?: number, out?: WriterContextOut): string;
51705180
/** @internal */ writeSymbol(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags?: SymbolFormatFlags, writer?: EmitTextWriter): string;
51715181
/** @internal */ writeTypePredicate(predicate: TypePredicate, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter): string;
51725182

@@ -5435,6 +5445,7 @@ export interface TypeChecker {
54355445
/** @internal */ fillMissingTypeArguments(typeArguments: readonly Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[];
54365446

54375447
getTypeArgumentsForResolvedSignature(signature: Signature): readonly Type[] | undefined;
5448+
/** @internal */ isLibType(type: Type): boolean;
54385449
}
54395450

54405451
/** @internal */
@@ -5881,6 +5892,7 @@ export interface EmitResolver {
58815892
isImportRequiredByAugmentation(decl: ImportDeclaration): boolean;
58825893
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
58835894
createLateBoundIndexSignatures(cls: ClassLikeDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): (IndexSignatureDeclaration | PropertyDeclaration)[] | undefined;
5895+
symbolToDeclarations(symbol: Symbol, meaning: SymbolFlags, flags: NodeBuilderFlags, verbosityLevel?: number, out?: WriterContextOut): Declaration[];
58845896
}
58855897

58865898
// dprint-ignore

src/compiler/utilities.ts

+122
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
__String,
33
AccessExpression,
44
AccessorDeclaration,
5+
addEmitFlags,
56
addRange,
67
affectsDeclarationPathOptionDeclarations,
78
affectsEmitOptionDeclarations,
@@ -511,6 +512,8 @@ import {
511512
ScriptTarget,
512513
semanticDiagnosticsOptionDeclarations,
513514
SetAccessorDeclaration,
515+
setOriginalNode,
516+
setTextRange,
514517
ShorthandPropertyAssignment,
515518
shouldAllowImportingTsExtension,
516519
Signature,
@@ -592,6 +595,7 @@ import {
592595
VariableDeclarationList,
593596
VariableLikeDeclaration,
594597
VariableStatement,
598+
visitEachChild,
595599
WhileStatement,
596600
WithStatement,
597601
WrappedExpression,
@@ -12208,3 +12212,121 @@ export function getOptionsSyntaxByValue(optionsObject: ObjectLiteralExpression |
1220812212
export function forEachOptionsSyntaxByName<T>(optionsObject: ObjectLiteralExpression | undefined, name: string, callback: (prop: PropertyAssignment) => T | undefined): T | undefined {
1220912213
return forEachPropertyAssignment(optionsObject, name, callback);
1221012214
}
12215+
12216+
/**
12217+
* Creates a deep, memberwise clone of a node with no source map location.
12218+
*
12219+
* WARNING: This is an expensive operation and is only intended to be used in refactorings
12220+
* and code fixes (because those are triggered by explicit user actions).
12221+
*
12222+
* @internal
12223+
*/
12224+
// Moved here to compiler utilities for usage in node builder for quickinfo.
12225+
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true): T {
12226+
const clone = node && getSynthesizedDeepCloneWorker(node);
12227+
if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone);
12228+
return setParentRecursive(clone, /*incremental*/ false);
12229+
}
12230+
12231+
/** @internal */
12232+
export function getSynthesizedDeepCloneWithReplacements<T extends Node>(
12233+
node: T,
12234+
includeTrivia: boolean,
12235+
replaceNode: (node: Node) => Node | undefined,
12236+
): T {
12237+
let clone = replaceNode(node);
12238+
if (clone) {
12239+
setOriginalNode(clone, node);
12240+
}
12241+
else {
12242+
clone = getSynthesizedDeepCloneWorker(node as NonNullable<T>, replaceNode);
12243+
}
12244+
12245+
if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone);
12246+
return clone as T;
12247+
}
12248+
12249+
function getSynthesizedDeepCloneWorker<T extends Node>(node: T, replaceNode?: (node: Node) => Node | undefined): T {
12250+
const nodeClone: <T extends Node>(n: T) => T = replaceNode
12251+
? n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode)
12252+
: getSynthesizedDeepClone;
12253+
const nodesClone: <T extends Node>(ns: NodeArray<T> | undefined) => NodeArray<T> | undefined = replaceNode
12254+
? ns => ns && getSynthesizedDeepClonesWithReplacements(ns, /*includeTrivia*/ true, replaceNode)
12255+
: ns => ns && getSynthesizedDeepClones(ns);
12256+
const visited = visitEachChild(node, nodeClone, /*context*/ undefined, nodesClone, nodeClone);
12257+
12258+
if (visited === node) {
12259+
// This only happens for leaf nodes - internal nodes always see their children change.
12260+
const clone = isStringLiteral(node) ? setOriginalNode(factory.createStringLiteralFromNode(node), node) as Node as T :
12261+
isNumericLiteral(node) ? setOriginalNode(factory.createNumericLiteral(node.text, node.numericLiteralFlags), node) as Node as T :
12262+
factory.cloneNode(node);
12263+
return setTextRange(clone, node);
12264+
}
12265+
12266+
// PERF: As an optimization, rather than calling factory.cloneNode, we'll update
12267+
// the new node created by visitEachChild with the extra changes factory.cloneNode
12268+
// would have made.
12269+
(visited as Mutable<T>).parent = undefined!;
12270+
return visited;
12271+
}
12272+
12273+
/** @internal */
12274+
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T>, includeTrivia?: boolean): NodeArray<T>;
12275+
/** @internal */
12276+
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia?: boolean): NodeArray<T> | undefined;
12277+
/** @internal */
12278+
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia = true): NodeArray<T> | undefined {
12279+
if (nodes) {
12280+
const cloned = factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma);
12281+
setTextRange(cloned, nodes);
12282+
return cloned;
12283+
}
12284+
return nodes;
12285+
}
12286+
12287+
/** @internal */
12288+
export function getSynthesizedDeepClonesWithReplacements<T extends Node>(
12289+
nodes: NodeArray<T>,
12290+
includeTrivia: boolean,
12291+
replaceNode: (node: Node) => Node | undefined,
12292+
): NodeArray<T> {
12293+
return factory.createNodeArray(nodes.map(n => getSynthesizedDeepCloneWithReplacements(n, includeTrivia, replaceNode)), nodes.hasTrailingComma);
12294+
}
12295+
12296+
/**
12297+
* Sets EmitFlags to suppress leading and trailing trivia on the node.
12298+
*
12299+
* @internal
12300+
*/
12301+
export function suppressLeadingAndTrailingTrivia(node: Node): void {
12302+
suppressLeadingTrivia(node);
12303+
suppressTrailingTrivia(node);
12304+
}
12305+
12306+
/**
12307+
* Sets EmitFlags to suppress leading trivia on the node.
12308+
*
12309+
* @internal
12310+
*/
12311+
export function suppressLeadingTrivia(node: Node): void {
12312+
addEmitFlagsRecursively(node, EmitFlags.NoLeadingComments, getFirstChild);
12313+
}
12314+
12315+
/**
12316+
* Sets EmitFlags to suppress trailing trivia on the node.
12317+
*
12318+
* @internal @knipignore
12319+
*/
12320+
export function suppressTrailingTrivia(node: Node): void {
12321+
addEmitFlagsRecursively(node, EmitFlags.NoTrailingComments, getLastChild);
12322+
}
12323+
12324+
function addEmitFlagsRecursively(node: Node, flag: EmitFlags, getChild: (n: Node) => Node | undefined) {
12325+
addEmitFlags(node, flag);
12326+
const child = getChild(node);
12327+
if (child) addEmitFlagsRecursively(child, flag, getChild);
12328+
}
12329+
12330+
function getFirstChild(node: Node): Node | undefined {
12331+
return forEachChild(node, child => child);
12332+
}

src/harness/client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ export class SessionClient implements LanguageService {
254254
return { line, character: offset };
255255
}
256256

257-
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
258-
const args = this.createFileLocationRequestArgs(fileName, position);
257+
getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel?: number | undefined): QuickInfo {
258+
const args = { ...this.createFileLocationRequestArgs(fileName, position), verbosityLevel };
259259

260260
const request = this.processRequest<protocol.QuickInfoRequest>(protocol.CommandTypes.Quickinfo, args);
261261
const response = this.processResponse<protocol.QuickInfoResponse>(request);
@@ -268,6 +268,7 @@ export class SessionClient implements LanguageService {
268268
displayParts: [{ kind: "text", text: body.displayString }],
269269
documentation: typeof body.documentation === "string" ? [{ kind: "text", text: body.documentation }] : body.documentation,
270270
tags: this.decodeLinkDisplayParts(body.tags),
271+
canIncreaseVerbosityLevel: body.canIncreaseVerbosityLevel,
271272
};
272273
}
273274

src/harness/fourslashImpl.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export interface TextSpan {
8686
end: number;
8787
}
8888

89+
export interface VerbosityLevels {
90+
[markerName: string]: number | number[] | undefined;
91+
}
92+
8993
// Name of testcase metadata including ts.CompilerOptions properties that will be used by globalOptions
9094
// To add additional option, add property into the testOptMetadataNames, refer the property in either globalMetadataNames or fileMetadataNames
9195
// Add cases into convertGlobalOptionsToCompilationsSettings function for the compiler to acknowledge such option from meta data
@@ -2451,19 +2455,28 @@ export class TestState {
24512455
return result;
24522456
}
24532457

2454-
public baselineQuickInfo(): void {
2455-
const result = ts.arrayFrom(this.testData.markerPositions.entries(), ([name, marker]) => ({
2456-
marker: { ...marker, name },
2457-
item: this.languageService.getQuickInfoAtPosition(marker.fileName, marker.position),
2458-
}));
2458+
public baselineQuickInfo(verbosityLevels?: VerbosityLevels): void {
2459+
const result = ts.arrayFrom(this.testData.markerPositions.entries(), ([name, marker]) => {
2460+
const verbosityLevel = toArray(verbosityLevels?.[name]);
2461+
const items = verbosityLevel.map(verbosityLevel => {
2462+
const item: ts.QuickInfo & { verbosityLevel?: number; } | undefined = this.languageService.getQuickInfoAtPosition(marker.fileName, marker.position, verbosityLevel);
2463+
if (item) item.verbosityLevel = verbosityLevel;
2464+
return {
2465+
marker: { ...marker, name },
2466+
item,
2467+
};
2468+
});
2469+
return items;
2470+
}).flat();
24592471
const annotations = this.annotateContentWithTooltips(
24602472
result,
24612473
"quickinfo",
24622474
item => item.textSpan,
2463-
({ displayParts, documentation, tags }) => [
2475+
({ displayParts, documentation, tags, verbosityLevel }) => [
24642476
...(displayParts ? displayParts.map(p => p.text).join("").split("\n") : []),
24652477
...(documentation?.length ? documentation.map(p => p.text).join("").split("\n") : []),
24662478
...(tags?.length ? tags.map(p => `@${p.name} ${p.text?.map(dp => dp.text).join("") ?? ""}`).join("\n").split("\n") : []),
2479+
...(verbosityLevel !== undefined ? [`(verbosity level: ${verbosityLevel})`] : []),
24672480
],
24682481
);
24692482
this.baseline("QuickInfo", annotations + "\n\n" + stringify(result));

src/harness/fourslashInterfaceImpl.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ export class Verify extends VerifyNegatable {
449449
this.state.baselineGetEmitOutput();
450450
}
451451

452-
public baselineQuickInfo(): void {
453-
this.state.baselineQuickInfo();
452+
public baselineQuickInfo(verbosityLevels?: FourSlash.VerbosityLevels): void {
453+
this.state.baselineQuickInfo(verbosityLevels);
454454
}
455455

456456
public baselineSignatureHelp(): void {

src/server/protocol.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,14 @@ export interface QuickInfoRequest extends FileLocationRequest {
20052005
arguments: FileLocationRequestArgs;
20062006
}
20072007

2008+
export interface QuickInfoRequestArgs extends FileLocationRequestArgs {
2009+
/**
2010+
* This controls how many levels of definitions will be expanded in the quick info response.
2011+
* The default value is 0.
2012+
*/
2013+
verbosityLevel?: number;
2014+
}
2015+
20082016
/**
20092017
* Body of QuickInfoResponse.
20102018
*/
@@ -2044,6 +2052,11 @@ export interface QuickInfoResponseBody {
20442052
* JSDoc tags associated with symbol.
20452053
*/
20462054
tags: JSDocTagInfo[];
2055+
2056+
/**
2057+
* Whether the verbosity level can be increased for this quick info response.
2058+
*/
2059+
canIncreaseVerbosityLevel?: boolean;
20472060
}
20482061

20492062
/**

src/server/session.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2392,10 +2392,10 @@ export class Session<TMessage = string> implements EventSender {
23922392
return languageService.isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0));
23932393
}
23942394

2395-
private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
2395+
private getQuickInfoWorker(args: protocol.QuickInfoRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
23962396
const { file, project } = this.getFileAndProject(args);
23972397
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
2398-
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo));
2398+
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo), args.verbosityLevel);
23992399
if (!quickInfo) {
24002400
return undefined;
24012401
}
@@ -2411,6 +2411,7 @@ export class Session<TMessage = string> implements EventSender {
24112411
displayString,
24122412
documentation: useDisplayParts ? this.mapDisplayParts(quickInfo.documentation, project) : displayPartsToString(quickInfo.documentation),
24132413
tags: this.mapJSDocTagInfo(quickInfo.tags, project, useDisplayParts),
2414+
canIncreaseVerbosityLevel: quickInfo.canIncreaseVerbosityLevel,
24142415
};
24152416
}
24162417
else {

src/services/services.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -2274,7 +2274,7 @@ export function createLanguageService(
22742274
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences);
22752275
}
22762276

2277-
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
2277+
function getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel?: number): QuickInfo | undefined {
22782278
synchronizeHostData();
22792279

22802280
const sourceFile = getValidSourceFile(fileName);
@@ -2293,20 +2293,34 @@ export function createLanguageService(
22932293
kind: ScriptElementKind.unknown,
22942294
kindModifiers: ScriptElementKindModifier.none,
22952295
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
2296-
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))),
2296+
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo), /*flags*/ undefined, verbosityLevel)),
22972297
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
22982298
tags: type.symbol ? type.symbol.getJsDocTags(typeChecker) : undefined,
22992299
};
23002300
}
23012301

2302-
const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo));
2302+
const { symbolKind, displayParts, documentation, tags, canIncreaseVerbosityLevel } = typeChecker.runWithCancellationToken(
2303+
cancellationToken,
2304+
typeChecker =>
2305+
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(
2306+
typeChecker,
2307+
symbol,
2308+
sourceFile,
2309+
getContainerNode(nodeForQuickInfo),
2310+
nodeForQuickInfo,
2311+
/*semanticMeaning*/ undefined,
2312+
/*alias*/ undefined,
2313+
verbosityLevel,
2314+
),
2315+
);
23032316
return {
23042317
kind: symbolKind,
23052318
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
23062319
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
23072320
displayParts,
23082321
documentation,
23092322
tags,
2323+
canIncreaseVerbosityLevel,
23102324
};
23112325
}
23122326

0 commit comments

Comments
 (0)