Skip to content

Commit 80eeb4e

Browse files
authored
Proposed expandable hover API (#59940)
1 parent 9d7e087 commit 80eeb4e

30 files changed

+8169
-57
lines changed

src/compiler/checker.ts

+105-32
Large diffs are not rendered by default.

src/compiler/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -5042,6 +5042,11 @@ export interface TypeCheckerHost extends ModuleSpecifierResolutionHost, SourceFi
50425042
packageBundlesTypes(packageName: string): boolean;
50435043
}
50445044

5045+
/** @internal */
5046+
export interface WriterContextOut {
5047+
couldUnfoldMore: boolean;
5048+
}
5049+
50455050
export interface TypeChecker {
50465051
getTypeOfSymbolAtLocation(symbol: Symbol, node: Node): Type;
50475052
getTypeOfSymbol(symbol: Symbol): Type;
@@ -5128,6 +5133,7 @@ export interface TypeChecker {
51285133
symbolToParameterDeclaration(symbol: Symbol, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): ParameterDeclaration | undefined;
51295134
/** Note that the resulting nodes cannot be checked. */
51305135
typeParameterToDeclaration(parameter: TypeParameter, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): TypeParameterDeclaration | undefined;
5136+
/** @internal */ typeParameterToDeclaration(parameter: TypeParameter, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker, verbosityLevel?: number): TypeParameterDeclaration | undefined; // eslint-disable-line @typescript-eslint/unified-signatures
51315137

51325138
getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[];
51335139
getSymbolAtLocation(node: Node): Symbol | undefined;
@@ -5160,7 +5166,7 @@ export interface TypeChecker {
51605166
typePredicateToString(predicate: TypePredicate, enclosingDeclaration?: Node, flags?: TypeFormatFlags): string;
51615167

51625168
/** @internal */ writeSignature(signature: Signature, enclosingDeclaration?: Node, flags?: TypeFormatFlags, kind?: SignatureKind, writer?: EmitTextWriter): string;
5163-
/** @internal */ writeType(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter): string;
5169+
/** @internal */ writeType(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter, verbosityLevel?: number, out?: WriterContextOut): string;
51645170
/** @internal */ writeSymbol(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags?: SymbolFormatFlags, writer?: EmitTextWriter): string;
51655171
/** @internal */ writeTypePredicate(predicate: TypePredicate, enclosingDeclaration?: Node, flags?: TypeFormatFlags, writer?: EmitTextWriter): string;
51665172

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
@@ -453,8 +453,8 @@ export class Verify extends VerifyNegatable {
453453
this.state.baselineGetEmitOutput();
454454
}
455455

456-
public baselineQuickInfo(): void {
457-
this.state.baselineQuickInfo();
456+
public baselineQuickInfo(verbosityLevels?: FourSlash.VerbosityLevels): void {
457+
this.state.baselineQuickInfo(verbosityLevels);
458458
}
459459

460460
public baselineSignatureHelp(): void {

src/server/protocol.ts

+10
Original file line numberDiff line numberDiff line change
@@ -2004,6 +2004,11 @@ export interface QuickInfoRequest extends FileLocationRequest {
20042004
arguments: FileLocationRequestArgs;
20052005
}
20062006

2007+
export interface QuickInfoRequestArgs extends FileLocationRequestArgs {
2008+
/** TODO */
2009+
verbosityLevel?: number;
2010+
}
2011+
20072012
/**
20082013
* Body of QuickInfoResponse.
20092014
*/
@@ -2043,6 +2048,11 @@ export interface QuickInfoResponseBody {
20432048
* JSDoc tags associated with symbol.
20442049
*/
20452050
tags: JSDocTagInfo[];
2051+
2052+
/**
2053+
* TODO
2054+
*/
2055+
canIncreaseVerbosityLevel?: boolean;
20462056
}
20472057

20482058
/**

src/server/session.ts

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

2397-
private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
2397+
private getQuickInfoWorker(args: protocol.QuickInfoRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
23982398
const { file, project } = this.getFileAndProject(args);
23992399
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
2400-
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo));
2400+
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo), args.verbosityLevel);
24012401
if (!quickInfo) {
24022402
return undefined;
24032403
}
@@ -2413,6 +2413,7 @@ export class Session<TMessage = string> implements EventSender {
24132413
displayString,
24142414
documentation: useDisplayParts ? this.mapDisplayParts(quickInfo.documentation, project) : displayPartsToString(quickInfo.documentation),
24152415
tags: this.mapJSDocTagInfo(quickInfo.tags, project, useDisplayParts),
2416+
canIncreaseVerbosityLevel: quickInfo.canIncreaseVerbosityLevel,
24162417
};
24172418
}
24182419
else {

src/services/services.ts

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

2280-
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
2280+
function getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel?: number): QuickInfo | undefined {
22812281
synchronizeHostData();
22822282

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

2305-
const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo));
2305+
const { symbolKind, displayParts, documentation, tags, canIncreaseVerbosityLevel } = typeChecker.runWithCancellationToken(
2306+
cancellationToken,
2307+
typeChecker =>
2308+
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(
2309+
typeChecker,
2310+
symbol,
2311+
sourceFile,
2312+
getContainerNode(nodeForQuickInfo),
2313+
nodeForQuickInfo,
2314+
/*semanticMeaning*/ undefined,
2315+
/*alias*/ undefined,
2316+
verbosityLevel,
2317+
),
2318+
);
23062319
return {
23072320
kind: symbolKind,
23082321
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
23092322
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
23102323
displayParts,
23112324
documentation,
23122325
tags,
2326+
canIncreaseVerbosityLevel,
23132327
};
23142328
}
23152329

src/services/symbolDisplay.ts

+53-6
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import {
108108
TypeParameter,
109109
typeToDisplayParts,
110110
VariableDeclaration,
111+
WriterContextOut,
111112
} from "./_namespaces/ts.js";
112113

113114
const symbolDisplayNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
@@ -254,9 +255,20 @@ export interface SymbolDisplayPartsDocumentationAndSymbolKind {
254255
documentation: SymbolDisplayPart[];
255256
symbolKind: ScriptElementKind;
256257
tags: JSDocTagInfo[] | undefined;
258+
canIncreaseVerbosityLevel?: boolean;
257259
}
258260

259-
function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: TypeChecker, symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node | undefined, location: Node, type: Type | undefined, semanticMeaning: SemanticMeaning, alias?: Symbol): SymbolDisplayPartsDocumentationAndSymbolKind {
261+
function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(
262+
typeChecker: TypeChecker,
263+
symbol: Symbol,
264+
sourceFile: SourceFile,
265+
enclosingDeclaration: Node | undefined,
266+
location: Node,
267+
type: Type | undefined,
268+
semanticMeaning: SemanticMeaning,
269+
alias?: Symbol,
270+
verbosityLevel?: number,
271+
): SymbolDisplayPartsDocumentationAndSymbolKind {
260272
const displayParts: SymbolDisplayPart[] = [];
261273
let documentation: SymbolDisplayPart[] = [];
262274
let tags: JSDocTagInfo[] = [];
@@ -267,6 +279,7 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
267279
let documentationFromAlias: SymbolDisplayPart[] | undefined;
268280
let tagsFromAlias: JSDocTagInfo[] | undefined;
269281
let hasMultipleSignatures = false;
282+
const typeWriterOut: WriterContextOut | undefined = verbosityLevel !== undefined ? { couldUnfoldMore: false } : undefined;
270283

271284
if (location.kind === SyntaxKind.ThisKeyword && !isThisExpression) {
272285
return { displayParts: [keywordPart(SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ScriptElementKind.primitiveType, tags: undefined };
@@ -462,7 +475,17 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
462475
displayParts.push(spacePart());
463476
displayParts.push(operatorPart(SyntaxKind.EqualsToken));
464477
displayParts.push(spacePart());
465-
addRange(displayParts, typeToDisplayParts(typeChecker, location.parent && isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias));
478+
addRange(
479+
displayParts,
480+
typeToDisplayParts(
481+
typeChecker,
482+
location.parent && isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol),
483+
enclosingDeclaration,
484+
TypeFormatFlags.InTypeAlias,
485+
verbosityLevel,
486+
typeWriterOut,
487+
),
488+
);
466489
}
467490
if (symbolFlags & SymbolFlags.Enum) {
468491
prefixNextMeaning();
@@ -650,13 +673,30 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
650673
// If the type is type parameter, format it specially
651674
if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter && symbolKind !== ScriptElementKind.indexSignatureElement) {
652675
const typeParameterParts = mapToDisplayParts(writer => {
653-
const param = typeChecker.typeParameterToDeclaration(type as TypeParameter, enclosingDeclaration, symbolDisplayNodeBuilderFlags)!;
676+
const param = typeChecker.typeParameterToDeclaration(
677+
type as TypeParameter,
678+
enclosingDeclaration,
679+
symbolDisplayNodeBuilderFlags,
680+
/*internalFlags*/ undefined,
681+
/*tracker*/ undefined,
682+
verbosityLevel,
683+
)!;
654684
getPrinter().writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer);
655685
});
656686
addRange(displayParts, typeParameterParts);
657687
}
658688
else {
659-
addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration));
689+
addRange(
690+
displayParts,
691+
typeToDisplayParts(
692+
typeChecker,
693+
type,
694+
enclosingDeclaration,
695+
/*flags*/ undefined,
696+
verbosityLevel,
697+
typeWriterOut,
698+
),
699+
);
660700
}
661701
if (isTransientSymbol(symbol) && symbol.links.target && isTransientSymbol(symbol.links.target) && symbol.links.target.links.tupleLabelDeclaration) {
662702
const labelDecl = symbol.links.target.links.tupleLabelDeclaration;
@@ -742,7 +782,13 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
742782
tags = tagsFromAlias;
743783
}
744784

745-
return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags };
785+
return {
786+
displayParts,
787+
documentation,
788+
symbolKind,
789+
tags: tags.length === 0 ? undefined : tags,
790+
canIncreaseVerbosityLevel: typeWriterOut?.couldUnfoldMore,
791+
};
746792

747793
function getPrinter() {
748794
return createPrinterWithRemoveComments();
@@ -874,8 +920,9 @@ export function getSymbolDisplayPartsDocumentationAndSymbolKind(
874920
location: Node,
875921
semanticMeaning: SemanticMeaning = getMeaningFromLocation(location),
876922
alias?: Symbol,
923+
verbosityLevel?: number,
877924
): SymbolDisplayPartsDocumentationAndSymbolKind {
878-
return getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker, symbol, sourceFile, enclosingDeclaration, location, /*type*/ undefined, semanticMeaning, alias);
925+
return getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker, symbol, sourceFile, enclosingDeclaration, location, /*type*/ undefined, semanticMeaning, alias, verbosityLevel);
879926
}
880927

881928
function isLocalVariableOrFunction(symbol: Symbol) {

src/services/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,8 @@ export interface LanguageService {
583583
* @param position A zero-based index of the character where you want the quick info
584584
*/
585585
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;
586+
/** @internal */
587+
getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel: number | undefined): QuickInfo | undefined; // eslint-disable-line @typescript-eslint/unified-signatures
586588

587589
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined;
588590

@@ -1325,6 +1327,7 @@ export interface QuickInfo {
13251327
displayParts?: SymbolDisplayPart[];
13261328
documentation?: SymbolDisplayPart[];
13271329
tags?: JSDocTagInfo[];
1330+
canIncreaseVerbosityLevel?: boolean;
13281331
}
13291332

13301333
export type RenameInfo = RenameInfoSuccess | RenameInfoFailure;

src/services/utilities.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ import {
390390
visitEachChild,
391391
VoidExpression,
392392
walkUpParenthesizedExpressions,
393+
WriterContextOut,
393394
YieldExpression,
394395
} from "./_namespaces/ts.js";
395396

@@ -3055,9 +3056,9 @@ export function mapToDisplayParts(writeDisplayParts: (writer: DisplayPartsSymbol
30553056
}
30563057

30573058
/** @internal */
3058-
export function typeToDisplayParts(typechecker: TypeChecker, type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None): SymbolDisplayPart[] {
3059+
export function typeToDisplayParts(typechecker: TypeChecker, type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None, verbosityLevel?: number, out?: WriterContextOut): SymbolDisplayPart[] {
30593060
return mapToDisplayParts(writer => {
3060-
typechecker.writeType(type, enclosingDeclaration, flags | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer);
3061+
typechecker.writeType(type, enclosingDeclaration, flags | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer, verbosityLevel, out);
30613062
});
30623063
}
30633064

0 commit comments

Comments
 (0)