Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add support for Call Hierarchies in language server
  • Loading branch information
rbuckton committed Nov 19, 2019
commit 4ca40ce4587b6de09e1c9070471b99a81f359965
6 changes: 4 additions & 2 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,8 +1330,10 @@ namespace ts {
return result;
}

export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[] {
return arrayFrom(arrayToMultiMap(values, getGroupId).values());
export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[];
export function group<T, R>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => R): R[];
export function group<T>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] {
return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector);
}

export function clone<T>(object: T): T {
Expand Down
45 changes: 45 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,51 @@ namespace ts.server {
return notImplemented();
}

private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem {
return {
file: item.file,
name: item.name,
kind: item.kind,
span: this.decodeSpan(item.span, item.file),
selectionSpan: this.decodeSpan(item.selectionSpan, item.file)
};
}

prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | undefined {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request);
return response.body && this.convertCallHierarchyItem(response.body);
}

private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
return {
from: this.convertCallHierarchyItem(item.from),
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file))
};
}

provideCallHierarchyIncomingCalls(fileName: string, position: number) {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request);
return response.body.map(item => this.convertCallHierarchyIncomingCall(item));
}

private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
return {
to: this.convertCallHierarchyItem(item.to),
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file))
};
}

provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request);
return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
}

getProgram(): Program {
throw new Error("SourceFile objects are not serializable through the server protocol.");
}
Expand Down
237 changes: 230 additions & 7 deletions src/harness/fourslash.ts

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,15 @@ namespace Harness.LanguageService {
getEditsForFileRename(): readonly ts.FileTextChanges[] {
throw new Error("Not supported on the shim.");
}
prepareCallHierarchy(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position));
}
provideCallHierarchyIncomingCalls(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position));
}
provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position));
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}
Expand Down
50 changes: 48 additions & 2 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Declaration module describing the TypeScript Server protocol
*/
namespace ts.server.protocol {
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
// NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`.
export const enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
Brace = "brace",
Expand Down Expand Up @@ -137,7 +137,11 @@ namespace ts.server.protocol {
/* @internal */
SelectionRangeFull = "selectionRange-full",

// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
PrepareCallHierarchy = "prepareCallHierarchy",
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",

// NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`.
}

/**
Expand Down Expand Up @@ -2953,6 +2957,48 @@ namespace ts.server.protocol {
body?: NavigationTree;
}

export interface CallHierarchyItem {
name: string;
kind: ScriptElementKind;
file: string;
span: TextSpan;
selectionSpan: TextSpan;
}

export interface CallHierarchyIncomingCall {
from: CallHierarchyItem;
fromSpans: TextSpan[];
}

export interface CallHierarchyOutgoingCall {
to: CallHierarchyItem;
fromSpans: TextSpan[];
}

export interface PrepareCallHierarchyRequest extends FileLocationRequest {
command: CommandTypes.PrepareCallHierarchy;
}

export interface PrepareCallHierarchyResponse extends Response {
readonly body: CallHierarchyItem;
}

export interface ProvideCallHierarchyIncomingCallsRequest extends FileLocationRequest {
command: CommandTypes.ProvideCallHierarchyIncomingCalls;
}

export interface ProvideCallHierarchyIncomingCallsResponse extends Response {
readonly body: CallHierarchyIncomingCall[];
}

export interface ProvideCallHierarchyOutgoingCallsRequest extends FileLocationRequest {
command: CommandTypes.ProvideCallHierarchyOutgoingCalls;
}

export interface ProvideCallHierarchyOutgoingCallsResponse extends Response {
readonly body: CallHierarchyOutgoingCall[];
}

export const enum IndentStyle {
None = "None",
Block = "Block",
Expand Down
108 changes: 95 additions & 13 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ namespace ts.server {
if (simplifiedResult) {
return {
definitions: this.mapDefinitionInfo(definitions, project),
textSpan: toProcolTextSpan(textSpan, scriptInfo)
textSpan: toProtocolTextSpan(textSpan, scriptInfo)
};
}

Expand Down Expand Up @@ -1306,7 +1306,7 @@ namespace ts.server {
if (info.canRename) {
const { canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan } = info;
return identity<protocol.RenameInfoSuccess>(
{ canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan: toProcolTextSpan(triggerSpan, scriptInfo) });
{ canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan: toProtocolTextSpan(triggerSpan, scriptInfo) });
}
else {
return info;
Expand Down Expand Up @@ -1406,8 +1406,8 @@ namespace ts.server {
if (simplifiedResult) {
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
return spans.map(s => ({
textSpan: toProcolTextSpan(s.textSpan, scriptInfo),
hintSpan: toProcolTextSpan(s.hintSpan, scriptInfo),
textSpan: toProtocolTextSpan(s.textSpan, scriptInfo),
hintSpan: toProtocolTextSpan(s.hintSpan, scriptInfo),
bannerText: s.bannerText,
autoCollapse: s.autoCollapse,
kind: s.kind
Expand Down Expand Up @@ -1596,7 +1596,7 @@ namespace ts.server {
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended } = entry;
const convertedSpan = replacementSpan ? toProcolTextSpan(replacementSpan, scriptInfo) : undefined;
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended };
}
Expand Down Expand Up @@ -1766,7 +1766,7 @@ namespace ts.server {
text: item.text,
kind: item.kind,
kindModifiers: item.kindModifiers,
spans: item.spans.map(span => toProcolTextSpan(span, scriptInfo)),
spans: item.spans.map(span => toProtocolTextSpan(span, scriptInfo)),
childItems: this.mapLocationNavigationBarItems(item.childItems, scriptInfo),
indent: item.indent
}));
Expand All @@ -1787,8 +1787,8 @@ namespace ts.server {
text: tree.text,
kind: tree.kind,
kindModifiers: tree.kindModifiers,
spans: tree.spans.map(span => toProcolTextSpan(span, scriptInfo)),
nameSpan: tree.nameSpan && toProcolTextSpan(tree.nameSpan, scriptInfo),
spans: tree.spans.map(span => toProtocolTextSpan(span, scriptInfo)),
nameSpan: tree.nameSpan && toProtocolTextSpan(tree.nameSpan, scriptInfo),
childItems: map(tree.childItems, item => this.toLocationNavigationTree(item, scriptInfo))
};
}
Expand Down Expand Up @@ -2050,7 +2050,7 @@ namespace ts.server {
return !spans
? undefined
: simplifiedResult
? spans.map(span => toProcolTextSpan(span, scriptInfo))
? spans.map(span => toProtocolTextSpan(span, scriptInfo))
: spans;
}

Expand Down Expand Up @@ -2122,14 +2122,81 @@ namespace ts.server {

private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange {
const result: protocol.SelectionRange = {
textSpan: toProcolTextSpan(selectionRange.textSpan, scriptInfo),
textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo),
};
if (selectionRange.parent) {
result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo);
}
return result;
}

private toProtocolCallHierarchyItem(item: CallHierarchyItem, scriptInfo?: ScriptInfo): protocol.CallHierarchyItem {
if (!scriptInfo) {
const file = toNormalizedPath(item.file);
scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (!scriptInfo) {
this.projectService.logErrorForScriptInfoNotFound(file);
return Errors.ThrowNoProject();
}
}
return {
name: item.name,
kind: item.kind,
file: item.file,
span: toProtocolTextSpan(item.span, scriptInfo),
selectionSpan: toProtocolTextSpan(item.selectionSpan, scriptInfo)
};
}

private toProtocolCallHierarchyIncomingCall(incomingCall: CallHierarchyIncomingCall, scriptInfo: ScriptInfo): protocol.CallHierarchyIncomingCall {
return {
from: this.toProtocolCallHierarchyItem(incomingCall.from),
fromSpans: incomingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo))
};
}

private toProtocolCallHierarchyOutgoingCall(outgoingCall: CallHierarchyOutgoingCall, scriptInfo: ScriptInfo): protocol.CallHierarchyOutgoingCall {
return {
to: this.toProtocolCallHierarchyItem(outgoingCall.to),
fromSpans: outgoingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo))
};
}

private prepareCallHierarchy(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyItem | undefined {
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (scriptInfo) {
const position = this.getPosition(args, scriptInfo);
const item = languageService.prepareCallHierarchy(file, position);
return !item
? undefined
: this.toProtocolCallHierarchyItem(item, scriptInfo);
}
return undefined;
}

private provideCallHierarchyIncomingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyIncomingCall[] {
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
Comment thread
rbuckton marked this conversation as resolved.
Outdated
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (!scriptInfo) {
this.projectService.logErrorForScriptInfoNotFound(file);
return Errors.ThrowNoProject();
}
const incomingCalls = languageService.provideCallHierarchyIncomingCalls(file, this.getPosition(args, scriptInfo));
return incomingCalls.map(call => this.toProtocolCallHierarchyIncomingCall(call, scriptInfo));
}

private provideCallHierarchyOutgoingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyOutgoingCall[] {
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (!scriptInfo) {
this.projectService.logErrorForScriptInfoNotFound(file);
return Errors.ThrowNoProject();
}
const outgoingCalls = languageService.provideCallHierarchyOutgoingCalls(file, this.getPosition(args, scriptInfo));
return outgoingCalls.map(call => this.toProtocolCallHierarchyOutgoingCall(call, scriptInfo));
}

getCanonicalFileName(fileName: string) {
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
return normalizePath(name);
Expand Down Expand Up @@ -2495,6 +2562,15 @@ namespace ts.server {
[CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => {
return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.PrepareCallHierarchy]: (request: protocol.PrepareCallHierarchyRequest) => {
return this.requiredResponse(this.prepareCallHierarchy(request.arguments));
},
[CommandNames.ProvideCallHierarchyIncomingCalls]: (request: protocol.ProvideCallHierarchyIncomingCallsRequest) => {
return this.requiredResponse(this.provideCallHierarchyIncomingCalls(request.arguments));
},
[CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => {
return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments));
},
});

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

function toProcolTextSpan(textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.TextSpan {
// function toLanguageServiceTextSpan(textSpan: protocol.TextSpan, scriptInfo: ScriptInfo): TextSpan {
// const start = scriptInfo.lineOffsetToPosition(textSpan.start.line, textSpan.start.offset);
// const end = scriptInfo.lineOffsetToPosition(textSpan.end.line, textSpan.end.offset);
Comment thread
rbuckton marked this conversation as resolved.
Outdated
// return { start, length: end - start };
// }

function toProtocolTextSpan(textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.TextSpan {
return {
start: scriptInfo.positionToLineOffset(textSpan.start),
end: scriptInfo.positionToLineOffset(textSpanEnd(textSpan))
};
}

function toProtocolTextSpanWithContext(span: TextSpan, contextSpan: TextSpan | undefined, scriptInfo: ScriptInfo): protocol.TextSpanWithContext {
const textSpan = toProcolTextSpan(span, scriptInfo);
const contextTextSpan = contextSpan && toProcolTextSpan(contextSpan, scriptInfo);
const textSpan = toProtocolTextSpan(span, scriptInfo);
const contextTextSpan = contextSpan && toProtocolTextSpan(contextSpan, scriptInfo);
return contextTextSpan ?
{ ...textSpan, contextStart: contextTextSpan.start, contextEnd: contextTextSpan.end } :
textSpan;
Expand Down
Loading