Skip to content

Commit 4ca40ce

Browse files
committed
Add support for Call Hierarchies in language server
1 parent ba5e86f commit 4ca40ce

30 files changed

+1820
-34
lines changed

src/compiler/core.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,8 +1330,10 @@ namespace ts {
13301330
return result;
13311331
}
13321332

1333-
export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[] {
1334-
return arrayFrom(arrayToMultiMap(values, getGroupId).values());
1333+
export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[];
1334+
export function group<T, R>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => R): R[];
1335+
export function group<T>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] {
1336+
return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector);
13351337
}
13361338

13371339
export function clone<T>(object: T): T {

src/harness/client.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,51 @@ namespace ts.server {
741741
return notImplemented();
742742
}
743743

744+
private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem {
745+
return {
746+
file: item.file,
747+
name: item.name,
748+
kind: item.kind,
749+
span: this.decodeSpan(item.span, item.file),
750+
selectionSpan: this.decodeSpan(item.selectionSpan, item.file)
751+
};
752+
}
753+
754+
prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | undefined {
755+
const args = this.createFileLocationRequestArgs(fileName, position);
756+
const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args);
757+
const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request);
758+
return response.body && this.convertCallHierarchyItem(response.body);
759+
}
760+
761+
private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
762+
return {
763+
from: this.convertCallHierarchyItem(item.from),
764+
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file))
765+
};
766+
}
767+
768+
provideCallHierarchyIncomingCalls(fileName: string, position: number) {
769+
const args = this.createFileLocationRequestArgs(fileName, position);
770+
const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
771+
const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request);
772+
return response.body.map(item => this.convertCallHierarchyIncomingCall(item));
773+
}
774+
775+
private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
776+
return {
777+
to: this.convertCallHierarchyItem(item.to),
778+
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file))
779+
};
780+
}
781+
782+
provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
783+
const args = this.createFileLocationRequestArgs(fileName, position);
784+
const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
785+
const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request);
786+
return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
787+
}
788+
744789
getProgram(): Program {
745790
throw new Error("SourceFile objects are not serializable through the server protocol.");
746791
}

src/harness/fourslash.ts

Lines changed: 230 additions & 7 deletions
Large diffs are not rendered by default.

src/harness/harnessLanguageService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,15 @@ namespace Harness.LanguageService {
574574
getEditsForFileRename(): readonly ts.FileTextChanges[] {
575575
throw new Error("Not supported on the shim.");
576576
}
577+
prepareCallHierarchy(fileName: string, position: number) {
578+
return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position));
579+
}
580+
provideCallHierarchyIncomingCalls(fileName: string, position: number) {
581+
return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position));
582+
}
583+
provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
584+
return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position));
585+
}
577586
getEmitOutput(fileName: string): ts.EmitOutput {
578587
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
579588
}

src/server/protocol.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Declaration module describing the TypeScript Server protocol
55
*/
66
namespace ts.server.protocol {
7-
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
7+
// NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`.
88
export const enum CommandTypes {
99
JsxClosingTag = "jsxClosingTag",
1010
Brace = "brace",
@@ -137,7 +137,11 @@ namespace ts.server.protocol {
137137
/* @internal */
138138
SelectionRangeFull = "selectionRange-full",
139139

140-
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
140+
PrepareCallHierarchy = "prepareCallHierarchy",
141+
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
142+
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
143+
144+
// NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`.
141145
}
142146

143147
/**
@@ -2953,6 +2957,48 @@ namespace ts.server.protocol {
29532957
body?: NavigationTree;
29542958
}
29552959

2960+
export interface CallHierarchyItem {
2961+
name: string;
2962+
kind: ScriptElementKind;
2963+
file: string;
2964+
span: TextSpan;
2965+
selectionSpan: TextSpan;
2966+
}
2967+
2968+
export interface CallHierarchyIncomingCall {
2969+
from: CallHierarchyItem;
2970+
fromSpans: TextSpan[];
2971+
}
2972+
2973+
export interface CallHierarchyOutgoingCall {
2974+
to: CallHierarchyItem;
2975+
fromSpans: TextSpan[];
2976+
}
2977+
2978+
export interface PrepareCallHierarchyRequest extends FileLocationRequest {
2979+
command: CommandTypes.PrepareCallHierarchy;
2980+
}
2981+
2982+
export interface PrepareCallHierarchyResponse extends Response {
2983+
readonly body: CallHierarchyItem;
2984+
}
2985+
2986+
export interface ProvideCallHierarchyIncomingCallsRequest extends FileLocationRequest {
2987+
command: CommandTypes.ProvideCallHierarchyIncomingCalls;
2988+
}
2989+
2990+
export interface ProvideCallHierarchyIncomingCallsResponse extends Response {
2991+
readonly body: CallHierarchyIncomingCall[];
2992+
}
2993+
2994+
export interface ProvideCallHierarchyOutgoingCallsRequest extends FileLocationRequest {
2995+
command: CommandTypes.ProvideCallHierarchyOutgoingCalls;
2996+
}
2997+
2998+
export interface ProvideCallHierarchyOutgoingCallsResponse extends Response {
2999+
readonly body: CallHierarchyOutgoingCall[];
3000+
}
3001+
29563002
export const enum IndentStyle {
29573003
None = "None",
29583004
Block = "Block",

src/server/session.ts

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,7 @@ namespace ts.server {
10511051
if (simplifiedResult) {
10521052
return {
10531053
definitions: this.mapDefinitionInfo(definitions, project),
1054-
textSpan: toProcolTextSpan(textSpan, scriptInfo)
1054+
textSpan: toProtocolTextSpan(textSpan, scriptInfo)
10551055
};
10561056
}
10571057

@@ -1306,7 +1306,7 @@ namespace ts.server {
13061306
if (info.canRename) {
13071307
const { canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan } = info;
13081308
return identity<protocol.RenameInfoSuccess>(
1309-
{ canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan: toProcolTextSpan(triggerSpan, scriptInfo) });
1309+
{ canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan: toProtocolTextSpan(triggerSpan, scriptInfo) });
13101310
}
13111311
else {
13121312
return info;
@@ -1406,8 +1406,8 @@ namespace ts.server {
14061406
if (simplifiedResult) {
14071407
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
14081408
return spans.map(s => ({
1409-
textSpan: toProcolTextSpan(s.textSpan, scriptInfo),
1410-
hintSpan: toProcolTextSpan(s.hintSpan, scriptInfo),
1409+
textSpan: toProtocolTextSpan(s.textSpan, scriptInfo),
1410+
hintSpan: toProtocolTextSpan(s.hintSpan, scriptInfo),
14111411
bannerText: s.bannerText,
14121412
autoCollapse: s.autoCollapse,
14131413
kind: s.kind
@@ -1596,7 +1596,7 @@ namespace ts.server {
15961596
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
15971597
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
15981598
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended } = entry;
1599-
const convertedSpan = replacementSpan ? toProcolTextSpan(replacementSpan, scriptInfo) : undefined;
1599+
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
16001600
// Use `hasAction || undefined` to avoid serializing `false`.
16011601
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended };
16021602
}
@@ -1766,7 +1766,7 @@ namespace ts.server {
17661766
text: item.text,
17671767
kind: item.kind,
17681768
kindModifiers: item.kindModifiers,
1769-
spans: item.spans.map(span => toProcolTextSpan(span, scriptInfo)),
1769+
spans: item.spans.map(span => toProtocolTextSpan(span, scriptInfo)),
17701770
childItems: this.mapLocationNavigationBarItems(item.childItems, scriptInfo),
17711771
indent: item.indent
17721772
}));
@@ -1787,8 +1787,8 @@ namespace ts.server {
17871787
text: tree.text,
17881788
kind: tree.kind,
17891789
kindModifiers: tree.kindModifiers,
1790-
spans: tree.spans.map(span => toProcolTextSpan(span, scriptInfo)),
1791-
nameSpan: tree.nameSpan && toProcolTextSpan(tree.nameSpan, scriptInfo),
1790+
spans: tree.spans.map(span => toProtocolTextSpan(span, scriptInfo)),
1791+
nameSpan: tree.nameSpan && toProtocolTextSpan(tree.nameSpan, scriptInfo),
17921792
childItems: map(tree.childItems, item => this.toLocationNavigationTree(item, scriptInfo))
17931793
};
17941794
}
@@ -2050,7 +2050,7 @@ namespace ts.server {
20502050
return !spans
20512051
? undefined
20522052
: simplifiedResult
2053-
? spans.map(span => toProcolTextSpan(span, scriptInfo))
2053+
? spans.map(span => toProtocolTextSpan(span, scriptInfo))
20542054
: spans;
20552055
}
20562056

@@ -2122,14 +2122,81 @@ namespace ts.server {
21222122

21232123
private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange {
21242124
const result: protocol.SelectionRange = {
2125-
textSpan: toProcolTextSpan(selectionRange.textSpan, scriptInfo),
2125+
textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo),
21262126
};
21272127
if (selectionRange.parent) {
21282128
result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo);
21292129
}
21302130
return result;
21312131
}
21322132

2133+
private toProtocolCallHierarchyItem(item: CallHierarchyItem, scriptInfo?: ScriptInfo): protocol.CallHierarchyItem {
2134+
if (!scriptInfo) {
2135+
const file = toNormalizedPath(item.file);
2136+
scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
2137+
if (!scriptInfo) {
2138+
this.projectService.logErrorForScriptInfoNotFound(file);
2139+
return Errors.ThrowNoProject();
2140+
}
2141+
}
2142+
return {
2143+
name: item.name,
2144+
kind: item.kind,
2145+
file: item.file,
2146+
span: toProtocolTextSpan(item.span, scriptInfo),
2147+
selectionSpan: toProtocolTextSpan(item.selectionSpan, scriptInfo)
2148+
};
2149+
}
2150+
2151+
private toProtocolCallHierarchyIncomingCall(incomingCall: CallHierarchyIncomingCall, scriptInfo: ScriptInfo): protocol.CallHierarchyIncomingCall {
2152+
return {
2153+
from: this.toProtocolCallHierarchyItem(incomingCall.from),
2154+
fromSpans: incomingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo))
2155+
};
2156+
}
2157+
2158+
private toProtocolCallHierarchyOutgoingCall(outgoingCall: CallHierarchyOutgoingCall, scriptInfo: ScriptInfo): protocol.CallHierarchyOutgoingCall {
2159+
return {
2160+
to: this.toProtocolCallHierarchyItem(outgoingCall.to),
2161+
fromSpans: outgoingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo))
2162+
};
2163+
}
2164+
2165+
private prepareCallHierarchy(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyItem | undefined {
2166+
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
2167+
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
2168+
if (scriptInfo) {
2169+
const position = this.getPosition(args, scriptInfo);
2170+
const item = languageService.prepareCallHierarchy(file, position);
2171+
return !item
2172+
? undefined
2173+
: this.toProtocolCallHierarchyItem(item, scriptInfo);
2174+
}
2175+
return undefined;
2176+
}
2177+
2178+
private provideCallHierarchyIncomingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyIncomingCall[] {
2179+
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
2180+
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
2181+
if (!scriptInfo) {
2182+
this.projectService.logErrorForScriptInfoNotFound(file);
2183+
return Errors.ThrowNoProject();
2184+
}
2185+
const incomingCalls = languageService.provideCallHierarchyIncomingCalls(file, this.getPosition(args, scriptInfo));
2186+
return incomingCalls.map(call => this.toProtocolCallHierarchyIncomingCall(call, scriptInfo));
2187+
}
2188+
2189+
private provideCallHierarchyOutgoingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyOutgoingCall[] {
2190+
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
2191+
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
2192+
if (!scriptInfo) {
2193+
this.projectService.logErrorForScriptInfoNotFound(file);
2194+
return Errors.ThrowNoProject();
2195+
}
2196+
const outgoingCalls = languageService.provideCallHierarchyOutgoingCalls(file, this.getPosition(args, scriptInfo));
2197+
return outgoingCalls.map(call => this.toProtocolCallHierarchyOutgoingCall(call, scriptInfo));
2198+
}
2199+
21332200
getCanonicalFileName(fileName: string) {
21342201
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
21352202
return normalizePath(name);
@@ -2495,6 +2562,15 @@ namespace ts.server {
24952562
[CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => {
24962563
return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false));
24972564
},
2565+
[CommandNames.PrepareCallHierarchy]: (request: protocol.PrepareCallHierarchyRequest) => {
2566+
return this.requiredResponse(this.prepareCallHierarchy(request.arguments));
2567+
},
2568+
[CommandNames.ProvideCallHierarchyIncomingCalls]: (request: protocol.ProvideCallHierarchyIncomingCallsRequest) => {
2569+
return this.requiredResponse(this.provideCallHierarchyIncomingCalls(request.arguments));
2570+
},
2571+
[CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => {
2572+
return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments));
2573+
},
24982574
});
24992575

25002576
public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {
@@ -2618,16 +2694,22 @@ namespace ts.server {
26182694
readonly project: Project;
26192695
}
26202696

2621-
function toProcolTextSpan(textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.TextSpan {
2697+
// function toLanguageServiceTextSpan(textSpan: protocol.TextSpan, scriptInfo: ScriptInfo): TextSpan {
2698+
// const start = scriptInfo.lineOffsetToPosition(textSpan.start.line, textSpan.start.offset);
2699+
// const end = scriptInfo.lineOffsetToPosition(textSpan.end.line, textSpan.end.offset);
2700+
// return { start, length: end - start };
2701+
// }
2702+
2703+
function toProtocolTextSpan(textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.TextSpan {
26222704
return {
26232705
start: scriptInfo.positionToLineOffset(textSpan.start),
26242706
end: scriptInfo.positionToLineOffset(textSpanEnd(textSpan))
26252707
};
26262708
}
26272709

26282710
function toProtocolTextSpanWithContext(span: TextSpan, contextSpan: TextSpan | undefined, scriptInfo: ScriptInfo): protocol.TextSpanWithContext {
2629-
const textSpan = toProcolTextSpan(span, scriptInfo);
2630-
const contextTextSpan = contextSpan && toProcolTextSpan(contextSpan, scriptInfo);
2711+
const textSpan = toProtocolTextSpan(span, scriptInfo);
2712+
const contextTextSpan = contextSpan && toProtocolTextSpan(contextSpan, scriptInfo);
26312713
return contextTextSpan ?
26322714
{ ...textSpan, contextStart: contextTextSpan.start, contextEnd: contextTextSpan.end } :
26332715
textSpan;

0 commit comments

Comments
 (0)