From a982e5674a8190c0838d33a12e094c7b673089d0 Mon Sep 17 00:00:00 2001 From: Victor Rubezhny Date: Mon, 14 Jan 2019 23:34:50 +0100 Subject: [PATCH] CallHierarchyService Plugin API #3765 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #3765 Signed-off-by: Victor Rubezhny Signed-off-by: Thomas Mäder --- CHANGELOG.md | 4 + .../src/browser/callhierarchy-context.ts | 12 +- .../src/browser/callhierarchy-contribution.ts | 3 +- .../src/browser/callhierarchy-service-impl.ts | 20 +- .../src/browser/callhierarchy-service.ts | 49 ++++- .../callhierarchy-tree-model.ts | 20 +- .../callhierarchy-tree-widget.tsx | 42 +++-- .../callhierarchy-tree/callhierarchy-tree.ts | 4 +- .../src/browser/callhierarchy.ts | 13 +- packages/callhierarchy/src/browser/utils.ts | 14 +- .../common/language-selector}/char-code.ts | 0 .../src/common/language-selector}/glob.ts | 0 .../src/common/language-selector/index.ts | 18 ++ .../language-selector/language-selector.ts | 103 ++++++++++ .../src/common/language-selector}/paths.ts | 3 +- .../src/common/language-selector}/strings.ts | 23 ++- .../src/node/plugin-vscode-init.ts | 2 +- packages/plugin-ext/package.json | 1 + packages/plugin-ext/src/common/paths-util.ts | 2 +- .../src/common/plugin-api-rpc-model.ts | 14 ++ .../plugin-ext/src/common/plugin-api-rpc.ts | 7 +- .../callhierarchy-type-converters.ts | 176 ++++++++++++++++++ .../in-plugin-filesystem-watcher-manager.ts | 2 +- .../src/main/browser/languages-main.ts | 59 ++++++ packages/plugin-ext/src/plugin/languages.ts | 109 +++-------- .../src/plugin/languages/call-hierarchy.ts | 105 +++++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 18 +- .../plugin-ext/src/plugin/type-converters.ts | 2 +- packages/plugin-ext/src/plugin/types-impl.ts | 40 +++- packages/plugin-ext/src/plugin/workspace.ts | 2 +- packages/plugin/src/theia.d.ts | 159 +++++++++++++++- 31 files changed, 866 insertions(+), 160 deletions(-) rename packages/{plugin-ext/src/common => languages/src/common/language-selector}/char-code.ts (100%) rename packages/{plugin-ext/src/common => languages/src/common/language-selector}/glob.ts (100%) create mode 100644 packages/languages/src/common/language-selector/index.ts create mode 100644 packages/languages/src/common/language-selector/language-selector.ts rename packages/{plugin-ext/src/common => languages/src/common/language-selector}/paths.ts (99%) rename packages/{plugin-ext/src/common => languages/src/common/language-selector}/strings.ts (83%) create mode 100644 packages/plugin-ext/src/main/browser/callhierarchy/callhierarchy-type-converters.ts create mode 100644 packages/plugin-ext/src/plugin/languages/call-hierarchy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a9b3f870b85..07295435b9225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Breaking changes: - `theiaPluginDir`: to specify the folder in which to download plugins, in respect to your `package.json` - `theiaPlugins`: to specify the list of plugins in the form of `"id": "url"` - [core] renamed method `registerComositionEventListeners()` to `registerCompositionEventListeners()` [#6961](https://github.com/eclipse-theia/theia/pull/6961) +- [callhierarchy] changed CallHierarchyService to align with VS Code API: + - Use LanaguageSelector instead of language id + - Use position instead of range for lookup of root symbol + - Changed data structures to be like VS Code API - [core] removed `virtual-renderer`. `react-renderer` should be used instead [#6885](https://github.com/eclipse-theia/theia/pull/6885) - [core] removed `virtual-widget`. `react-widget` should be used instead [#6885](https://github.com/eclipse-theia/theia/pull/6885) - [task] renamed method `getStrigifiedTaskSchema()` has been renamed to `getStringifiedTaskSchema()` [#6780](https://github.com/eclipse-theia/theia/pull/6780) diff --git a/packages/callhierarchy/src/browser/callhierarchy-context.ts b/packages/callhierarchy/src/browser/callhierarchy-context.ts index 0c252d3e04ce3..5792510b83a5e 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-context.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-context.ts @@ -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'; @@ -53,25 +53,23 @@ export class CallHierarchyContext implements Disposable { return model; } - async getDefinitionLocation(location: Location): Promise { - const uri = location.uri; - const { line, character } = location.range.start; + async getDefinitionLocation(uri: DocumentUri, position: Position): Promise { // Definition can be null // eslint-disable-next-line no-null/no-null let locations: Location | Location[] | LocationLink[] | null = null; try { locations = await this.languageClient.sendRequest(DefinitionRequest.type, { - 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 diff --git a/packages/callhierarchy/src/browser/callhierarchy-contribution.ts b/packages/callhierarchy/src/browser/callhierarchy-contribution.ts index e532e75b609db..1b926242f9ef9 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-contribution.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-contribution.ts @@ -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'; @@ -53,7 +54,7 @@ export class CallHierarchyContribution extends AbstractViewContribution): Promise { diff --git a/packages/callhierarchy/src/browser/callhierarchy-service-impl.ts b/packages/callhierarchy/src/browser/callhierarchy-service-impl.ts index 00de3a91d9445..c15f69522fc2e 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-service-impl.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-service-impl.ts @@ -15,9 +15,10 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; +import { LanguageSelector } from '@theia/languages/lib/common/language-selector'; 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'; @@ -30,19 +31,22 @@ export type ExtendedDocumentSymbol = DocumentSymbol & Location & { containerName @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 { + public async getRootDefinition(uri: DocumentUri, position: Position): Promise { return this.withContext(async services => { - const definitionLocation = await services.getDefinitionLocation(location); + const definitionLocation = await services.getDefinitionLocation(uri, position); if (!definitionLocation) { return undefined; } @@ -119,8 +123,8 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc return result; } - protected toCaller(callerDefinition: Definition, references: Location[]): Caller { - return { callerDefinition, references }; + protected toCaller(def: Definition, references: Location[]): Caller { + return { callerDefinition: def, references: references.map(ref => ref.range) }; } protected async toDefinition(symbol: ExtendedDocumentSymbol | SymbolInformation, context: CallHierarchyContext): Promise { @@ -131,9 +135,9 @@ export abstract class AbstractDefaultCallHierarchyService implements CallHierarc const symbolName = symbol.name; const symbolKind = symbol.kind; const containerName = symbol.containerName; - return { location, symbolName, symbolKind, containerName }; + const selectionRange = location.range; + return { location, selectionRange, symbolName, symbolKind, containerName }; } - /** * Override this to configure the callables of your language. */ diff --git a/packages/callhierarchy/src/browser/callhierarchy-service.ts b/packages/callhierarchy/src/browser/callhierarchy-service.ts index 8412742a2a972..e63f708633e30 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-service.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-service.ts @@ -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/languages/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 - getCallers(definition: Definition): Promise + + readonly selector: LanguageSelector; + + getRootDefinition(uri: DocumentUri, position: Position, cancellationToken: CancellationToken): Promise + getCallers(definition: Definition, cancellationToken: CancellationToken): Promise + getCallees?(definition: Definition, cancellationToken: CancellationToken): Promise } @injectable() @@ -33,7 +40,33 @@ export class CallHierarchyServiceProvider { @inject(ContributionProvider) @named(CallHierarchyService) protected readonly contributions: ContributionProvider; - 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; } } diff --git a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-model.ts b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-model.ts index d90ad1ec5de87..d2cce3ca0c8a0 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-model.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-model.ts @@ -18,11 +18,15 @@ 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; @@ -30,14 +34,20 @@ export class CallHierarchyTreeModel extends TreeModelImpl { return this.tree; } - async initializeCallHierarchy(languageId: string | undefined, location: Location | undefined): Promise { + get languageId(): string | undefined { + return this._languageId; + } + + async initializeCallHierarchy(languageId: string | undefined, uri: string | undefined, position: Position | undefined): Promise { 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; diff --git a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx index 1efa9a9106606..c7e5c457b2f90 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx +++ b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx @@ -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'; @@ -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[] { @@ -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]; - } - 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); - } - }); + this.doOpenEditor(node.caller.callerDefinition.location.uri, node.caller.references[0], keepFocus); } } + 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 {}; @@ -202,9 +204,9 @@ export class CallHierarchyTreeWidget extends TreeWidget { // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((oldState as any).root && (oldState as any).languageId) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.model.root = this.inflateFromStorage((oldState as any).root); + const root = this.inflateFromStorage((oldState as any).root) as DefinitionNode; // eslint-disable-next-line @typescript-eslint/no-explicit-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); } } } diff --git a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree.ts b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree.ts index 4a832ffee93b7..917fdc8bcfeda 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree.ts +++ b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree.ts @@ -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 { @@ -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([]); } diff --git a/packages/callhierarchy/src/browser/callhierarchy.ts b/packages/callhierarchy/src/browser/callhierarchy.ts index d4f3c25c6d387..bb489847dbfdb 100644 --- a/packages/callhierarchy/src/browser/callhierarchy.ts +++ b/packages/callhierarchy/src/browser/callhierarchy.ts @@ -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[] } diff --git a/packages/callhierarchy/src/browser/utils.ts b/packages/callhierarchy/src/browser/utils.ts index 109b11ba76c1c..a35940eacab5a 100644 --- a/packages/callhierarchy/src/browser/utils.ts +++ b/packages/callhierarchy/src/browser/utils.ts @@ -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. @@ -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; @@ -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 []; diff --git a/packages/plugin-ext/src/common/char-code.ts b/packages/languages/src/common/language-selector/char-code.ts similarity index 100% rename from packages/plugin-ext/src/common/char-code.ts rename to packages/languages/src/common/language-selector/char-code.ts diff --git a/packages/plugin-ext/src/common/glob.ts b/packages/languages/src/common/language-selector/glob.ts similarity index 100% rename from packages/plugin-ext/src/common/glob.ts rename to packages/languages/src/common/language-selector/glob.ts diff --git a/packages/languages/src/common/language-selector/index.ts b/packages/languages/src/common/language-selector/index.ts new file mode 100644 index 0000000000000..d47ab2a7f2953 --- /dev/null +++ b/packages/languages/src/common/language-selector/index.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export * from './language-selector'; +export * from './glob'; diff --git a/packages/languages/src/common/language-selector/language-selector.ts b/packages/languages/src/common/language-selector/language-selector.ts new file mode 100644 index 0000000000000..0bb6901688d5a --- /dev/null +++ b/packages/languages/src/common/language-selector/language-selector.ts @@ -0,0 +1,103 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { match as matchGlobPattern } from './glob'; + +export interface RelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; +} + +export interface LanguageFilter { + language?: string; + scheme?: string; + pattern?: string | RelativePattern; + hasAccessToAllModels?: boolean; +} +export type LanguageSelector = string | LanguageFilter | (string | LanguageFilter)[]; + +export function score(selector: LanguageSelector | undefined, uriScheme: string, path: string, candidateLanguage: string, candidateIsSynchronized: boolean): number { + + if (Array.isArray(selector)) { + let ret = 0; + for (const filter of selector) { + const value = score(filter, uriScheme, path, candidateLanguage, candidateIsSynchronized); + if (value === 10) { + return value; + } + if (value > ret) { + ret = value; + } + } + return ret; + + } else if (typeof selector === 'string') { + + if (!candidateIsSynchronized) { + return 0; + } + + if (selector === '*') { + return 5; + } else if (selector === candidateLanguage) { + return 10; + } else { + return 0; + } + + } else if (selector) { + const { language, pattern, scheme, hasAccessToAllModels } = selector; + + if (!candidateIsSynchronized && !hasAccessToAllModels) { + return 0; + } + + let result = 0; + + if (scheme) { + if (scheme === uriScheme) { + result = 10; + } else if (scheme === '*') { + result = 5; + } else { + return 0; + } + } + + if (language) { + if (language === candidateLanguage) { + result = 10; + } else if (language === '*') { + result = Math.max(result, 5); + } else { + return 0; + } + } + + if (pattern) { + if (pattern === path || matchGlobPattern(pattern, path)) { + result = 10; + } else { + return 0; + } + } + + return result; + + } else { + return 0; + } +} diff --git a/packages/plugin-ext/src/common/paths.ts b/packages/languages/src/common/language-selector/paths.ts similarity index 99% rename from packages/plugin-ext/src/common/paths.ts rename to packages/languages/src/common/language-selector/paths.ts index 68e8eb5638ec7..6500cb9b1d83d 100644 --- a/packages/plugin-ext/src/common/paths.ts +++ b/packages/languages/src/common/language-selector/paths.ts @@ -20,9 +20,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /* eslint-disable no-void */ +/* eslint-disable no-null/no-null */ 'use strict'; -import { startsWithIgnoreCase } from './strings'; import { isWindows } from '@theia/core/lib/common/os'; +import { startsWithIgnoreCase } from './strings'; import { CharCode } from './char-code'; /** diff --git a/packages/plugin-ext/src/common/strings.ts b/packages/languages/src/common/language-selector/strings.ts similarity index 83% rename from packages/plugin-ext/src/common/strings.ts rename to packages/languages/src/common/language-selector/strings.ts index 57b21ba0c2dd2..1d5d2b3692e72 100644 --- a/packages/plugin-ext/src/common/strings.ts +++ b/packages/languages/src/common/language-selector/strings.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -// copied from https://github.com/Microsoft/vscode/blob/bf7ac9201e7a7d01741d4e6e64b5dc9f3197d97b/src/vs/base/common/strings.ts +// based on https://github.com/Microsoft/vscode/blob/bf7ac9201e7a7d01741d4e6e64b5dc9f3197d97b/src/vs/base/common/strings.ts /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -104,3 +104,24 @@ export function startsWithIgnoreCase(str: string, candidate: string): boolean { return doEqualsIgnoreCase(str, candidate, candidateLength); } + +export function* split(s: string, splitter: string): IterableIterator { + let start = 0; + while (start < s.length) { + let end = s.indexOf(splitter, start); + if (end === -1) { + end = s.length; + } + + yield s.substring(start, end); + start = end + splitter.length; + } +} + +export function escapeInvisibleChars(value: string): string { + return value.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); +} + +export function unescapeInvisibleChars(value: string): string { + return value.replace(/\\n/g, '\n').replace(/\\r/g, '\r'); +} diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index 00e21a331cfc0..f245dcc5f132b 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -19,7 +19,7 @@ import * as theia from '@theia/plugin'; import { BackendInitializationFn, PluginAPIFactory, Plugin, emptyPlugin } from '@theia/plugin-ext'; -export const VSCODE_DEFAULT_API_VERSION = '1.38.0'; +export const VSCODE_DEFAULT_API_VERSION = '1.41.1'; /** Set up en as a default locale for VS Code extensions using vscode-nls */ process.env['VSCODE_NLS_CONFIG'] = JSON.stringify({ locale: 'en', availableLanguages: {} }); diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 9d73cc5768f56..d0e92e8e03f1f 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -6,6 +6,7 @@ "typings": "lib/common/index.d.ts", "dependencies": { "@theia/core": "^0.14.0", + "@theia/callhierarchy": "^0.14.0", "@theia/debug": "^0.14.0", "@theia/editor": "^0.14.0", "@theia/file-search": "^0.14.0", diff --git a/packages/plugin-ext/src/common/paths-util.ts b/packages/plugin-ext/src/common/paths-util.ts index 37f21aa6dc286..8b2eb20904d7a 100644 --- a/packages/plugin-ext/src/common/paths-util.ts +++ b/packages/plugin-ext/src/common/paths-util.ts @@ -42,7 +42,7 @@ SOFTWARE. ==== */ -import { sep } from './paths'; +import { sep } from '@theia/languages/lib/common/language-selector/paths'; const replaceRegex = new RegExp('//+', 'g'); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 01a39cf8dd7f2..72392f6e3ae82 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -505,3 +505,17 @@ export interface RenameProvider { provideRenameEdits(model: monaco.editor.ITextModel, position: Position, newName: string): PromiseLike; resolveRenameLocation?(model: monaco.editor.ITextModel, position: Position): PromiseLike; } + +export interface CallHierarchyDefinition { + name: string; + kind: SymbolKind; + detail?: string; + uri: UriComponents; + range: Range; + selectionRange: Range; +} + +export interface CallHierarchyReference { + callerDefinition: CallHierarchyDefinition, + references: Range[] +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index d4eddf919ed1a..1622b83fc3580 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -65,7 +65,9 @@ import { CodeAction, CodeActionContext, FoldingContext, - FoldingRange + FoldingRange, + CallHierarchyDefinition, + CallHierarchyReference } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -1193,6 +1195,8 @@ export interface LanguagesExt { $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: RawColorInfo, token: CancellationToken): PromiseLike; $provideRenameEdits(handle: number, resource: UriComponents, position: Position, newName: string, token: CancellationToken): PromiseLike; $resolveRenameLocation(handle: number, resource: UriComponents, position: Position, token: CancellationToken): PromiseLike; + $provideRootDefinition(handle: number, resource: UriComponents, location: Position, token: CancellationToken): Promise; + $provideCallers(handle: number, definition: CallHierarchyDefinition, token: CancellationToken): Promise; } export const LanguagesMainFactory = Symbol('LanguagesMainFactory'); @@ -1234,6 +1238,7 @@ export interface LanguagesMain { $registerFoldingRangeProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerDocumentColorProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerRenameProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], supportsResoveInitialValues: boolean): void; + $registerCallHierarchyProvider(handle: number, selector: SerializedDocumentFilter[]): void; } export interface WebviewInitData { diff --git a/packages/plugin-ext/src/main/browser/callhierarchy/callhierarchy-type-converters.ts b/packages/plugin-ext/src/main/browser/callhierarchy/callhierarchy-type-converters.ts new file mode 100644 index 0000000000000..bf592d0edf241 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/callhierarchy/callhierarchy-type-converters.ts @@ -0,0 +1,176 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Definition as CallHierarchyDefinition, Caller as CallHierarchyCaller } from '@theia/callhierarchy/lib/browser'; +import * as model from '../../../common/plugin-api-rpc-model'; +import * as rpc from '../../../common/plugin-api-rpc'; +import * as callhierarchy from 'vscode-languageserver-types'; +import URI from 'vscode-uri'; +import { UriComponents } from '../../../common/uri-components'; +import { Location } from 'vscode-languageserver-types'; + +export function toUriComponents(uri: string): UriComponents { + return URI.parse(uri); +} + +export function fromUriComponents(uri: UriComponents): string { + return URI.revive(uri).toString(); +} + +export function fromLocation(location: Location): model.Location { + return { + uri: URI.parse(location.uri), + range: fromRange(location.range) + }; +} + +export function toLocation(uri: UriComponents, range: model.Range): Location { + return { + uri: URI.revive(uri).toString(), + range: toRange(range) + }; +} + +export function fromPosition(position: callhierarchy.Position): rpc.Position { + return { + lineNumber: position.line, + column: position.character + }; +} + +export function fromRange(range: callhierarchy.Range): model.Range { + const { start, end } = range; + return { + startLineNumber: start.line, + startColumn: start.character, + endLineNumber: end.line, + endColumn: end.character + }; +} + +export function toRange(range: model.Range): callhierarchy.Range { + return callhierarchy.Range.create(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); +} + +export namespace SymbolKindConverter { + // tslint:disable-next-line:no-null-keyword + const fromMapping: { [kind: number]: model.SymbolKind } = Object.create(null); + fromMapping[callhierarchy.SymbolKind.File] = model.SymbolKind.File; + fromMapping[callhierarchy.SymbolKind.Module] = model.SymbolKind.Module; + fromMapping[callhierarchy.SymbolKind.Namespace] = model.SymbolKind.Namespace; + fromMapping[callhierarchy.SymbolKind.Package] = model.SymbolKind.Package; + fromMapping[callhierarchy.SymbolKind.Class] = model.SymbolKind.Class; + fromMapping[callhierarchy.SymbolKind.Method] = model.SymbolKind.Method; + fromMapping[callhierarchy.SymbolKind.Property] = model.SymbolKind.Property; + fromMapping[callhierarchy.SymbolKind.Field] = model.SymbolKind.Field; + fromMapping[callhierarchy.SymbolKind.Constructor] = model.SymbolKind.Constructor; + fromMapping[callhierarchy.SymbolKind.Enum] = model.SymbolKind.Enum; + fromMapping[callhierarchy.SymbolKind.Interface] = model.SymbolKind.Interface; + fromMapping[callhierarchy.SymbolKind.Function] = model.SymbolKind.Function; + fromMapping[callhierarchy.SymbolKind.Variable] = model.SymbolKind.Variable; + fromMapping[callhierarchy.SymbolKind.Constant] = model.SymbolKind.Constant; + fromMapping[callhierarchy.SymbolKind.String] = model.SymbolKind.String; + fromMapping[callhierarchy.SymbolKind.Number] = model.SymbolKind.Number; + fromMapping[callhierarchy.SymbolKind.Boolean] = model.SymbolKind.Boolean; + fromMapping[callhierarchy.SymbolKind.Array] = model.SymbolKind.Array; + fromMapping[callhierarchy.SymbolKind.Object] = model.SymbolKind.Object; + fromMapping[callhierarchy.SymbolKind.Key] = model.SymbolKind.Key; + fromMapping[callhierarchy.SymbolKind.Null] = model.SymbolKind.Null; + fromMapping[callhierarchy.SymbolKind.EnumMember] = model.SymbolKind.EnumMember; + fromMapping[callhierarchy.SymbolKind.Struct] = model.SymbolKind.Struct; + fromMapping[callhierarchy.SymbolKind.Event] = model.SymbolKind.Event; + fromMapping[callhierarchy.SymbolKind.Operator] = model.SymbolKind.Operator; + fromMapping[callhierarchy.SymbolKind.TypeParameter] = model.SymbolKind.TypeParameter; + + export function fromSymbolKind(kind: callhierarchy.SymbolKind): model.SymbolKind { + return fromMapping[kind] || model.SymbolKind.Property; + } + + // tslint:disable-next-line:no-null-keyword + const toMapping: { [kind: number]: callhierarchy.SymbolKind } = Object.create(null); + toMapping[model.SymbolKind.File] = callhierarchy.SymbolKind.File; + toMapping[model.SymbolKind.Module] = callhierarchy.SymbolKind.Module; + toMapping[model.SymbolKind.Namespace] = callhierarchy.SymbolKind.Namespace; + toMapping[model.SymbolKind.Package] = callhierarchy.SymbolKind.Package; + toMapping[model.SymbolKind.Class] = callhierarchy.SymbolKind.Class; + toMapping[model.SymbolKind.Method] = callhierarchy.SymbolKind.Method; + toMapping[model.SymbolKind.Property] = callhierarchy.SymbolKind.Property; + toMapping[model.SymbolKind.Field] = callhierarchy.SymbolKind.Field; + toMapping[model.SymbolKind.Constructor] = callhierarchy.SymbolKind.Constructor; + toMapping[model.SymbolKind.Enum] = callhierarchy.SymbolKind.Enum; + toMapping[model.SymbolKind.Interface] = callhierarchy.SymbolKind.Interface; + toMapping[model.SymbolKind.Function] = callhierarchy.SymbolKind.Function; + toMapping[model.SymbolKind.Variable] = callhierarchy.SymbolKind.Variable; + toMapping[model.SymbolKind.Constant] = callhierarchy.SymbolKind.Constant; + toMapping[model.SymbolKind.String] = callhierarchy.SymbolKind.String; + toMapping[model.SymbolKind.Number] = callhierarchy.SymbolKind.Number; + toMapping[model.SymbolKind.Boolean] = callhierarchy.SymbolKind.Boolean; + toMapping[model.SymbolKind.Array] = callhierarchy.SymbolKind.Array; + toMapping[model.SymbolKind.Object] = callhierarchy.SymbolKind.Object; + toMapping[model.SymbolKind.Key] = callhierarchy.SymbolKind.Key; + toMapping[model.SymbolKind.Null] = callhierarchy.SymbolKind.Null; + toMapping[model.SymbolKind.EnumMember] = callhierarchy.SymbolKind.EnumMember; + toMapping[model.SymbolKind.Struct] = callhierarchy.SymbolKind.Struct; + toMapping[model.SymbolKind.Event] = callhierarchy.SymbolKind.Event; + toMapping[model.SymbolKind.Operator] = callhierarchy.SymbolKind.Operator; + toMapping[model.SymbolKind.TypeParameter] = callhierarchy.SymbolKind.TypeParameter; + + export function toSymbolKind(kind: model.SymbolKind): callhierarchy.SymbolKind { + return toMapping[kind] || model.SymbolKind.Property; + } +} + +export function toDefinition(definition: model.CallHierarchyDefinition): CallHierarchyDefinition; +export function toDefinition(definition: model.CallHierarchyDefinition | undefined): CallHierarchyDefinition | undefined; +export function toDefinition(definition: model.CallHierarchyDefinition | undefined): CallHierarchyDefinition | undefined { + if (!definition) { + return undefined; + } + return { + location: { + uri: fromUriComponents(definition.uri), + range: toRange(definition.range) + }, + selectionRange: toRange(definition.selectionRange), + symbolName: definition.name, + symbolKind: SymbolKindConverter.toSymbolKind(definition.kind), + containerName: undefined + }; +} + +export function fromDefinition(definition: CallHierarchyDefinition): model.CallHierarchyDefinition { + return { + uri: toUriComponents(definition.location.uri), + range: fromRange(definition.location.range), + selectionRange: fromRange(definition.selectionRange), + name: definition.symbolName, + kind: SymbolKindConverter.fromSymbolKind(definition.symbolKind) + }; +} + +export function toCaller(caller: model.CallHierarchyReference): CallHierarchyCaller { + return { + callerDefinition: toDefinition(caller.callerDefinition), + references: caller.references.map(toRange) + }; +} + +export function fromCaller(caller: CallHierarchyCaller): model.CallHierarchyReference { + return { + callerDefinition: fromDefinition(caller.callerDefinition), + references: caller.references.map(fromRange) + }; +} diff --git a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts b/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts index 680bf81194efa..bb91d22254d9d 100644 --- a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts +++ b/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts @@ -15,10 +15,10 @@ ********************************************************************************/ import { injectable, inject, postConstruct } from 'inversify'; +import { parse, ParsedPattern, IRelativePattern } from '@theia/languages/lib/common/language-selector'; import { FileSystemWatcher, FileChangeEvent, FileChangeType, FileChange, FileMoveEvent, FileWillMoveEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { WorkspaceExt } from '../../common/plugin-api-rpc'; import { FileWatcherSubscriberOptions } from '../../common/plugin-api-rpc-model'; -import { parse, ParsedPattern, IRelativePattern } from '../../common/glob'; import { RelativePattern } from '../../plugin/types-impl'; import { theiaUritoUriComponents } from '../../common/uri-components'; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 8c1a95e704d72..b89429abde8c1 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -41,6 +41,7 @@ import { } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { fromLanguageSelector } from '../../plugin/type-converters'; +import { DocumentFilter, MonacoModelIdentifier, testGlob } from 'monaco-languageclient/lib'; import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import CoreURI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; @@ -49,6 +50,11 @@ import { ProblemManager } from '@theia/markers/lib/browser'; import * as vst from 'vscode-languageserver-types'; import * as theia from '@theia/plugin'; import { UriComponents } from '../../common/uri-components'; +import { CancellationToken } from '@theia/core/lib/common'; +import { LanguageSelector } from '@theia/languages/lib/common/language-selector'; +import { CallHierarchyService, CallHierarchyServiceProvider, Caller, Definition } from '@theia/callhierarchy/lib/browser'; +import { toDefinition, toUriComponents, fromDefinition, fromPosition, toCaller } from './callhierarchy/callhierarchy-type-converters'; +import { Position, DocumentUri } from 'vscode-languageserver-types'; @injectable() export class LanguagesMainImpl implements LanguagesMain, Disposable { @@ -59,6 +65,9 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { @inject(ProblemManager) private readonly problemManager: ProblemManager; + @inject(CallHierarchyServiceProvider) + private readonly callHierarchyServiceContributionRegistry: CallHierarchyServiceProvider; + private readonly proxy: LanguagesExt; private readonly services = new Map(); private readonly toDispose = new DisposableCollection(); @@ -708,6 +717,56 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { return this.proxy.$provideRenameEdits(handle, model.uri, position, newName, token).then(toMonacoWorkspaceEdit); } + $registerCallHierarchyProvider(handle: number, selector: SerializedDocumentFilter[]): void { + const languageSelector = fromLanguageSelector(selector); + const callHierarchyService = this.createCallHierarchyService(handle, languageSelector); + this.register(handle, this.callHierarchyServiceContributionRegistry.add(callHierarchyService)); + } + + protected createCallHierarchyService(handle: number, language: LanguageSelector): CallHierarchyService { + return { + selector: language, + getRootDefinition: (uri: DocumentUri, position: Position, cancellationToken: CancellationToken) => + this.proxy.$provideRootDefinition(handle, toUriComponents(uri), fromPosition(position), cancellationToken) + .then(def => toDefinition(def)), + getCallers: (definition: Definition, cancellationToken: CancellationToken) => this.proxy.$provideCallers(handle, fromDefinition(definition), cancellationToken) + .then(result => { + if (!result) { + return undefined!; + } + + if (Array.isArray(result)) { + const callers: Caller[] = []; + for (const item of result) { + callers.push(toCaller(item)); + } + return callers; + } + + return undefined!; + }) + }; + } + + protected matchModel(selector: LanguageSelector | undefined, model: MonacoModelIdentifier): boolean { + if (Array.isArray(selector)) { + return selector.some(filter => this.matchModel(filter, model)); + } + if (DocumentFilter.is(selector)) { + if (!!selector.language && selector.language !== model.languageId) { + return false; + } + if (!!selector.scheme && selector.scheme !== model.uri.scheme) { + return false; + } + if (!!selector.pattern && !testGlob(selector.pattern, model.uri.path)) { + return false; + } + return true; + } + return selector === model.languageId; + } + protected resolveRenameLocation(handle: number, model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): monaco.languages.ProviderResult { return this.proxy.$resolveRenameLocation(handle, model.uri, position, token); diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index f972e153832d9..4a22f0ca5c730 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -34,7 +34,6 @@ import { DocumentsExtImpl } from './documents'; import { PluginModel } from '../common/plugin-protocol'; import { Disposable } from './types-impl'; import URI from 'vscode-uri/lib/umd'; -import { match as matchGlobPattern } from '../common/glob'; import { UriComponents } from '../common/uri-components'; import { CompletionContext, @@ -60,6 +59,8 @@ import { CodeActionContext, CodeAction, FoldingRange, + CallHierarchyDefinition, + CallHierarchyReference } from '../common/plugin-api-rpc-model'; import { CompletionAdapter } from './languages/completion'; import { Diagnostics } from './languages/diagnostics'; @@ -85,6 +86,7 @@ import { RenameAdapter } from './languages/rename'; import { Event } from '@theia/core/lib/common/event'; import { CommandRegistryImpl } from './command-registry'; import { DeclarationAdapter } from './languages/declaration'; +import { CallHierarchyAdapter } from './languages/call-hierarchy'; /* eslint-disable @typescript-eslint/indent */ type Adapter = CompletionAdapter | @@ -107,7 +109,8 @@ type Adapter = CompletionAdapter | WorkspaceSymbolAdapter | FoldingProviderAdapter | ColorProviderAdapter | - RenameAdapter; + RenameAdapter | + CallHierarchyAdapter; /* eslint-enable @typescript-eslint/indent */ export class LanguagesExtImpl implements LanguagesExt { @@ -546,6 +549,22 @@ export class LanguagesExtImpl implements LanguagesExt { return this.withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position, token)); } // ### Rename Provider end + + // ### Call Hierarchy Provider begin + registerCallHierarchyProvider(selector: theia.DocumentSelector, provider: theia.CallHierarchyProvider): theia.Disposable { + const callId = this.addNewAdapter(new CallHierarchyAdapter(provider, this.documents)); + this.proxy.$registerCallHierarchyProvider(callId, this.transformDocumentSelector(selector)); + return this.createDisposable(callId); + } + + $provideRootDefinition(handle: number, resource: UriComponents, location: Position, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideRootDefinition(URI.revive(resource), location, token)); + } + + $provideCallers(handle: number, definition: CallHierarchyDefinition, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideCallers(definition, token)); + } + // ### Call Hierarchy Provider end } function serializeEnterRules(rules?: theia.OnEnterRule[]): SerializedOnEnterRule[] | undefined { @@ -584,89 +603,3 @@ function serializeIndentation(indentationRules?: theia.IndentationRule): Seriali unIndentedLinePattern: serializeRegExp(indentationRules.unIndentedLinePattern) }; } - -export interface RelativePattern { - base: string; - pattern: string; - pathToRelative(from: string, to: string): string; -} -export interface LanguageFilter { - language?: string; - scheme?: string; - pattern?: string | RelativePattern; - hasAccessToAllModels?: boolean; -} -export type LanguageSelector = string | LanguageFilter | (string | LanguageFilter)[]; - -export function score(selector: LanguageSelector | undefined, candidateUri: URI, candidateLanguage: string, candidateIsSynchronized: boolean): number { - - if (Array.isArray(selector)) { - let ret = 0; - for (const filter of selector) { - const value = score(filter, candidateUri, candidateLanguage, candidateIsSynchronized); - if (value === 10) { - return value; - } - if (value > ret) { - ret = value; - } - } - return ret; - - } else if (typeof selector === 'string') { - - if (!candidateIsSynchronized) { - return 0; - } - - if (selector === '*') { - return 5; - } else if (selector === candidateLanguage) { - return 10; - } else { - return 0; - } - - } else if (selector) { - const { language, pattern, scheme, hasAccessToAllModels } = selector; - - if (!candidateIsSynchronized && !hasAccessToAllModels) { - return 0; - } - - let result = 0; - - if (scheme) { - if (scheme === candidateUri.scheme) { - result = 10; - } else if (scheme === '*') { - result = 5; - } else { - return 0; - } - } - - if (language) { - if (language === candidateLanguage) { - result = 10; - } else if (language === '*') { - result = Math.max(result, 5); - } else { - return 0; - } - } - - if (pattern) { - if (pattern === candidateUri.fsPath || matchGlobPattern(pattern, candidateUri.fsPath)) { - result = 10; - } else { - return 0; - } - } - - return result; - - } else { - return 0; - } -} diff --git a/packages/plugin-ext/src/plugin/languages/call-hierarchy.ts b/packages/plugin-ext/src/plugin/languages/call-hierarchy.ts new file mode 100644 index 0000000000000..d9ed02a23b359 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/call-hierarchy.ts @@ -0,0 +1,105 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import URI from 'vscode-uri/lib/umd'; +import * as theia from '@theia/plugin'; +import * as Converter from '../type-converters'; +import { DocumentsExtImpl } from '../documents'; +import * as model from '../../common/plugin-api-rpc-model'; +import * as rpc from '../../common/plugin-api-rpc'; +import * as types from '../types-impl'; + +export class CallHierarchyAdapter { + + constructor( + private readonly provider: theia.CallHierarchyProvider, + private readonly documents: DocumentsExtImpl + ) { } + + async provideRootDefinition(resource: URI, position: rpc.Position, token: theia.CancellationToken): Promise { + const documentData = this.documents.getDocumentData(resource); + if (!documentData) { + return Promise.reject(new Error(`There is no document for ${resource}`)); + } + + const definition = await this.provider.prepareCallHierarchy(documentData.document, + new types.Position( + position.lineNumber, + position.column + ), + token); + if (!definition) { + return undefined; + } + + return this.fromCallHierarchyitem(definition); + } + + async provideCallers(definition: model.CallHierarchyDefinition, token: theia.CancellationToken): Promise { + const callers = await this.provider.provideCallHierarchyIncomingCalls(this.toCallHierarchyItem(definition), token); + if (!callers) { + return undefined; + } + + return callers.map(item => this.fromCallHierarchyIncomingCall(item)); + } + + private fromCallHierarchyitem(item: theia.CallHierarchyItem): model.CallHierarchyDefinition { + return { + uri: item.uri, + range: this.fromRange(item.range), + selectionRange: this.fromRange(item.selectionRange), + name: item.name, + kind: item.kind + }; + } + + private fromRange(range: theia.Range): model.Range { + return { + startLineNumber: range.start.line, + startColumn: range.start.character, + endLineNumber: range.end.line, + endColumn: range.end.character + }; + } + + private toRange(range: model.Range): types.Range { + return new types.Range( + range.startLineNumber, + range.startColumn, + range.endLineNumber, + range.endColumn + ); + } + + private toCallHierarchyItem(definition: model.CallHierarchyDefinition): theia.CallHierarchyItem { + return new types.CallHierarchyItem( + Converter.SymbolKind.toSymbolKind(definition.kind), + definition.name, + definition.detail ? definition.detail : '', + URI.revive(definition.uri), + this.toRange(definition.range), + this.toRange(definition.selectionRange), + ); + } + + private fromCallHierarchyIncomingCall(caller: theia.CallHierarchyIncomingCall): model.CallHierarchyReference { + return { + callerDefinition: this.fromCallHierarchyitem(caller.from), + references: caller.fromRanges.map(l => this.fromRange(l)) + }; + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 14092f170045e..f393b5905474e 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -111,7 +111,10 @@ import { FileSystemError, CommentThreadCollapsibleState, QuickInputButtons, - CommentMode + CommentMode, + CallHierarchyItem, + CallHierarchyIncomingCall, + CallHierarchyOutgoingCall } from './types-impl'; import { SymbolKind } from '../common/plugin-api-rpc-model'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; @@ -122,11 +125,12 @@ import { TextEditorCursorStyle } from '../common/editor-options'; import { PreferenceRegistryExtImpl } from './preference-registry'; import { OutputChannelRegistryExtImpl } from './output-channel-registry'; import { TerminalServiceExtImpl, TerminalExtImpl } from './terminal-ext'; -import { LanguagesExtImpl, score } from './languages'; +import { LanguagesExtImpl } from './languages'; import { fromDocumentSelector, pluginToPluginInfo } from './type-converters'; import { DialogsExtImpl } from './dialogs'; import { NotificationExtImpl } from './notification'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { score } from '@theia/languages/lib/common/language-selector'; import { MarkdownString } from './markdown-string'; import { TreeViewsExtImpl } from './tree/tree-views'; import { LanguagesContributionExtImpl } from './languages-contribution-ext'; @@ -527,7 +531,7 @@ export function createAPIFactory( return languagesExt.changeLanguage(document.uri, languageId); }, match(selector: theia.DocumentSelector, document: theia.TextDocument): number { - return score(fromDocumentSelector(selector), document.uri, document.languageId, true); + return score(fromDocumentSelector(selector), document.uri.scheme, document.uri.path, document.languageId, true); }, get onDidChangeDiagnostics(): theia.Event { return languagesExt.onDidChangeDiagnostics; @@ -619,6 +623,9 @@ export function createAPIFactory( registerRenameProvider(selector: theia.DocumentSelector, provider: theia.RenameProvider): theia.Disposable { return languagesExt.registerRenameProvider(selector, provider, pluginToPluginInfo(plugin)); }, + registerCallHierarchyProvider(selector: theia.DocumentSelector, provider: theia.CallHierarchyProvider): theia.Disposable { + return languagesExt.registerCallHierarchyProvider(selector, provider); + } }; const plugins: typeof theia.plugins = { @@ -847,7 +854,10 @@ export function createAPIFactory( FileSystemError, CommentThreadCollapsibleState, QuickInputButtons, - CommentMode + CommentMode, + CallHierarchyItem, + CallHierarchyIncomingCall, + CallHierarchyOutgoingCall }; }; } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 5d69adaac842c..9d4203e68012f 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -23,7 +23,7 @@ import { ResourceTextEditDto, Selection, TaskDto, WorkspaceEditDto } from '../common/plugin-api-rpc'; import * as model from '../common/plugin-api-rpc-model'; -import { LanguageFilter, LanguageSelector, RelativePattern } from './languages'; +import { LanguageFilter, LanguageSelector, RelativePattern } from '@theia/languages/lib/common/language-selector'; import { isMarkdownString, MarkdownString } from './markdown-string'; import { Item } from './quick-open'; import * as types from './types-impl'; diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 71d19abc07c69..b194c6b86021a 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -27,7 +27,7 @@ import * as theia from '@theia/plugin'; import * as crypto from 'crypto'; import URI from 'vscode-uri'; import { relative } from '../common/paths-util'; -import { startsWithIgnoreCase } from '../common/strings'; +import { startsWithIgnoreCase } from '@theia/languages/lib/common/language-selector/strings'; import { MarkdownString, isMarkdownString } from './markdown-string'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -2011,3 +2011,41 @@ export enum WebviewPanelTargetArea { Right = 'right', Bottom = 'bottom' } +export class CallHierarchyItem { + kind: SymbolKind; + name: string; + detail?: string; + uri: URI; + range: Range; + selectionRange: Range; + + constructor(kind: SymbolKind, name: string, detail: string, uri: URI, range: Range, selectionRange: Range) { + this.kind = kind; + this.name = name; + this.detail = detail; + this.uri = uri; + this.range = range; + this.selectionRange = selectionRange; + } +} + +export class CallHierarchyIncomingCall { + + from: theia.CallHierarchyItem; + fromRanges: theia.Range[]; + + constructor(item: theia.CallHierarchyItem, fromRanges: theia.Range[]) { + this.fromRanges = fromRanges; + this.from = item; + } +} +export class CallHierarchyOutgoingCall { + + to: theia.CallHierarchyItem; + fromRanges: theia.Range[]; + + constructor(item: theia.CallHierarchyItem, fromRanges: theia.Range[]) { + this.fromRanges = fromRanges; + this.to = item; + } +} diff --git a/packages/plugin-ext/src/plugin/workspace.ts b/packages/plugin-ext/src/plugin/workspace.ts index 3cdc769925ddf..6274dee598ca8 100644 --- a/packages/plugin-ext/src/plugin/workspace.ts +++ b/packages/plugin-ext/src/plugin/workspace.ts @@ -37,7 +37,7 @@ import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; import { InPluginFileSystemWatcherProxy } from './in-plugin-filesystem-watcher-proxy'; import URI from 'vscode-uri'; import { FileStat } from '@theia/filesystem/lib/common'; -import { normalize } from '../common/paths'; +import { normalize } from '@theia/languages/lib/common/language-selector/paths'; import { relative } from '../common/paths-util'; import { Schemes } from '../common/uri-components'; import { toWorkspaceFolder } from './type-converters'; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 82e9fea36f1c5..840eee32a5f16 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -50,10 +50,10 @@ declare module '@theia/plugin' { static from(...disposableLikes: { dispose: () => any }[]): Disposable; /** - * Creates a new Disposable calling the provided function - * on dispose. - * @param callOnDispose Function that disposes something. - */ + * Creates a new Disposable calling the provided function + * on dispose. + * @param callOnDispose Function that disposes something. + */ constructor(callOnDispose: Function); } @@ -7245,6 +7245,19 @@ declare module '@theia/plugin' { * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable; + + /** + * Register a call hierarchy provider. + * + * Multiple provider can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param service A call hierarchy provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable; } /** @@ -8971,4 +8984,142 @@ declare module '@theia/plugin' { */ export function createCommentController(id: string, label: string): CommentController; } + + /** + * Represents programming constructs like functions or constructors in the context + * of call hierarchy. + */ + export class CallHierarchyItem { + /** + * The name of this item. + */ + name: string; + + /** + * The kind of this item. + */ + kind: SymbolKind; + + /** + * More detail for this item, e.g. the signature of a function. + */ + detail?: string; + + /** + * The resource identifier of this item. + */ + uri: Uri; + + /** + * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. + */ + 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 [`range`](#CallHierarchyItem.range). + */ + selectionRange: Range; + + /** + * Creates a new call hierarchy item. + */ + constructor(kind: SymbolKind, name: string, detail: string, uri: Uri, range: Range, selectionRange: Range); + } + + /** + * Represents an incoming call, e.g. a caller of a method or constructor. + */ + export class CallHierarchyIncomingCall { + + /** + * The item that makes the call. + */ + from: CallHierarchyItem; + + /** + * The range at which at which the calls appears. This is relative to the caller + * denoted by [`this.from`](#CallHierarchyIncomingCall.from). + */ + fromRanges: Range[]; + + /** + * Create a new call object. + * + * @param item The item making the call. + * @param fromRanges The ranges at which the calls appear. + */ + constructor(item: CallHierarchyItem, fromRanges: Range[]); + } + + /** + * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. + */ + export class CallHierarchyOutgoingCall { + + /** + * The item that is called. + */ + to: CallHierarchyItem; + + /** + * The range at which this item is called. This is the range relative to the caller, e.g the item + * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) + * and not [`this.to`](#CallHierarchyOutgoingCall.to). + */ + fromRanges: Range[]; + + /** + * Create a new call object. + * + * @param item The item being called + * @param fromRanges The ranges at which the calls appear. + */ + constructor(item: CallHierarchyItem, fromRanges: Range[]); + } + + /** + * The call hierarchy provider interface describes the constract between extensions + * and the call hierarchy feature which allows to browse calls and caller of function, + * methods, constructor etc. + */ + export interface CallHierarchyProvider { + + /** + * Bootstraps call hierarchy by returning the item that is denoted by the given document + * and position. This item will be used as entry into the call graph. Providers should + * return `undefined` or `null` when there is no item at the given location. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @returns A call hierarchy item or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + + /** + * Provide all incoming calls for an item, e.g all callers for a method. In graph terms this describes directed + * and annotated edges inside the call graph, e.g the given item is the starting node and the result is the nodes + * that can be reached. + * + * @param item The hierarchy item for which incoming calls should be computed. + * @param token A cancellation token. + * @returns A set of incoming calls or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideCallHierarchyIncomingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult; + + /** + * Provide all outgoing calls for an item, e.g call calls to functions, methods, or constructors from the given item. In + * graph terms this describes directed and annotated edges inside the call graph, e.g the given item is the starting + * node and the result is the nodes that can be reached. + * + * @param item The hierarchy item for which outgoing calls should be computed. + * @param token A cancellation token. + * @returns A set of outgoing calls or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult; + } }