Skip to content

Commit

Permalink
CallHierarchyService Plugin API eclipse-theia#3765
Browse files Browse the repository at this point in the history
Issue eclipse-theia#3765

Signed-off-by: Thomas MÃder <tmader@redhat.com>
  • Loading branch information
vrubezhny authored and tsmaeder committed Jan 20, 2020
1 parent fd2d384 commit 013a6ac
Show file tree
Hide file tree
Showing 31 changed files with 905 additions and 208 deletions.
25 changes: 10 additions & 15 deletions packages/callhierarchy/src/browser/callhierarchy-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import { ILanguageClient } from '@theia/languages/lib/browser';
import {
ReferencesRequest, DocumentSymbolRequest, DefinitionRequest, TextDocumentPositionParams,
TextDocumentIdentifier, SymbolInformation, Location, Position, DocumentSymbol, ReferenceParams, LocationLink
ReferencesRequest, DocumentSymbolRequest, DefinitionRequest, TextDocumentPositionParams, Range,
TextDocumentIdentifier, SymbolInformation, Location, Position, DocumentSymbol, ReferenceParams, LocationLink, DocumentUri
} from 'monaco-languageclient/lib/services';
import * as utils from './utils';
import { ILogger, Disposable } from '@theia/core';
Expand Down Expand Up @@ -53,47 +53,42 @@ export class CallHierarchyContext implements Disposable {
return model;
}

async getDefinitionLocation(location: Location): Promise<Location | undefined> {
const uri = location.uri;
const { line, character } = location.range.start;
async getDefinitionLocation(uri: DocumentUri, position: Position): Promise<Location | undefined> {

// Definition can be null
// tslint:disable-next-line:no-null-keyword
let locations: Location | Location[] | LocationLink[] | null = null;
try {
locations = await this.languageClient.sendRequest(DefinitionRequest.type, <TextDocumentPositionParams>{
position: Position.create(line, character),
position: position,
textDocument: { uri }
});
} catch (error) {
this.logger.error(`Error from definitions request: ${uri}#${line}/${character}`, error);
this.logger.error(`Error from definitions request: ${uri}#${position.line}/${position.character}`, error);
}
if (!locations) {
return undefined;
}
const targetLocation = Array.isArray(locations) ? locations[0] : locations;
const targetLocation = Array.isArray(locations) ? locations[0] : locations;
return LocationLink.is(targetLocation) ? {
uri: targetLocation.targetUri,
range: targetLocation.targetSelectionRange
} : targetLocation;
}

async getCallerReferences(definition: Location): Promise<Location[]> {
async getCallerReferences(uri: DocumentUri, range: Range): Promise<Location[]> {
try {
const references = await this.languageClient.sendRequest(ReferencesRequest.type, <ReferenceParams>{
context: {
includeDeclaration: false // TODO find out, why definitions are still contained
},
position: {
line: definition.range.start.line,
character: definition.range.start.character
},
position: range.start,
textDocument: {
uri: definition.uri
uri: uri
}
});
const uniqueReferences = utils.filterUnique(references);
const filteredReferences = utils.filterSame(uniqueReferences, definition);
const filteredReferences = utils.filterSame(uniqueReferences, uri, range);
return filteredReferences;
} catch (error) {
this.logger.error('Error from references request', error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CallHierarchyTreeWidget } from './callhierarchy-tree/callhierarchy-tree
import { CALLHIERARCHY_ID } from './callhierarchy';
import { CurrentEditorAccess } from './current-editor-access';
import { CallHierarchyServiceProvider } from './callhierarchy-service';
import URI from '@theia/core/lib/common/uri';

export const CALL_HIERARCHY_TOGGLE_COMMAND_ID = 'callhierachy:toggle';
export const CALL_HIERARCHY_LABEL = 'Call Hierarchy';
Expand Down Expand Up @@ -53,7 +54,7 @@ export class CallHierarchyContribution extends AbstractViewContribution<CallHier
protected isCallHierarchyAvailable(): boolean {
const selection = this.editorAccess.getSelection();
const languageId = this.editorAccess.getLanguageId();
return !!selection && !!languageId && !!this.callHierarchyServiceProvider.get(languageId);
return !!selection && !!languageId && !!this.callHierarchyServiceProvider.get(languageId, new URI(selection.uri));
}

async openView(args?: Partial<OpenViewArguments>): Promise<CallHierarchyTreeWidget> {
Expand Down
45 changes: 27 additions & 18 deletions packages/callhierarchy/src/browser/callhierarchy-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,40 @@
import { injectable, inject } from 'inversify';
import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider';
import {
SymbolInformation, Location, Position, Range, SymbolKind, DocumentSymbol
SymbolInformation, Location, Position, Range, SymbolKind, DocumentSymbol, DocumentUri
} from 'monaco-languageclient/lib/services';
import * as utils from './utils';
import { Definition, Caller } from './callhierarchy';
import { CallHierarchyService } from './callhierarchy-service';
import { ILogger } from '@theia/core';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { CallHierarchyContext } from './callhierarchy-context';
import { LanguageSelector } from '@theia/core/lib/common/language-selector';

export type ExtendedDocumentSymbol = DocumentSymbol & Location & { containerName: string };

@injectable()
export abstract class AbstractDefaultCallHierarchyService implements CallHierarchyService {

@inject(LanguageClientProvider) readonly languageClientProvider: LanguageClientProvider;
@inject(ILogger) readonly logger: ILogger;
@inject(MonacoTextModelService) readonly textModelService: MonacoTextModelService;

abstract get languageId(): string;

get selector(): LanguageSelector {
return this.languageId;
}

/**
* Returns root definition of caller hierarchy.
*/
public async getRootDefinition(location: Location): Promise<Definition | undefined> {
public async getRootDefinition(uri: DocumentUri, position: Position): Promise<Definition | undefined> {
return this.withContext(async services => {
const definitionLocation = await services.getDefinitionLocation(location);
const definitionLocation = await services.getDefinitionLocation(uri, position);
if (!definitionLocation) {
return undefined;
}
const definitionSymbol = await this.getEnclosingRootSymbol(definitionLocation, services);
const definitionSymbol = await this.getEnclosingRootSymbol(definitionLocation.uri, definitionLocation.range, services);
if (!definitionSymbol) {
return undefined;
}
Expand All @@ -59,7 +63,7 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
*/
public async getCallers(definition: Definition): Promise<Caller[] | undefined> {
return this.withContext(async services => {
const callerReferences = await services.getCallerReferences(definition.location);
const callerReferences = await services.getCallerReferences(definition.uri, definition.range);
const callers = this.createCallers(callerReferences, services);
return callers;
});
Expand Down Expand Up @@ -96,7 +100,7 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
const result: Caller[] = [];
const caller2references = new Map<ExtendedDocumentSymbol | SymbolInformation, Location[]>();
for (const reference of callerReferences) {
const callerSymbol = await this.getEnclosingCallerSymbol(reference, context);
const callerSymbol = await this.getEnclosingCallerSymbol(reference.uri, reference.range, context);
if (callerSymbol) {
const references = caller2references.get(callerSymbol);
if (references) {
Expand All @@ -119,8 +123,8 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
return result;
}

protected toCaller(callerDefinition: Definition, references: Location[]): Caller {
return <Caller>{ callerDefinition, references };
protected toCaller(def: Definition, references: Location[]): Caller {
return <Caller>{ callerDefinition: def, references: references.map(ref => ref.range) };
}

protected async toDefinition(symbol: ExtendedDocumentSymbol | SymbolInformation, context: CallHierarchyContext): Promise<Definition | undefined> {
Expand All @@ -131,7 +135,12 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
const symbolName = symbol.name;
const symbolKind = symbol.kind;
const containerName = symbol.containerName;
return <Definition>{ location, symbolName, symbolKind, containerName };
return <Definition>{
uri: location.uri,
range: location.range,
symbolName: symbolName,
symbolKind: symbolKind, containerName
};
}

/**
Expand Down Expand Up @@ -159,16 +168,16 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
* As we only check regions that contain the definition, that is the one with the
* latest start position.
*/
protected async getEnclosingRootSymbol(definition: Location, context: CallHierarchyContext): Promise<ExtendedDocumentSymbol | SymbolInformation | undefined> {
const allSymbols = await context.getAllSymbols(definition.uri);
protected async getEnclosingRootSymbol(uri: DocumentUri, range: Range, context: CallHierarchyContext): Promise<ExtendedDocumentSymbol | SymbolInformation | undefined> {
const allSymbols = await context.getAllSymbols(uri);
if (allSymbols.length === 0) {
return undefined;
}
if (DocumentSymbol.is(allSymbols[0])) {
const symbols = (allSymbols as DocumentSymbol[]);
const containsDefinition = (symbol: DocumentSymbol) => utils.containsRange(symbol.range, definition.range);
const containsDefinition = (symbol: DocumentSymbol) => utils.containsRange(symbol.range, range);
for (const symbol of symbols) {
let containerName = definition.uri.split('/').pop();
let containerName = uri.split('/').pop();
let candidate = containsDefinition(symbol) ? symbol : undefined;
outer: while (candidate) {
const children = candidate.children || [];
Expand All @@ -182,7 +191,7 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
break;
}
if (candidate) {
return <ExtendedDocumentSymbol>{ uri: definition.uri, containerName, ...candidate };
return <ExtendedDocumentSymbol>{ uri: uri, containerName, ...candidate };
}
}
return undefined;
Expand All @@ -192,7 +201,7 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
let bestRange: Range | undefined = undefined;
for (const candidate of symbols) {
const candidateRange = candidate.location.range;
if (utils.containsRange(candidateRange, definition.range)) {
if (utils.containsRange(candidateRange, range)) {
if (!bestMatch || utils.startsAfter(candidateRange, bestRange!)) {
bestMatch = candidate;
bestRange = candidateRange;
Expand All @@ -206,8 +215,8 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
/**
* Finds the symbol that encloses the reference range of a caller
*/
protected async getEnclosingCallerSymbol(reference: Location, context: CallHierarchyContext): Promise<ExtendedDocumentSymbol | SymbolInformation | undefined> {
return this.getEnclosingRootSymbol(reference, context);
protected async getEnclosingCallerSymbol(uri: DocumentUri, range: Range, context: CallHierarchyContext): Promise<ExtendedDocumentSymbol | SymbolInformation | undefined> {
return this.getEnclosingRootSymbol(uri, range, context);
}

/**
Expand Down
49 changes: 41 additions & 8 deletions packages/callhierarchy/src/browser/callhierarchy-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject, named } from 'inversify';
import { Location } from 'vscode-languageserver-types';
import { Definition, Caller } from './callhierarchy';
import { injectable, inject, named, postConstruct } from 'inversify';
import { Position, DocumentUri } from 'vscode-languageserver-types';
import { Definition, Caller, Callee } from './callhierarchy';
import { ContributionProvider } from '@theia/core/lib/common';
import { LanguageSelector, score } from '@theia/core/lib/common/language-selector';
import URI from '@theia/core/lib/common/uri';
import { Disposable } from '@theia/core/lib/common';
import { CancellationToken } from '@theia/core';

export const CallHierarchyService = Symbol('CallHierarchyService');

export interface CallHierarchyService {
readonly languageId: string
getRootDefinition(location: Location): Promise<Definition | undefined>
getCallers(definition: Definition): Promise<Caller[] | undefined>

readonly selector: LanguageSelector;

getRootDefinition(uri: DocumentUri, position: Position, cancellationToken: CancellationToken): Promise<Definition | undefined>
getCallers(definition: Definition, cancellationToken: CancellationToken): Promise<Caller[] | undefined>
getCallees?(definition: Definition, cancellationToken: CancellationToken): Promise<Callee[] | undefined>
}

@injectable()
Expand All @@ -33,7 +40,33 @@ export class CallHierarchyServiceProvider {
@inject(ContributionProvider) @named(CallHierarchyService)
protected readonly contributions: ContributionProvider<CallHierarchyService>;

get(languageId: string): CallHierarchyService | undefined {
return this.contributions.getContributions().find(service => languageId === service.languageId);
private services: CallHierarchyService[] = [];

@postConstruct()
init(): void {
this.services = this.services.concat(this.contributions.getContributions());
}

get(languageId: string, uri: URI): CallHierarchyService | undefined {

return this.services.sort(
(left, right) =>
score(right.selector, uri.scheme, uri.path.toString(), languageId, true) - score(left.selector, uri.scheme, uri.path.toString(), languageId, true))[0];
}

add(service: CallHierarchyService): Disposable {
this.services.push(service);
const that = this;
return {
dispose: () => {
that.remove(service);
}
};
}

private remove(service: CallHierarchyService): boolean {
const length = this.services.length;
this.services = this.services.filter(value => value !== service);
return length !== this.services.length;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,36 @@ import { injectable, inject } from 'inversify';
import { TreeModelImpl, TreeNode } from '@theia/core/lib/browser';
import { CallHierarchyTree, DefinitionNode } from './callhierarchy-tree';
import { CallHierarchyServiceProvider } from '../callhierarchy-service';
import { Location } from 'vscode-languageserver-types';
import { Position } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';

@injectable()
export class CallHierarchyTreeModel extends TreeModelImpl {

private _languageId: string | undefined;

@inject(CallHierarchyTree) protected readonly tree: CallHierarchyTree;
@inject(CallHierarchyServiceProvider) protected readonly callHierarchyServiceProvider: CallHierarchyServiceProvider;

getTree(): CallHierarchyTree {
return this.tree;
}

async initializeCallHierarchy(languageId: string | undefined, location: Location | undefined): Promise<void> {
get languageId(): string | undefined {
return this._languageId;
}

async initializeCallHierarchy(languageId: string | undefined, uri: string | undefined, position: Position | undefined): Promise<void> {
this.tree.root = undefined;
this.tree.callHierarchyService = undefined;
if (languageId && location) {
const callHierarchyService = this.callHierarchyServiceProvider.get(languageId);
this._languageId = languageId;
if (languageId && uri && position) {
const callHierarchyService = this.callHierarchyServiceProvider.get(languageId, new URI(uri));
if (callHierarchyService) {
this.tree.callHierarchyService = callHierarchyService;
const rootDefinition = await callHierarchyService.getRootDefinition(location);
const cancellationSource = new CancellationTokenSource();
const rootDefinition = await callHierarchyService.getRootDefinition(uri, position, cancellationSource.token);
if (rootDefinition) {
const rootNode = DefinitionNode.create(rootDefinition, undefined);
this.tree.root = rootNode;
Expand Down
Loading

0 comments on commit 013a6ac

Please sign in to comment.