Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ package-lock.json
.eslintcache
*v8.log
/lib/
PR_DESCRIPTION.md
8 changes: 8 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10564,6 +10564,14 @@ export interface UserPreferences {
* Default: `500`
*/
readonly maximumHoverLength?: number;
/**
* Maximum number of results to return per level in type hierarchy requests.
* A positive integer limiting the number of supertypes/subtypes returned to prevent
* performance issues in very large codebases.
*
* Default: `1000`
*/
readonly typeHierarchyMaxResults?: number;
}

export type OrganizeImportsTypeOrder = "last" | "inline" | "first";
Expand Down
34 changes: 34 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
TextSpan,
TodoComment,
TodoCommentDescriptor,
TypeHierarchyItem,
UserPreferences,
} from "./_namespaces/ts.js";
import { protocol } from "./_namespaces/ts.server.js";
Expand Down Expand Up @@ -1017,6 +1018,39 @@ export class SessionClient implements LanguageService {
return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
}

private convertTypeHierarchyItem(item: protocol.TypeHierarchyItem): TypeHierarchyItem {
return {
file: item.file,
name: item.name,
kind: item.kind,
kindModifiers: item.kindModifiers,
containerName: item.containerName,
span: this.decodeSpan(item.span, item.file),
selectionSpan: this.decodeSpan(item.selectionSpan, item.file),
};
}

prepareTypeHierarchy(fileName: string, position: number): TypeHierarchyItem | TypeHierarchyItem[] | undefined {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.PrepareTypeHierarchyRequest>(protocol.CommandTypes.PrepareTypeHierarchy, args);
const response = this.processResponse<protocol.PrepareTypeHierarchyResponse>(request);
return response.body && mapOneOrMany(response.body, item => this.convertTypeHierarchyItem(item));
}

provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideTypeHierarchySupertypesRequest>(protocol.CommandTypes.ProvideTypeHierarchySupertypes, args);
const response = this.processResponse<protocol.ProvideTypeHierarchySupertypesResponse>(request);
return response.body.map(item => this.convertTypeHierarchyItem(item));
}

provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideTypeHierarchySubtypesRequest>(protocol.CommandTypes.ProvideTypeHierarchySubtypes, args);
const response = this.processResponse<protocol.ProvideTypeHierarchySubtypesResponse>(request);
return response.body.map(item => this.convertTypeHierarchyItem(item));
}

getSupportedCodeFixes(): readonly string[] {
return getSupportedCodeFixes();
}
Expand Down
119 changes: 119 additions & 0 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ const enum CallHierarchyItemDirection {
Outgoing,
}

const enum TypeHierarchyItemDirection {
Root,
Supertypes,
Subtypes,
}

interface RealizedDiagnostic {
message: string;
start: number;
Expand Down Expand Up @@ -4421,6 +4427,119 @@ export class TestState {
this.baseline("Call Hierarchy", text, ".callHierarchy.txt");
}

private formatLibFileHierarchyItemSpan(item: ts.TypeHierarchyItem, prefix: string, trailingPrefix: string): string {
// For lib files, we don't have the source available in the test
let text = "";
text += `${prefix}╭ ${item.file} (lib file)\n`;
text += `${prefix}│ <source not available>\n`;
text += `${trailingPrefix}╰\n`;
return text;
}

private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
if (!file) {
return this.formatLibFileHierarchyItemSpan(item, prefix, trailingPrefix);
}
return this.formatCallHierarchyItemSpan(file, span, prefix, trailingPrefix);
}

private formatTypeHierarchyItem(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, direction: TypeHierarchyItemDirection, seen: Map<string, boolean>, prefix: string, trailingPrefix: string = prefix): string {
const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using JSON.stringify for creating cache keys can be expensive when called frequently. Consider using a simpler concatenation like ${item.file}|${item.span.start}|${item.span.length}|${direction} for better performance.

Suggested change
const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`;
const key = `${item.file}|${item.span.start}|${item.span.length}|${direction}`;

Copilot uses AI. Check for mistakes.
const alreadySeen = seen.has(key);
seen.set(key, true);

const supertypes = direction === TypeHierarchyItemDirection.Subtypes ? { result: "skip" } as const :
alreadySeen ? { result: "seen" } as const :
{ result: "show", values: this.languageService.provideTypeHierarchySupertypes(item.file, item.selectionSpan.start) } as const;

const subtypes = direction === TypeHierarchyItemDirection.Supertypes ? { result: "skip" } as const :
alreadySeen ? { result: "seen" } as const :
{ result: "show", values: this.languageService.provideTypeHierarchySubtypes(item.file, item.selectionSpan.start) } as const;

let text = "";
text += `${prefix}╭ name: ${item.name}\n`;
text += `${prefix}├ kind: ${item.kind}\n`;
if (item.kindModifiers) {
text += `${prefix}├ kindModifiers: ${item.kindModifiers}\n`;
}
if (item.containerName) {
text += `${prefix}├ containerName: ${item.containerName}\n`;
}
text += `${prefix}├ file: ${item.file}\n`;
text += `${prefix}├ span:\n`;
text += this.formatTypeHierarchyItemSpan(file, item, item.span, `${prefix}│ `);
text += `${prefix}├ selectionSpan:\n`;
text += this.formatTypeHierarchyItemSpan(
file,
item,
item.selectionSpan,
`${prefix}│ `,
supertypes.result !== "skip" || subtypes.result !== "skip" ? `${prefix}│ ` : `${trailingPrefix}╰ `,
);

if (supertypes.result === "seen") {
if (subtypes.result === "skip") {
text += `${trailingPrefix}╰ supertypes: ...\n`;
}
else {
text += `${prefix}├ supertypes: ...\n`;
}
}
else if (supertypes.result === "show") {
if (!ts.some(supertypes.values)) {
if (subtypes.result === "skip") {
text += `${trailingPrefix}╰ supertypes: none\n`;
}
else {
text += `${prefix}├ supertypes: none\n`;
}
}
else {
text += `${prefix}├ supertypes:\n`;
for (let i = 0; i < supertypes.values.length; i++) {
const supertype = supertypes.values[i];
const supertypeFile = this.tryFindFileWorker(supertype.file).file;
text += `${prefix}│ ╭\n`;
text += this.formatTypeHierarchyItem(supertypeFile, supertype, TypeHierarchyItemDirection.Supertypes, seen, `${prefix}│ │ `, i < supertypes.values.length - 1 ? `${prefix}│ ╰ ` : subtypes.result !== "skip" ? `${prefix}│ ╰ ` : `${trailingPrefix}╰ ╰ `);
}
}
}

if (subtypes.result === "seen") {
text += `${trailingPrefix}╰ subtypes: ...\n`;
}
else if (subtypes.result === "show") {
if (!ts.some(subtypes.values)) {
text += `${trailingPrefix}╰ subtypes: none\n`;
}
else {
text += `${prefix}├ subtypes:\n`;
for (let i = 0; i < subtypes.values.length; i++) {
const subtype = subtypes.values[i];
const subtypeFile = this.tryFindFileWorker(subtype.file).file;
text += `${prefix}│ ╭\n`;
text += this.formatTypeHierarchyItem(subtypeFile, subtype, TypeHierarchyItemDirection.Subtypes, seen, `${prefix}│ │ `, i < subtypes.values.length - 1 ? `${prefix}│ ╰ ` : `${trailingPrefix}╰ ╰ `);
}
}
}
return text;
}

private formatTypeHierarchy(item: ts.TypeHierarchyItem | undefined): string {
let text = "";
if (item) {
const file = this.tryFindFileWorker(item.file).file;
text += this.formatTypeHierarchyItem(file, item, TypeHierarchyItemDirection.Root, new Map(), "");
}
return text;
}

public baselineTypeHierarchy(): void {
const item = this.languageService.prepareTypeHierarchy(this.activeFile.fileName, this.currentCaretPosition);
const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none";
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline arrow function and join callback make this line difficult to read. Consider breaking this into multiple lines or extracting the mapping logic for better clarity.

Suggested change
const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none";
let text: string;
if (item) {
const formatHierarchyItem = (hierarchyItem: ts.TypeHierarchyItem) => this.formatTypeHierarchy(hierarchyItem);
const concatenateResults = (results: string[]) => results.join("");
text = ts.mapOneOrMany(item, formatHierarchyItem, concatenateResults);
}
else {
text = "none";
}

Copilot uses AI. Check for mistakes.
this.baseline("Type Hierarchy", text, ".typeHierarchy.txt");
}

private getLineContent(index: number) {
const text = this.getFileContent(this.activeFile.fileName);
const pos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
Expand Down
4 changes: 4 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,10 @@ export class Verify extends VerifyNegatable {
this.state.baselineCallHierarchy();
}

public baselineTypeHierarchy(): void {
this.state.baselineTypeHierarchy();
}

public moveToNewFile(options: MoveToNewFileOptions): void {
this.state.moveToNewFile(options);
}
Expand Down
29 changes: 29 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ export const enum CommandTypes {
PrepareCallHierarchy = "prepareCallHierarchy",
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
PrepareTypeHierarchy = "prepareTypeHierarchy",
ProvideTypeHierarchySupertypes = "provideTypeHierarchySupertypes",
ProvideTypeHierarchySubtypes = "provideTypeHierarchySubtypes",
ProvideInlayHints = "provideInlayHints",
WatchChange = "watchChange",
MapCode = "mapCode",
Expand Down Expand Up @@ -3218,6 +3221,32 @@ export interface ProvideCallHierarchyOutgoingCallsResponse extends Response {
readonly body: CallHierarchyOutgoingCall[];
}

export type TypeHierarchyItem = ChangePropertyTypes<ts.TypeHierarchyItem, { span: TextSpan; selectionSpan: TextSpan; }>;

export interface PrepareTypeHierarchyRequest extends FileLocationRequest {
command: CommandTypes.PrepareTypeHierarchy;
}

export interface PrepareTypeHierarchyResponse extends Response {
readonly body: TypeHierarchyItem | TypeHierarchyItem[];
}

export interface ProvideTypeHierarchySupertypesRequest extends FileLocationRequest {
command: CommandTypes.ProvideTypeHierarchySupertypes;
}

export interface ProvideTypeHierarchySupertypesResponse extends Response {
readonly body: TypeHierarchyItem[];
}

export interface ProvideTypeHierarchySubtypesRequest extends FileLocationRequest {
command: CommandTypes.ProvideTypeHierarchySubtypes;
}

export interface ProvideTypeHierarchySubtypesResponse extends Response {
readonly body: TypeHierarchyItem[];
}

export const enum IndentStyle {
None = "None",
Block = "Block",
Expand Down
51 changes: 51 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
toArray,
toFileNameLowerCase,
tracing,
TypeHierarchyItem,
unmangleScopedPackageName,
version,
WithMetadata,
Expand Down Expand Up @@ -937,6 +938,9 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [
protocol.CommandTypes.PrepareCallHierarchy,
protocol.CommandTypes.ProvideCallHierarchyIncomingCalls,
protocol.CommandTypes.ProvideCallHierarchyOutgoingCalls,
protocol.CommandTypes.PrepareTypeHierarchy,
protocol.CommandTypes.ProvideTypeHierarchySupertypes,
protocol.CommandTypes.ProvideTypeHierarchySubtypes,
protocol.CommandTypes.GetPasteEdits,
protocol.CommandTypes.CopilotRelated,
];
Expand Down Expand Up @@ -3385,6 +3389,44 @@ export class Session<TMessage = string> implements EventSender {
return outgoingCalls.map(call => this.toProtocolCallHierarchyOutgoingCall(call, scriptInfo));
}

private toProtocolTypeHierarchyItem(item: TypeHierarchyItem): protocol.TypeHierarchyItem {
const scriptInfo = this.getScriptInfoFromProjectService(item.file);
return {
name: item.name,
kind: item.kind,
kindModifiers: item.kindModifiers,
file: item.file,
containerName: item.containerName,
span: toProtocolTextSpan(item.span, scriptInfo),
selectionSpan: toProtocolTextSpan(item.selectionSpan, scriptInfo),
};
}

private prepareTypeHierarchy(args: protocol.FileLocationRequestArgs): protocol.TypeHierarchyItem | protocol.TypeHierarchyItem[] | undefined {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (scriptInfo) {
const position = this.getPosition(args, scriptInfo);
const result = project.getLanguageService().prepareTypeHierarchy(file, position);
return result && mapOneOrMany(result, item => this.toProtocolTypeHierarchyItem(item));
}
return undefined;
}

private provideTypeHierarchySupertypes(args: protocol.FileLocationRequestArgs): protocol.TypeHierarchyItem[] {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = this.getScriptInfoFromProjectService(file);
const supertypes = project.getLanguageService().provideTypeHierarchySupertypes(file, this.getPosition(args, scriptInfo), this.getPreferences(file));
return supertypes.map(item => this.toProtocolTypeHierarchyItem(item));
}

private provideTypeHierarchySubtypes(args: protocol.FileLocationRequestArgs): protocol.TypeHierarchyItem[] {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = this.getScriptInfoFromProjectService(file);
const subtypes = project.getLanguageService().provideTypeHierarchySubtypes(file, this.getPosition(args, scriptInfo), this.getPreferences(file));
return subtypes.map(item => this.toProtocolTypeHierarchyItem(item));
}

getCanonicalFileName(fileName: string): string {
const name = this.host.useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName);
return normalizePath(name);
Expand Down Expand Up @@ -3777,6 +3819,15 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => {
return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments));
},
[protocol.CommandTypes.PrepareTypeHierarchy]: (request: protocol.PrepareTypeHierarchyRequest) => {
return this.requiredResponse(this.prepareTypeHierarchy(request.arguments));
},
[protocol.CommandTypes.ProvideTypeHierarchySupertypes]: (request: protocol.ProvideTypeHierarchySupertypesRequest) => {
return this.requiredResponse(this.provideTypeHierarchySupertypes(request.arguments));
},
[protocol.CommandTypes.ProvideTypeHierarchySubtypes]: (request: protocol.ProvideTypeHierarchySubtypesRequest) => {
return this.requiredResponse(this.provideTypeHierarchySubtypes(request.arguments));
},
[protocol.CommandTypes.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => {
return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ true));
},
Expand Down
1 change: 1 addition & 0 deletions src/services/_namespaces/ts.TypeHierarchy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "../typeHierarchy.js";
Loading
Loading