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>
Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
  • Loading branch information
vrubezhny authored and tsmaeder committed Jan 21, 2020
1 parent e9449a0 commit be160da
Show file tree
Hide file tree
Showing 30 changed files with 862 additions and 159 deletions.
12 changes: 5 additions & 7 deletions packages/callhierarchy/src/browser/callhierarchy-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { ILanguageClient } from '@theia/languages/lib/browser';
import {
ReferencesRequest, DocumentSymbolRequest, DefinitionRequest, TextDocumentPositionParams,
TextDocumentIdentifier, SymbolInformation, Location, Position, DocumentSymbol, ReferenceParams, LocationLink
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,25 +53,23 @@ 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
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
20 changes: 12 additions & 8 deletions packages/callhierarchy/src/browser/callhierarchy-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,36 @@
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;
}
Expand Down Expand Up @@ -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,9 +135,9 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc
const symbolName = symbol.name;
const symbolKind = symbol.kind;
const containerName = symbol.containerName;
return <Definition>{ location, symbolName, symbolKind, containerName };
const selectionRange = location.range;
return { location, selectionRange, symbolName, symbolKind, containerName };
}

/**
* Override this to configure the callables of your language.
*/
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { DefinitionNode, CallerNode } from './callhierarchy-tree';
import { CallHierarchyTreeModel } from './callhierarchy-tree-model';
import { CALLHIERARCHY_ID, Definition, Caller } from '../callhierarchy';
import URI from '@theia/core/lib/common/uri';
import { Location, Range, SymbolKind } from 'vscode-languageserver-types';
import { Location, Range, SymbolKind, DocumentUri } from 'vscode-languageserver-types';
import { EditorManager } from '@theia/editor/lib/browser';
import * as React from 'react';

Expand Down Expand Up @@ -65,7 +65,7 @@ export class CallHierarchyTreeWidget extends TreeWidget {
}

initializeModel(selection: Location | undefined, languageId: string | undefined): void {
this.model.initializeCallHierarchy(languageId, selection);
this.model.initializeCallHierarchy(languageId, selection ? selection.uri : undefined, selection ? selection.range.start : undefined);
}

protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
Expand Down Expand Up @@ -165,33 +165,35 @@ export class CallHierarchyTreeWidget extends TreeWidget {
}

private openEditor(node: TreeNode, keepFocus: boolean): void {
let location: Location | undefined;

if (DefinitionNode.is(node)) {
location = node.definition.location;
const def = node.definition;
this.doOpenEditor(node.definition.location.uri, def.selectionRange ? def.selectionRange : def.location.range, keepFocus);
}
if (CallerNode.is(node)) {
location = node.caller.references[0];
this.doOpenEditor(node.caller.callerDefinition.location.uri, node.caller.references[0], keepFocus);
}
if (location) {
this.editorManager.open(
new URI(location.uri), {
mode: keepFocus ? 'reveal' : 'activate',
selection: Range.create(location.range.start, location.range.end)
}
).then(editorWidget => {
if (editorWidget.parent instanceof DockPanel) {
editorWidget.parent.selectWidget(editorWidget);
}
});
}

private doOpenEditor(uri: DocumentUri, range: Range, keepFocus: boolean): void {
this.editorManager.open(
new URI(uri), {
mode: keepFocus ? 'reveal' : 'activate',
selection: range
}
).then(editorWidget => {
if (editorWidget.parent instanceof DockPanel) {
editorWidget.parent.selectWidget(editorWidget);
}
});
}

storeState(): object {
const callHierarchyService = this.model.getTree().callHierarchyService;
if (this.model.root && callHierarchyService) {
return {
root: this.deflateForStorage(this.model.root),
languageId: callHierarchyService.languageId,
languageId: this.model.languageId,
};
} else {
return {};
Expand All @@ -202,9 +204,9 @@ export class CallHierarchyTreeWidget extends TreeWidget {
// tslint:disable-next-line:no-any
if ((oldState as any).root && (oldState as any).languageId) {
// tslint:disable-next-line:no-any
this.model.root = this.inflateFromStorage((oldState as any).root);
const root = this.inflateFromStorage((oldState as any).root) as DefinitionNode;
// tslint:disable-next-line:no-any
this.model.initializeCallHierarchy((oldState as any).languageId, (this.model.root as DefinitionNode).definition.location);
this.model.initializeCallHierarchy((oldState as any).languageId, root.definition.location.uri, root.definition.location.range.start);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Definition, Caller } from '../callhierarchy';
import { CallHierarchyService } from '../callhierarchy-service';

import { Md5 } from 'ts-md5/dist/md5';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';

@injectable()
export class CallHierarchyTree extends TreeImpl {
Expand Down Expand Up @@ -49,7 +50,8 @@ export class CallHierarchyTree extends TreeImpl {
definition = parent.caller.callerDefinition;
}
if (definition) {
const callers = await this.callHierarchyService.getCallers(definition);
const cancellationSource = new CancellationTokenSource();
const callers = await this.callHierarchyService.getCallers(definition, cancellationSource.token);
if (!callers) {
return Promise.resolve([]);
}
Expand Down
13 changes: 9 additions & 4 deletions packages/callhierarchy/src/browser/callhierarchy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { Location, SymbolKind } from 'vscode-languageserver-types';
import { Range, SymbolKind, Location } from 'vscode-languageserver-types';

export const CALLHIERARCHY_ID = 'callhierarchy';

export interface Definition {
location: Location,
selectionRange: Range,
symbolName: string,
symbolKind: SymbolKind,
containerName: string,
callers: Caller[] | undefined
containerName: string | undefined
}

export interface Caller {
callerDefinition: Definition,
references: Location[]
references: Range[]
}

export interface Callee {
calleeDefinition: Definition,
references: Range[]
}
14 changes: 13 additions & 1 deletion packages/callhierarchy/src/browser/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { Location, Range } from 'vscode-languageserver-types';
import { Location, Range, Position } from 'vscode-languageserver-types';

/**
* Test if `otherRange` is in `range`. If the ranges are equal, will return true.
Expand All @@ -35,6 +35,10 @@ export function containsRange(range: Range, otherRange: Range): boolean {
return true;
}

export function containsPosition(range: Range, position: Position): boolean {
return comparePosition(range.start, position) >= 0 && comparePosition(range.end, position) <= 0;
}

function sameStart(a: Range, b: Range): boolean {
const pos1 = a.start;
const pos2 = b.start;
Expand All @@ -48,6 +52,14 @@ export function filterSame(locations: Location[], definition: Location): Locatio
);
}

export function comparePosition(left: Position, right: Position): number {
const diff = right.line - left.line;
if (diff !== 0) {
return diff;
}
return right.character - left.character;
}

export function filterUnique(locations: Location[] | null): Location[] {
if (!locations) {
return [];
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit be160da

Please sign in to comment.