Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactable parameter inlay hints #54734

Merged
merged 12 commits into from
Jul 20, 2023
27 changes: 16 additions & 11 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1538,7 +1538,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return node ? getTypeFromTypeNode(node) : errorType;
},
getParameterType: getTypeAtPosition,
getParameterIdentifierNameAtPosition,
getParameterIdentifierInfoAtPosition,
getPromisedTypeOfPromise,
getAwaitedType: type => getAwaitedType(type),
getReturnTypeOfSignature,
Expand Down Expand Up @@ -34845,18 +34845,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return restParameter.escapedName;
}

function getParameterIdentifierNameAtPosition(signature: Signature, pos: number): [parameterName: __String, isRestParameter: boolean] | undefined {
function getParameterIdentifierInfoAtPosition(signature: Signature, pos: number): [parameter: Identifier, parameterName: __String, isRestParameter: boolean] | undefined {
if (signature.declaration?.kind === SyntaxKind.JSDocFunctionType) {
return undefined;
}
const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0);
if (pos < paramCount) {
const param = signature.parameters[pos];
return isParameterDeclarationWithIdentifierName(param) ? [param.escapedName, false] : undefined;
const paramIdent = getParameterDeclarationIdentifier(param);
return paramIdent ? [paramIdent, param.escapedName, false] : undefined;
}

const restParameter = signature.parameters[paramCount] || unknownSymbol;
if (!isParameterDeclarationWithIdentifierName(restParameter)) {
const restIdent = getParameterDeclarationIdentifier(restParameter);
if (!restIdent) {
return undefined;
}

Expand All @@ -34866,20 +34868,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const index = pos - paramCount;
const associatedName = associatedNames?.[index];
const isRestTupleElement = !!associatedName?.dotDotDotToken;
return associatedName ? [
getTupleElementLabel(associatedName),
isRestTupleElement
] : undefined;

if (associatedName) {
Debug.assert(isIdentifier(associatedName.name));
return [associatedName.name, associatedName.name.escapedText, isRestTupleElement];
}

return undefined;
}

if (pos === paramCount) {
return [restParameter.escapedName, true];
return [restIdent, restParameter.escapedName, true];
}
return undefined;
}

function isParameterDeclarationWithIdentifierName(symbol: Symbol) {
return symbol.valueDeclaration && isParameter(symbol.valueDeclaration) && isIdentifier(symbol.valueDeclaration.name);
function getParameterDeclarationIdentifier(symbol: Symbol) {
return symbol.valueDeclaration && isParameter(symbol.valueDeclaration) && isIdentifier(symbol.valueDeclaration.name) && symbol.valueDeclaration.name;
}
function isValidDeclarationForTupleLabel(d: Declaration): d is NamedTupleMember | (ParameterDeclaration & { name: Identifier }) {
return d.kind === SyntaxKind.NamedTupleMember || (isParameter(d) && d.name && isIdentifier(d.name));
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4992,7 +4992,7 @@ export interface TypeChecker {
* @internal
*/
getParameterType(signature: Signature, parameterIndex: number): Type;
/** @internal */ getParameterIdentifierNameAtPosition(signature: Signature, parameterIndex: number): [parameterName: __String, isRestParameter: boolean] | undefined;
/** @internal */ getParameterIdentifierInfoAtPosition(signature: Signature, parameterIndex: number): [parameter: Identifier, parameterName: __String, isRestParameter: boolean] | undefined;
getNullableType(type: Type, flags: TypeFlags): Type;
getNonNullableType(type: Type): Type;
/** @internal */ getNonOptionalType(type: Type): Type;
Expand Down Expand Up @@ -9985,6 +9985,7 @@ export interface UserPreferences {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly interactiveInlayHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
Expand Down
23 changes: 18 additions & 5 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,11 +759,24 @@ export class SessionClient implements LanguageService {
const request = this.processRequest<protocol.InlayHintsRequest>(protocol.CommandTypes.ProvideInlayHints, args);
const response = this.processResponse<protocol.InlayHintsResponse>(request);

return response.body!.map(item => ({ // TODO: GH#18217
...item,
kind: item.kind as InlayHintKind,
position: this.lineOffsetToPosition(file, item.position),
}));
return response.body!.map(item => {
const { text, position } = item;
const hint = typeof text === "string" ? text : text.map(({ text, span }) => ({
text,
span: span && {
start: this.lineOffsetToPosition(span.file, span.start),
length: this.lineOffsetToPosition(span.file, span.end) - this.lineOffsetToPosition(span.file, span.start),
},
file: span && span.file
}));

return ({
...item,
position: this.lineOffsetToPosition(file, position),
text: hint,
kind: item.kind as InlayHintKind
});
});
}

private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
Expand Down
9 changes: 8 additions & 1 deletion src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2675,13 +2675,18 @@ export interface InlayHintsRequest extends Request {
}

export interface InlayHintItem {
text: string;
text: string | InlayHintItemDisplayPart[];
position: Location;
kind: InlayHintKind;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}

export interface InlayHintItemDisplayPart {
text: string;
span?: FileSpan;
}

export interface InlayHintsResponse extends Response {
body?: InlayHintItem[];
}
Expand Down Expand Up @@ -3536,6 +3541,8 @@ export interface UserPreferences {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly interactiveInlayHints?: boolean;

readonly autoImportFileExcludePatterns?: string[];

/**
Expand Down
21 changes: 17 additions & 4 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,10 +1844,23 @@ export class Session<TMessage = string> implements EventSender {
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const hints = project.getLanguageService().provideInlayHints(file, args, this.getPreferences(file));

return hints.map(hint => ({
...hint,
position: scriptInfo.positionToLineOffset(hint.position),
}));
return hints.map(hint => {
const { text, position } = hint;
const hintText = typeof text === "string" ? text : text.map(({ text, span, file }) => ({
text,
span: span && {
start: scriptInfo.positionToLineOffset(span.start),
end: scriptInfo.positionToLineOffset(span.start + span.length),
file: file!
}
}));

return {
...hint,
position: scriptInfo.positionToLineOffset(position),
text: hintText
};
});
}

private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void {
Expand Down
48 changes: 32 additions & 16 deletions src/services/inlayHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ArrowFunction,
CallExpression,
createPrinterWithRemoveComments,
createTextSpanFromNode,
Debug,
ElementFlags,
EmitHint,
Expand All @@ -23,6 +24,7 @@ import {
hasContextSensitiveParameters,
Identifier,
InlayHint,
InlayHintDisplayPart,
InlayHintKind,
InlayHintsContext,
isArrowFunction,
Expand Down Expand Up @@ -73,7 +75,7 @@ import {
VariableDeclaration,
} from "./_namespaces/ts";

const maxHintsLength = 30;
const maxTypeHintLength = 30;

const leadingParameterNameCommentRegexFactory = (name: string) => {
return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`);
Expand All @@ -87,6 +89,10 @@ function shouldShowLiteralParameterNameHintsOnly(preferences: UserPreferences) {
return preferences.includeInlayParameterNameHints === "literals";
}

function shouldUseInteractiveInlayHints(preferences: UserPreferences) {
return preferences.interactiveInlayHints === true;
}

/** @internal */
export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
const { file, program, span, cancellationToken, preferences } = context;
Expand Down Expand Up @@ -151,9 +157,17 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
return isArrowFunction(node) || isFunctionExpression(node) || isFunctionDeclaration(node) || isMethodDeclaration(node) || isGetAccessorDeclaration(node);
}

function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) {
function addParameterHints(text: string, parameter: Identifier, position: number, isFirstVariadicArgument: boolean) {
let hintText: string | InlayHintDisplayPart[] = `${isFirstVariadicArgument ? "..." : ""}${text}`;
if (shouldUseInteractiveInlayHints(preferences)) {
hintText = [getNodeDisplayPart(hintText, parameter), { text: ":" }];
}
else {
hintText += ":";
}

result.push({
text: `${isFirstVariadicArgument ? "..." : ""}${truncation(text, maxHintsLength)}:`,
text: hintText,
position,
kind: InlayHintKind.Parameter,
whitespaceAfter: true,
Expand All @@ -162,7 +176,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {

function addTypeHints(text: string, position: number) {
result.push({
text: `: ${truncation(text, maxHintsLength)}`,
text: `: ${text.length > maxTypeHintLength ? text.substr(0, maxTypeHintLength - "...".length) + "..." : text}`,
position,
kind: InlayHintKind.Type,
whitespaceBefore: true,
Expand All @@ -171,7 +185,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {

function addEnumMemberValueHints(text: string, position: number) {
result.push({
text: `= ${truncation(text, maxHintsLength)}`,
text: `= ${text}`,
position,
kind: InlayHintKind.Enum,
whitespaceBefore: true,
Expand Down Expand Up @@ -253,10 +267,10 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
}
}

const identifierNameInfo = checker.getParameterIdentifierNameAtPosition(signature, signatureParamPos);
const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, signatureParamPos);
signatureParamPos = signatureParamPos + (spreadArgs || 1);
if (identifierNameInfo) {
const [parameterName, isFirstVariadicArgument] = identifierNameInfo;
if (identifierInfo) {
const [parameter, parameterName, isFirstVariadicArgument] = identifierInfo;
const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName);
if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) {
continue;
Expand All @@ -267,7 +281,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
continue;
}

addParameterHints(name, originalArg.getStart(), isFirstVariadicArgument);
addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument);
}
}
}
Expand Down Expand Up @@ -394,13 +408,6 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
return printTypeInSingleLine(signatureParamType);
}

function truncation(text: string, maxLength: number) {
if (text.length > maxLength) {
return text.substr(0, maxLength - "...".length) + "...";
}
return text;
}

function printTypeInSingleLine(type: Type) {
const flags = NodeBuilderFlags.IgnoreErrors | TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
const printer = createPrinterWithRemoveComments();
Expand All @@ -423,4 +430,13 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
}
return true;
}

function getNodeDisplayPart(text: string, node: Node): InlayHintDisplayPart {
const sourceFile = node.getSourceFile();
return {
text,
span: createTextSpanFromNode(node, sourceFile),
file: sourceFile.fileName
};
}
}
8 changes: 7 additions & 1 deletion src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,13 +866,19 @@ export const enum InlayHintKind {
}

export interface InlayHint {
text: string;
text: string | InlayHintDisplayPart[];
position: number;
kind: InlayHintKind;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}

export interface InlayHintDisplayPart {
text: string;
span?: TextSpan;
file?: string;
}

export interface TodoCommentDescriptor {
text: string;
priority: number;
Expand Down
15 changes: 13 additions & 2 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2124,12 +2124,16 @@ declare namespace ts {
arguments: InlayHintsRequestArgs;
}
interface InlayHintItem {
text: string;
text: string | InlayHintItemDisplayPart[];
position: Location;
kind: InlayHintKind;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}
interface InlayHintItemDisplayPart {
text: string;
span?: FileSpan;
}
interface InlayHintsResponse extends Response {
body?: InlayHintItem[];
}
Expand Down Expand Up @@ -2832,6 +2836,7 @@ declare namespace ts {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly interactiveInlayHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];
/**
* Indicates whether imports should be organized in a case-insensitive manner.
Expand Down Expand Up @@ -8400,6 +8405,7 @@ declare namespace ts {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly interactiveInlayHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
Expand Down Expand Up @@ -10382,12 +10388,17 @@ declare namespace ts {
Enum = "Enum"
}
interface InlayHint {
text: string;
text: string | InlayHintDisplayPart[];
position: number;
kind: InlayHintKind;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}
interface InlayHintDisplayPart {
text: string;
span?: TextSpan;
file?: string;
}
interface TodoCommentDescriptor {
text: string;
priority: number;
Expand Down
8 changes: 7 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4347,6 +4347,7 @@ declare namespace ts {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly interactiveInlayHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
Expand Down Expand Up @@ -6413,12 +6414,17 @@ declare namespace ts {
Enum = "Enum"
}
interface InlayHint {
text: string;
text: string | InlayHintDisplayPart[];
position: number;
kind: InlayHintKind;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}
interface InlayHintDisplayPart {
text: string;
span?: TextSpan;
file?: string;
}
interface TodoCommentDescriptor {
text: string;
priority: number;
Expand Down
Loading