Skip to content

Commit

Permalink
[call-hierarchy] Revise implementation of the proposed call hierarchy…
Browse files Browse the repository at this point in the history
… protocol.

Links:
microsoft/language-server-protocol#468
microsoft/vscode-languageserver-node#420

Signed-off-by: Alex Tugarev <alex.tugarev@typefox.io>
  • Loading branch information
AlexTugarev committed Jan 23, 2019
1 parent 6bdc7c6 commit d7a76a1
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 293 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Maintained by [TypeFox](http://typefox.io) and others.
- [x] textDocument/didSave

- [x] textDocument/codeAction
- [x] textDocument/callHierarchy (proposed protocol extension)
- [x] textDocument/completion (incl. completion/resolve)
- [x] textDocument/definition
- [x] textDocument/documentHighlight
Expand Down
134 changes: 82 additions & 52 deletions server/src/calls.ts → server/src/callHierarchy.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,104 @@

import * as tsp from 'typescript/lib/protocol';
import * as lsp from 'vscode-languageserver';
import * as lspcalls from './lsp-protocol.calls.proposed';
import * as lspCallHierarchy from './lsp-protocol.callHierarchy.proposed';
import { TspClient } from './tsp-client';
import { CommandTypes } from './tsp-command-types';
import { uriToPath, toLocation, asRange, Range, toSymbolKind, pathToUri } from './protocol-translation';
import { uriToPath, toLocation, asRange, Range, toSymbolKind, pathToUri, asTextSpan } from './protocol-translation';

export async function computeCallers(tspClient: TspClient, args: lsp.TextDocumentPositionParams): Promise<lspcalls.CallsResult> {
const nullResult = { calls: [] };
const contextDefinition = await getDefinition(tspClient, args);
if (!contextDefinition) {
return nullResult;
export type DocumentProvider = (file: string) => lsp.TextDocument | undefined;

// tslint:disable-next-line:max-line-length
export async function computeCallHierarchy(tspClient: TspClient, documentProvider: DocumentProvider, params: lspCallHierarchy.CallHierarchyParams | lspCallHierarchy.ResolveCallHierarchyItemParams): Promise<lspCallHierarchy.CallHierarchyItem | null> {
const item = ('item' in params) ? params.item : await getItem(tspClient, params);
if (!item) {
return null;
}
const direction = params.direction || 'incoming';
const levelsToResolve = params.resolve || 0;
if (direction === 'incoming') {
// TODO consider levels
if (levelsToResolve > 0) {
const callers = await resolveCallers(tspClient, item);
item.callers = callers;
}
return item;
} else {
// TODO consider levels
if (levelsToResolve > 0) {
const callees = await resolveCallees(tspClient, documentProvider, item);
item.callees = callees;
}
return item;
}
const contextSymbol = await findEnclosingSymbol(tspClient, contextDefinition);
if (!contextSymbol) {
return nullResult;
}

async function getItem(tspClient: TspClient, params: lspCallHierarchy.CallHierarchyParams): Promise<lspCallHierarchy.CallHierarchyItem | null> {
const contextDefinition = await getDefinition(tspClient, params);
const uri = contextDefinition && pathToUri(contextDefinition.file, undefined);
const contextSymbol = contextDefinition && await findEnclosingSymbol(tspClient, contextDefinition);
if (!contextSymbol || !uri) {
return null;
}
const { name, detail, kind, range, selectionRange } = contextSymbol;
return { uri, name, detail, kind, range, selectionRange };
}

async function resolveCallers(tspClient: TspClient, item: lspCallHierarchy.CallHierarchyItem): Promise<lspCallHierarchy.CallHierarchyItem[]> {

const file = uriToPath(item.uri);
if (!file) {
return [];
}
const callerReferences = await findNonDefinitionReferences(tspClient, contextDefinition);
const calls: lspcalls.Call[] = [];
const range = asTextSpan(item.selectionRange);
const callerReferences = await findNonDefinitionReferences(tspClient, { file, ...range });

const callers: lspCallHierarchy.CallHierarchyItem[] = [];
for (const callerReference of callerReferences) {
const symbol = await findEnclosingSymbol(tspClient, callerReference);
if (!symbol) {
continue;
}
const location = toLocation(callerReference, undefined);
calls.push({
location,
symbol
});
const { name, detail, kind, range, selectionRange } = symbol;
const uri = pathToUri(callerReference.file, undefined);
const callLocation = toLocation(callerReference, undefined);
callers.push({ uri, name, detail, kind, range, selectionRange, callLocation });
}
return { calls, symbol: contextSymbol };
return callers;
}
export type DocumentProvider = (file: string) => lsp.TextDocument | undefined;
export async function computeCallees(tspClient: TspClient, args: lsp.TextDocumentPositionParams, documentProvider: DocumentProvider): Promise<lspcalls.CallsResult> {
const nullResult = { calls: [] };
const contextDefinition = await getDefinition(tspClient, args);
if (!contextDefinition) {
return nullResult;
}
const contextSymbol = await findEnclosingSymbol(tspClient, contextDefinition);
if (!contextSymbol) {
return nullResult;
}
const outgoingCallReferences = await findOutgoingCalls(tspClient, contextSymbol, documentProvider);
const calls: lspcalls.Call[] = [];

async function resolveCallees(tspClient: TspClient, documentProvider: DocumentProvider, item: lspCallHierarchy.CallHierarchyItem): Promise<lspCallHierarchy.CallHierarchyItem[]> {

const file = uriToPath(item.uri);
if (!file) {
return [];
}
const range = asTextSpan(item.selectionRange);

const outgoingCallReferences = await findOutgoingCalls(tspClient, item, documentProvider);
const callees: lspCallHierarchy.CallHierarchyItem[] = [];
for (const reference of outgoingCallReferences) {
let definitionSymbol: lsp.DocumentSymbol | undefined;
let uri: string | undefined;
const definitionReferences = await findDefinitionReferences(tspClient, reference);
const definitionReference = definitionReferences[0];
if (!definitionReference) {
continue;
for (const definitionReference of definitionReferences) {
definitionSymbol = await findEnclosingSymbol(tspClient, definitionReference);
if (definitionSymbol) {
uri = pathToUri(definitionReference.file, undefined);
break;
}
}
const definitionSymbol = await findEnclosingSymbol(tspClient, definitionReference);
if (!definitionSymbol) {
if (!definitionSymbol || !uri) {
continue;
}
const location = toLocation(reference, undefined);
calls.push({
location,
symbol: definitionSymbol
});
const callLocation = toLocation(reference, undefined);
const { name, detail, kind, range, selectionRange } = definitionSymbol;
callees.push({ uri, name, detail, kind, range, selectionRange, callLocation });
}
return { calls, symbol: contextSymbol };
return callees;
}

async function findOutgoingCalls(tspClient: TspClient, contextSymbol: lspcalls.DefinitionSymbol, documentProvider: DocumentProvider): Promise<tsp.FileSpan[]> {

async function findOutgoingCalls(tspClient: TspClient, callHierarchyItem: lspCallHierarchy.CallHierarchyItem, documentProvider: DocumentProvider): Promise<tsp.FileSpan[]> {
/**
* The TSP does not provide call references.
* As long as we are not able to access the AST in a tsserver plugin and return the information necessary as metadata to the reponse,
Expand Down Expand Up @@ -104,12 +138,12 @@ async function findOutgoingCalls(tspClient: TspClient, contextSymbol: lspcalls.D
}

const calls: tsp.FileSpan[] = [];
const file = uriToPath(contextSymbol.location.uri)!;
const file = uriToPath(callHierarchyItem.uri)!;
const document = documentProvider(file);
if (!document) {
return calls;
}
const candidateRanges = computeCallCandidates(document, contextSymbol.location.range);
const candidateRanges = computeCallCandidates(document, callHierarchyItem.range);
for (const candidateRange of candidateRanges) {
const call = await validateCall(file, candidateRange);
if (call) {
Expand All @@ -132,7 +166,7 @@ async function getDefinition(tspClient: TspClient, args: lsp.TextDocumentPositio
return definitionResult.body ? definitionResult.body[0] : undefined;
}

async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Promise<lspcalls.DefinitionSymbol | undefined> {
async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Promise<lsp.DocumentSymbol | undefined> {
const file = args.file;
const response = await tspClient.request(CommandTypes.NavTree, { file });
const tree = response.body;
Expand All @@ -141,11 +175,7 @@ async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Pr
}
const pos = lsp.Position.create(args.start.line - 1, args.start.offset - 1);
const symbol = await findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos));
if (!symbol) {
return undefined;
}
const uri = pathToUri(file, undefined);
return lspcalls.DefinitionSymbol.create(uri, symbol);
return symbol;
}

async function findEnclosingSymbolInTree(parent: tsp.NavigationTree, range: lsp.Range): Promise<lsp.DocumentSymbol | undefined> {
Expand Down
6 changes: 3 additions & 3 deletions server/src/lsp-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import * as lsp from 'vscode-languageserver';
import * as lspcalls from './lsp-protocol.calls.proposed'
import * as lspCallHierarchy from './lsp-protocol.callHierarchy.proposed'

import { Logger, LspClientLogger } from './logger';
import { LspServer } from './lsp-server';
Expand Down Expand Up @@ -55,8 +55,8 @@ export function createLspConnection(options: IServerOptions): lsp.IConnection {
connection.onWorkspaceSymbol(server.workspaceSymbol.bind(server));
connection.onFoldingRanges(server.foldingRanges.bind(server));

// proposed `textDocument/calls` request
connection.onRequest(lspcalls.CallsRequest.type, server.calls.bind(server));
// proposed `textDocument/callHierarchy` request
connection.onRequest(lspCallHierarchy.CallHierarchyRequest.type, server.callHierarchy.bind(server));

return connection;
}
128 changes: 128 additions & 0 deletions server/src/lsp-protocol.callHierarchy.proposed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) TypeFox and others. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
'use strict';

import { RequestType, RequestHandler } from 'vscode-jsonrpc';
import { Location, SymbolKind, Range, DocumentSymbol } from 'vscode-languageserver-types';
import * as lsp from 'vscode-languageserver';

export interface CallHierarchyClientCapabilities {
/**
* The text document client capabilities
*/
textDocument?: {
/**
* Capabilities specific to the `textDocument/callHierarchy`
*/
callHierarchy?: {
/**
* Whether implementation supports dynamic registration. If this is set to `true`
* the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
* return value for the corresponding server capability as well.
*/
dynamicRegistration?: boolean;
};
}
}

export interface CallHierarchyServerCapabilities {
/**
* The server provides Call Hierarchy support.
*/
callHierarchyProvider?: boolean | (lsp.TextDocumentRegistrationOptions & lsp.StaticRegistrationOptions);
}

/**
* Request to request or resolve the call hierarchy at a given text document position.
*
* The request's parameter for the first request is of type [CallHierarchyParams](#CallHierarchyParams). The request's
* parameter for the following requests is of type [ResolveCallHierarchyItemParams](#ResolveCallHierarchyItemParams).
*
* The first request returns an item for the given cursor position.
*
* The response is of type [CallHierarchyItem](#CallHierarchyItem) or a Thenable that resolves to such.
*/
export namespace CallHierarchyRequest {
export const type = new RequestType<CallHierarchyParams, ResolveCallHierarchyItemParams, void, lsp.TextDocumentRegistrationOptions>('textDocument/callHierarchy');
export type HandlerSignature = RequestHandler<CallHierarchyParams | ResolveCallHierarchyItemParams, CallHierarchyItem | null, void>;
}

/**
* The parameters of a `textDocument/callHierarchy` request.
*/
export interface CallHierarchyParams extends lsp.TextDocumentPositionParams {
resolve?: number; // the levels to resolve.
/**
* Outgoing direction for callees.
* The default is incoming for callers.
*/
direction?: 'incoming' | 'outgoing';
}

/**
* The parameters of a `textDocument/callHierarchy` request.
*/
export interface ResolveCallHierarchyItemParams {
item: CallHierarchyItem;
resolve: number; // the levels to resolve.
/**
* Outgoing direction for callees.
* The default is incoming for callers.
*/
direction: 'incoming' | 'outgoing';
}

/**
* The result of a `textDocument/callHierarchy` request.
*/
export interface CallHierarchyItem {
/**
* The name of the symbol targeted by the call hierarchy request.
*/
name: string;
/**
* More detail for this symbol, e.g the signature of a function.
*/
detail?: string;
/**
* The kind of this symbol.
*/
kind: SymbolKind;
/**
* URI of the document containing the symbol.
*/
uri: string;
/**
* The range enclosing this symbol not including leading/trailing whitespace but everything else
* like comments. This information is typically used to determine if the the clients cursor is
* inside the symbol to reveal in the symbol in the UI.
*/
range: Range;
/**
* The range that should be selected and revealed when this symbol is being picked, e.g the name of a function.
* Must be contained by the the `range`.
*/
selectionRange: Range;

/**
* The actual location of the call.
*
* Must be defined in resolved callers/callees.
*/
callLocation?: Location;

/**
* List of incoming calls.
*
* *Note*: The items is _unresolved_ if `callers` and `callees` is undefined.
*/
callers?: CallHierarchyItem[];
/**
* List of outgoing calls.
*
* *Note*: The items is _unresolved_ if `callers` and `callees` is undefined.
*/
callees?: CallHierarchyItem[];
}

0 comments on commit d7a76a1

Please sign in to comment.