Skip to content

Commit

Permalink
Proposed expandable hover API (#59940)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabritto authored Nov 6, 2024
1 parent 9d7e087 commit 80eeb4e
Show file tree
Hide file tree
Showing 30 changed files with 8,169 additions and 57 deletions.
137 changes: 105 additions & 32 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5042,6 +5042,11 @@ export interface TypeCheckerHost extends ModuleSpecifierResolutionHost, SourceFi
packageBundlesTypes(packageName: string): boolean;
}

/** @internal */
export interface WriterContextOut {
couldUnfoldMore: boolean;
}

export interface TypeChecker {
getTypeOfSymbolAtLocation(symbol: Symbol, node: Node): Type;
getTypeOfSymbol(symbol: Symbol): Type;
Expand Down Expand Up @@ -5128,6 +5133,7 @@ export interface TypeChecker {
symbolToParameterDeclaration(symbol: Symbol, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): ParameterDeclaration | undefined;
/** Note that the resulting nodes cannot be checked. */
typeParameterToDeclaration(parameter: TypeParameter, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): TypeParameterDeclaration | undefined;
/** @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

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

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

Expand Down
5 changes: 3 additions & 2 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ export class SessionClient implements LanguageService {
return { line, character: offset };
}

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

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

Expand Down
25 changes: 19 additions & 6 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export interface TextSpan {
end: number;
}

export interface VerbosityLevels {
[markerName: string]: number | number[] | undefined;
}

// Name of testcase metadata including ts.CompilerOptions properties that will be used by globalOptions
// To add additional option, add property into the testOptMetadataNames, refer the property in either globalMetadataNames or fileMetadataNames
// Add cases into convertGlobalOptionsToCompilationsSettings function for the compiler to acknowledge such option from meta data
Expand Down Expand Up @@ -2451,19 +2455,28 @@ export class TestState {
return result;
}

public baselineQuickInfo(): void {
const result = ts.arrayFrom(this.testData.markerPositions.entries(), ([name, marker]) => ({
marker: { ...marker, name },
item: this.languageService.getQuickInfoAtPosition(marker.fileName, marker.position),
}));
public baselineQuickInfo(verbosityLevels?: VerbosityLevels): void {
const result = ts.arrayFrom(this.testData.markerPositions.entries(), ([name, marker]) => {
const verbosityLevel = toArray(verbosityLevels?.[name]);
const items = verbosityLevel.map(verbosityLevel => {
const item: ts.QuickInfo & { verbosityLevel?: number; } | undefined = this.languageService.getQuickInfoAtPosition(marker.fileName, marker.position, verbosityLevel);
if (item) item.verbosityLevel = verbosityLevel;
return {
marker: { ...marker, name },
item,
};
});
return items;
}).flat();
const annotations = this.annotateContentWithTooltips(
result,
"quickinfo",
item => item.textSpan,
({ displayParts, documentation, tags }) => [
({ displayParts, documentation, tags, verbosityLevel }) => [
...(displayParts ? displayParts.map(p => p.text).join("").split("\n") : []),
...(documentation?.length ? documentation.map(p => p.text).join("").split("\n") : []),
...(tags?.length ? tags.map(p => `@${p.name} ${p.text?.map(dp => dp.text).join("") ?? ""}`).join("\n").split("\n") : []),
...(verbosityLevel !== undefined ? [`(verbosity level: ${verbosityLevel})`] : []),
],
);
this.baseline("QuickInfo", annotations + "\n\n" + stringify(result));
Expand Down
4 changes: 2 additions & 2 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,8 @@ export class Verify extends VerifyNegatable {
this.state.baselineGetEmitOutput();
}

public baselineQuickInfo(): void {
this.state.baselineQuickInfo();
public baselineQuickInfo(verbosityLevels?: FourSlash.VerbosityLevels): void {
this.state.baselineQuickInfo(verbosityLevels);
}

public baselineSignatureHelp(): void {
Expand Down
10 changes: 10 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,11 @@ export interface QuickInfoRequest extends FileLocationRequest {
arguments: FileLocationRequestArgs;
}

export interface QuickInfoRequestArgs extends FileLocationRequestArgs {
/** TODO */
verbosityLevel?: number;
}

/**
* Body of QuickInfoResponse.
*/
Expand Down Expand Up @@ -2043,6 +2048,11 @@ export interface QuickInfoResponseBody {
* JSDoc tags associated with symbol.
*/
tags: JSDocTagInfo[];

/**
* TODO
*/
canIncreaseVerbosityLevel?: boolean;
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2394,10 +2394,10 @@ export class Session<TMessage = string> implements EventSender {
return languageService.isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0));
}

private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
private getQuickInfoWorker(args: protocol.QuickInfoRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo));
const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo), args.verbosityLevel);
if (!quickInfo) {
return undefined;
}
Expand All @@ -2413,6 +2413,7 @@ export class Session<TMessage = string> implements EventSender {
displayString,
documentation: useDisplayParts ? this.mapDisplayParts(quickInfo.documentation, project) : displayPartsToString(quickInfo.documentation),
tags: this.mapJSDocTagInfo(quickInfo.tags, project, useDisplayParts),
canIncreaseVerbosityLevel: quickInfo.canIncreaseVerbosityLevel,
};
}
else {
Expand Down
20 changes: 17 additions & 3 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2277,7 +2277,7 @@ export function createLanguageService(
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences);
}

function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
function getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel?: number): QuickInfo | undefined {
synchronizeHostData();

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

const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo));
const { symbolKind, displayParts, documentation, tags, canIncreaseVerbosityLevel } = typeChecker.runWithCancellationToken(
cancellationToken,
typeChecker =>
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(
typeChecker,
symbol,
sourceFile,
getContainerNode(nodeForQuickInfo),
nodeForQuickInfo,
/*semanticMeaning*/ undefined,
/*alias*/ undefined,
verbosityLevel,
),
);
return {
kind: symbolKind,
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
displayParts,
documentation,
tags,
canIncreaseVerbosityLevel,
};
}

Expand Down
59 changes: 53 additions & 6 deletions src/services/symbolDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import {
TypeParameter,
typeToDisplayParts,
VariableDeclaration,
WriterContextOut,
} from "./_namespaces/ts.js";

const symbolDisplayNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
Expand Down Expand Up @@ -254,9 +255,20 @@ export interface SymbolDisplayPartsDocumentationAndSymbolKind {
documentation: SymbolDisplayPart[];
symbolKind: ScriptElementKind;
tags: JSDocTagInfo[] | undefined;
canIncreaseVerbosityLevel?: boolean;
}

function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: TypeChecker, symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node | undefined, location: Node, type: Type | undefined, semanticMeaning: SemanticMeaning, alias?: Symbol): SymbolDisplayPartsDocumentationAndSymbolKind {
function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(
typeChecker: TypeChecker,
symbol: Symbol,
sourceFile: SourceFile,
enclosingDeclaration: Node | undefined,
location: Node,
type: Type | undefined,
semanticMeaning: SemanticMeaning,
alias?: Symbol,
verbosityLevel?: number,
): SymbolDisplayPartsDocumentationAndSymbolKind {
const displayParts: SymbolDisplayPart[] = [];
let documentation: SymbolDisplayPart[] = [];
let tags: JSDocTagInfo[] = [];
Expand All @@ -267,6 +279,7 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
let documentationFromAlias: SymbolDisplayPart[] | undefined;
let tagsFromAlias: JSDocTagInfo[] | undefined;
let hasMultipleSignatures = false;
const typeWriterOut: WriterContextOut | undefined = verbosityLevel !== undefined ? { couldUnfoldMore: false } : undefined;

if (location.kind === SyntaxKind.ThisKeyword && !isThisExpression) {
return { displayParts: [keywordPart(SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ScriptElementKind.primitiveType, tags: undefined };
Expand Down Expand Up @@ -462,7 +475,17 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
displayParts.push(spacePart());
displayParts.push(operatorPart(SyntaxKind.EqualsToken));
displayParts.push(spacePart());
addRange(displayParts, typeToDisplayParts(typeChecker, location.parent && isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias));
addRange(
displayParts,
typeToDisplayParts(
typeChecker,
location.parent && isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol),
enclosingDeclaration,
TypeFormatFlags.InTypeAlias,
verbosityLevel,
typeWriterOut,
),
);
}
if (symbolFlags & SymbolFlags.Enum) {
prefixNextMeaning();
Expand Down Expand Up @@ -650,13 +673,30 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
// If the type is type parameter, format it specially
if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter && symbolKind !== ScriptElementKind.indexSignatureElement) {
const typeParameterParts = mapToDisplayParts(writer => {
const param = typeChecker.typeParameterToDeclaration(type as TypeParameter, enclosingDeclaration, symbolDisplayNodeBuilderFlags)!;
const param = typeChecker.typeParameterToDeclaration(
type as TypeParameter,
enclosingDeclaration,
symbolDisplayNodeBuilderFlags,
/*internalFlags*/ undefined,
/*tracker*/ undefined,
verbosityLevel,
)!;
getPrinter().writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer);
});
addRange(displayParts, typeParameterParts);
}
else {
addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration));
addRange(
displayParts,
typeToDisplayParts(
typeChecker,
type,
enclosingDeclaration,
/*flags*/ undefined,
verbosityLevel,
typeWriterOut,
),
);
}
if (isTransientSymbol(symbol) && symbol.links.target && isTransientSymbol(symbol.links.target) && symbol.links.target.links.tupleLabelDeclaration) {
const labelDecl = symbol.links.target.links.tupleLabelDeclaration;
Expand Down Expand Up @@ -742,7 +782,13 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker: Type
tags = tagsFromAlias;
}

return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags };
return {
displayParts,
documentation,
symbolKind,
tags: tags.length === 0 ? undefined : tags,
canIncreaseVerbosityLevel: typeWriterOut?.couldUnfoldMore,
};

function getPrinter() {
return createPrinterWithRemoveComments();
Expand Down Expand Up @@ -874,8 +920,9 @@ export function getSymbolDisplayPartsDocumentationAndSymbolKind(
location: Node,
semanticMeaning: SemanticMeaning = getMeaningFromLocation(location),
alias?: Symbol,
verbosityLevel?: number,
): SymbolDisplayPartsDocumentationAndSymbolKind {
return getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker, symbol, sourceFile, enclosingDeclaration, location, /*type*/ undefined, semanticMeaning, alias);
return getSymbolDisplayPartsDocumentationAndSymbolKindWorker(typeChecker, symbol, sourceFile, enclosingDeclaration, location, /*type*/ undefined, semanticMeaning, alias, verbosityLevel);
}

function isLocalVariableOrFunction(symbol: Symbol) {
Expand Down
3 changes: 3 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,8 @@ export interface LanguageService {
* @param position A zero-based index of the character where you want the quick info
*/
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;
/** @internal */
getQuickInfoAtPosition(fileName: string, position: number, verbosityLevel: number | undefined): QuickInfo | undefined; // eslint-disable-line @typescript-eslint/unified-signatures

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

Expand Down Expand Up @@ -1325,6 +1327,7 @@ export interface QuickInfo {
displayParts?: SymbolDisplayPart[];
documentation?: SymbolDisplayPart[];
tags?: JSDocTagInfo[];
canIncreaseVerbosityLevel?: boolean;
}

export type RenameInfo = RenameInfoSuccess | RenameInfoFailure;
Expand Down
5 changes: 3 additions & 2 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ import {
visitEachChild,
VoidExpression,
walkUpParenthesizedExpressions,
WriterContextOut,
YieldExpression,
} from "./_namespaces/ts.js";

Expand Down Expand Up @@ -3055,9 +3056,9 @@ export function mapToDisplayParts(writeDisplayParts: (writer: DisplayPartsSymbol
}

/** @internal */
export function typeToDisplayParts(typechecker: TypeChecker, type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None): SymbolDisplayPart[] {
export function typeToDisplayParts(typechecker: TypeChecker, type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.None, verbosityLevel?: number, out?: WriterContextOut): SymbolDisplayPart[] {
return mapToDisplayParts(writer => {
typechecker.writeType(type, enclosingDeclaration, flags | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer);
typechecker.writeType(type, enclosingDeclaration, flags | TypeFormatFlags.MultilineObjectLiterals | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer, verbosityLevel, out);
});
}

Expand Down
Loading

0 comments on commit 80eeb4e

Please sign in to comment.