diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 424fd364c57d8..ac557db625ee2 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -42,6 +42,10 @@ "name": "vs/workbench/contrib/codeinset", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/callHierarchy", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/comments", "project": "vscode-workbench" diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 8a3c9d3463764..6b04f0a26216d 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -13,6 +13,7 @@ "./vs/workbench/common/**/*", "./vs/workbench/browser/**/*", "./vs/workbench/electron-browser/**/*", + "./vs/workbench/contrib/callHierarchy/**/*", "./vs/workbench/contrib/emmet/**/*", "./vs/workbench/contrib/extensions/**/*", "./vs/workbench/contrib/externalTerminal/**/*", @@ -391,4 +392,4 @@ "./typings/require-monaco.d.ts", "./vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts" ] -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/quickOpen/quickOpen.ts b/src/vs/editor/contrib/quickOpen/quickOpen.ts index 48c01f13058c0..dba67c40b01dd 100644 --- a/src/vs/editor/contrib/quickOpen/quickOpen.ts +++ b/src/vs/editor/contrib/quickOpen/quickOpen.ts @@ -11,6 +11,7 @@ import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; export function getDocumentSymbols(model: ITextModel, flat: boolean, token: CancellationToken): Promise { @@ -70,8 +71,20 @@ registerLanguageCommand('_executeDocumentSymbolProvider', function (accessor, ar throw illegalArgument('resource'); } const model = accessor.get(IModelService).getModel(resource); - if (!model) { - throw illegalArgument('resource'); + if (model) { + return getDocumentSymbols(model, false, CancellationToken.None); } - return getDocumentSymbols(model, false, CancellationToken.None); + + return accessor.get(ITextModelService).createModelReference(resource).then(reference => { + return new Promise((resolve, reject) => { + try { + const result = getDocumentSymbols(reference.object.textEditorModel, false, CancellationToken.None); + resolve(result); + } catch (err) { + reject(err); + } + }).finally(() => { + reference.dispose(); + }); + }); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9725170a08b15..d3e8631aa558d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,6 +16,46 @@ declare module 'vscode' { + //#region Joh - call hierarchy + + export enum CallHierarchyDirection { + CallsFrom = 1, + CallsTo = 2, + } + + 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); + } + + export interface CallHierarchyItemProvider { + + provideCallHierarchyItem( + document: TextDocument, + postion: Position, + token: CancellationToken + ): ProviderResult; + + resolveCallHierarchyItem( + item: CallHierarchyItem, + direction: CallHierarchyDirection, + token: CancellationToken + ): ProviderResult<[CallHierarchyItem, Location[]][]>; + } + + export namespace languages { + export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyItemProvider): Disposable; + } + + //#endregion + + //#region Alex - resolvers export class ResolvedAuthority { diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index d735e82055c44..e5d3ac02ff915 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto } from '../node/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IHeapService } from './mainThreadHeapService'; @@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -114,6 +115,13 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return data; } + private static _reviveCallHierarchyItemDto(data: CallHierarchyDto | undefined): callh.CallHierarchyItem { + if (data) { + data.uri = URI.revive(data.uri); + } + return data as callh.CallHierarchyItem; + } + //#endregion // --- outline @@ -471,6 +479,30 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }); } + // --- call hierarchy + + $registerCallHierarchyProvider(handle: number, selector: ISerializedDocumentFilter[]): void { + this._registrations[handle] = callh.CallHierarchyProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { + provideCallHierarchyItem: (document, position, token) => { + return this._proxy.$provideCallHierarchyItem(handle, document.uri, position, token).then(MainThreadLanguageFeatures._reviveCallHierarchyItemDto); + }, + resolveCallHierarchyItem: (item, direction, token) => { + return this._proxy.$resolveCallHierarchyItem(handle, item, direction, token).then(data => { + if (data) { + for (let i = 0; i < data.length; i++) { + const [item, locations] = data[i]; + data[i] = [ + MainThreadLanguageFeatures._reviveCallHierarchyItemDto(item), + MainThreadLanguageFeatures._reviveLocationDto(locations) + ]; + } + } + return data as [callh.CallHierarchyItem, modes.Location[]][]; + }); + } + }); + } + // --- configuration private static _reviveRegExp(regExp: ISerializedRegExp): RegExp { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 64b5032357e78..7472c494fe99c 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -357,6 +357,10 @@ export function createApiFactory( registerSelectionRangeProvider(selector: vscode.DocumentSelector, provider: vscode.SelectionRangeProvider): vscode.Disposable { return extHostLanguageFeatures.registerSelectionRangeProvider(extension, selector, provider); }, + registerCallHierarchyProvider(selector: vscode.DocumentSelector, provider: vscode.CallHierarchyItemProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerCallHierarchyProvider(extension, selector, provider); + }, setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration): vscode.Disposable => { return extHostLanguageFeatures.setLanguageConfiguration(language, configuration); } @@ -831,7 +835,9 @@ export function createApiFactory( Uri: URI, ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, - // functions + // proposed + CallHierarchyDirection: extHostTypes.CallHierarchyDirection, + CallHierarchyItem: extHostTypes.CallHierarchyItem }; }; } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index c24fbb1016c31..1d7d5adbb3449 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -46,6 +46,7 @@ import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityReso import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IRemoteConsoleLog } from 'vs/base/node/console'; import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; +import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -337,6 +338,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentColorProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $registerFoldingRangeProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $registerSelectionRangeProvider(handle: number, selector: ISerializedDocumentFilter[]): void; + $registerCallHierarchyProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ISerializedLanguageConfiguration): void; } @@ -920,6 +922,16 @@ export interface CodeLensDto extends ObjectIdentifier { export type CodeInsetDto = ObjectIdentifier & codeInset.ICodeInsetSymbol; +export interface CallHierarchyDto { + _id: number; + kind: modes.SymbolKind; + name: string; + detail?: string; + uri: UriComponents; + range: IRange; + selectionRange: IRange; +} + export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; @@ -952,6 +964,8 @@ export interface ExtHostLanguageFeaturesShape { $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo, token: CancellationToken): Promise; $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext, token: CancellationToken): Promise; $provideSelectionRanges(handle: number, resource: UriComponents, positions: IPosition[], token: CancellationToken): Promise; + $provideCallHierarchyItem(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $resolveCallHierarchyItem(handle: number, item: callHierarchy.CallHierarchyItem, direction: callHierarchy.CallHierarchyDirection, token: CancellationToken): Promise<[CallHierarchyDto, modes.Location[]][]>; } export interface ExtHostQuickOpenShape { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 0020a4bf01c06..b509dba344bd4 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -28,6 +28,8 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtHostWebview } from 'vs/workbench/api/node/extHostWebview'; import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import { generateUuid } from 'vs/base/common/uuid'; +import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { LRUCache } from 'vs/base/common/map'; // --- adapter @@ -963,11 +965,66 @@ class SelectionRangeAdapter { } } +class CallHierarchyAdapter { + + // todo@joh keep object (heap service, lifecycle) + private readonly _cache = new LRUCache(1000, 0.8); + private _idPool = 0; + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.CallHierarchyItemProvider + ) { } + + provideCallHierarchyItem(resource: URI, pos: IPosition, token: CancellationToken): Promise { + const document = this._documents.getDocument(resource); + const position = typeConvert.Position.to(pos); + + return asPromise(() => this._provider.provideCallHierarchyItem(document, position, token)).then(item => { + if (!item) { + return undefined; + } + return this._fromItem(item); + }); + } + + resolveCallHierarchyItem(item: callHierarchy.CallHierarchyItem, direction: callHierarchy.CallHierarchyDirection, token: CancellationToken): Promise<[callHierarchy.CallHierarchyItem, modes.Location[]][]> { + return asPromise(() => this._provider.resolveCallHierarchyItem( + this._cache.get(item._id)!, + direction as number, token) // todo@joh proper convert + ).then(data => { + if (!data) { + return []; + } + return data.map(tuple => { + return <[callHierarchy.CallHierarchyItem, modes.Location[]]>[ + this._fromItem(tuple[0]), + tuple[1].map(typeConvert.location.from) + ]; + }); + }); + } + + private _fromItem(item: vscode.CallHierarchyItem, _id: number = this._idPool++): callHierarchy.CallHierarchyItem { + const res = { + _id, + name: item.name, + detail: item.detail, + kind: typeConvert.SymbolKind.from(item.kind), + uri: item.uri, + range: typeConvert.Range.from(item.range), + selectionRange: typeConvert.Range.from(item.selectionRange), + }; + this._cache.set(_id, item); + return res; + } +} + type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter - | ColorProviderAdapter | FoldingProviderAdapter | CodeInsetAdapter | DeclarationAdapter | SelectionRangeAdapter; + | ColorProviderAdapter | FoldingProviderAdapter | CodeInsetAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter; class AdapterData { constructor( @@ -1415,6 +1472,22 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, SelectionRangeAdapter, adapter => adapter.provideSelectionRanges(URI.revive(resource), positions, token)); } + // --- call hierarchy + + registerCallHierarchyProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CallHierarchyItemProvider): vscode.Disposable { + const handle = this._addNewAdapter(new CallHierarchyAdapter(this._documents, provider), extension); + this._proxy.$registerCallHierarchyProvider(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideCallHierarchyItem(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { + return this._withAdapter(handle, CallHierarchyAdapter, adapter => adapter.provideCallHierarchyItem(URI.revive(resource), position, token)); + } + + $resolveCallHierarchyItem(handle: number, item: callHierarchy.CallHierarchyItem, direction: callHierarchy.CallHierarchyDirection, token: CancellationToken): Promise<[callHierarchy.CallHierarchyItem, modes.Location[]][]> { + return this._withAdapter(handle, CallHierarchyAdapter, adapter => adapter.resolveCallHierarchyItem(item, direction, token)); + } + // --- configuration private static _serializeRegExp(regExp: RegExp): ISerializedRegExp { diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index ebc2cf411c99b..078f2c166992d 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -883,7 +883,6 @@ export namespace TextDocumentSaveReason { } } - export namespace EndOfLine { export function from(eol: vscode.EndOfLine): EndOfLineSequence | undefined { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 21fb78b59f8e3..3e9ef712e6cbf 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1111,6 +1111,29 @@ export class SelectionRange { } +export enum CallHierarchyDirection { + CallsFrom = 1, + CallsTo = 2, +} + +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; + } +} + @es5ClassCompat export class CodeLens { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts new file mode 100644 index 0000000000000..8eaa17c0040f0 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { CallHierarchyProviderRegistry, CallHierarchyDirection } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { CallHierarchyTreePeekWidget } from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek'; +import { Event } from 'vs/base/common/event'; +import { registerEditorContribution, registerEditorAction, EditorAction, registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; + + +const _ctxHasCompletionItemProvider = new RawContextKey('editorHasCallHierarchyProvider', false); +const _ctxCallHierarchyVisible = new RawContextKey('callHierarchyVisible', false); + +class CallHierarchyController extends Disposable implements IEditorContribution { + + static Id = 'callHierarchy'; + + static get(editor: ICodeEditor): CallHierarchyController { + return editor.getContribution(CallHierarchyController.Id); + } + + private readonly _ctxHasProvider: IContextKey; + private readonly _ctxIsVisible: IContextKey; + + private _sessionDispose: IDisposable[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._ctxIsVisible = _ctxCallHierarchyVisible.bindTo(this._contextKeyService); + this._ctxHasProvider = _ctxHasCompletionItemProvider.bindTo(this._contextKeyService); + this._register(Event.any(_editor.onDidChangeModel, _editor.onDidChangeModelLanguage, CallHierarchyProviderRegistry.onDidChange)(() => { + this._ctxHasProvider.set(_editor.hasModel() && CallHierarchyProviderRegistry.has(_editor.getModel())); + })); + + this._register({ dispose: () => dispose(this._sessionDispose) }); + } + + dispose(): void { + this._ctxHasProvider.reset(); + this._ctxIsVisible.reset(); + super.dispose(); + } + + getId(): string { + return CallHierarchyController.Id; + } + + async startCallHierarchy(): Promise { + this._sessionDispose = dispose(this._sessionDispose); + + if (!this._editor.hasModel()) { + return; + } + + const model = this._editor.getModel(); + const position = this._editor.getPosition(); + const [provider] = CallHierarchyProviderRegistry.ordered(model); + if (!provider) { + return; + } + + Event.any(this._editor.onDidChangeModel, this._editor.onDidChangeModelLanguage)(this.endCallHierarchy, this, this._sessionDispose); + const widget = this._instantiationService.createInstance( + CallHierarchyTreePeekWidget, + this._editor, + position, + provider, + CallHierarchyDirection.CallsTo + ); + + widget.showLoading(); + this._ctxIsVisible.set(true); + + const cancel = new CancellationTokenSource(); + + this._sessionDispose.push(widget.onDidClose(() => this.endCallHierarchy())); + this._sessionDispose.push({ dispose() { cancel.cancel(); } }); + this._sessionDispose.push(widget); + + Promise.resolve(provider.provideCallHierarchyItem(model, position, cancel.token)).then(item => { + if (cancel.token.isCancellationRequested) { + return; + } + if (!item) { + widget.showMessage(localize('no.item', "No results")); + return; + } + + widget.showItem(item); + }); + } + + endCallHierarchy(): void { + this._sessionDispose = dispose(this._sessionDispose); + this._ctxIsVisible.set(false); + this._editor.focus(); + } +} + +registerEditorContribution(CallHierarchyController); + +registerEditorAction(class extends EditorAction { + + constructor() { + super({ + id: 'editor.showCallHierarchy', + label: localize('title', "Call Hierarchy"), + alias: 'Call Hierarchy', + menuOpts: { + group: 'navigation', + order: 111 + }, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H + }, + precondition: _ctxHasCompletionItemProvider + }); + } + + async run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + return CallHierarchyController.get(editor).startCallHierarchy(); + } +}); + + +registerEditorCommand(new class extends EditorCommand { + + constructor() { + super({ + id: 'editor.closeCallHierarchy', + kbOpts: { + weight: KeybindingWeight.WorkbenchContrib + 10, + primary: KeyCode.Escape + }, + precondition: ContextKeyExpr.and(_ctxCallHierarchyVisible, ContextKeyExpr.not('config.editor.stablePeek')) + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void { + return CallHierarchyController.get(editor).endCallHierarchy(); + } +}); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts new file mode 100644 index 0000000000000..28f4ad7cec9f5 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -0,0 +1,425 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/callHierarchy'; +import { PeekViewWidget } from 'vs/editor/contrib/referenceSearch/peekViewWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CallHierarchyItem, CallHierarchyProvider, CallHierarchyDirection } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { FuzzyScore } from 'vs/base/common/filters'; +import * as callHTree from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyTree'; +import { IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { localize } from 'vs/nls'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { SplitView, Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { Dimension, addClass } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions, OverviewRulerLane } from 'vs/editor/common/model'; +import { registerThemingParticipant, themeColorFromId, IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; +import * as referencesWidget from 'vs/editor/contrib/referenceSearch/referencesWidget'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IPosition } from 'vs/editor/common/core/position'; +import { Action } from 'vs/base/common/actions'; +import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { Color } from 'vs/base/common/color'; + +const enum State { + Loading = 'loading', + Message = 'message', + Data = 'data' +} + +class ToggleHierarchyDirectionAction extends Action { + + constructor(public direction: () => CallHierarchyDirection, callback: () => void) { + super('toggle.dir', undefined, 'call-hierarchy-toggle', true, () => { + callback(); + this._update(); + return Promise.resolve(); + }); + this._update(); + } + + private _update() { + if (this.direction() === CallHierarchyDirection.CallsFrom) { + this.label = localize('toggle.from', "Calls From..."); + this.checked = true; + } else { + this.label = localize('toggle.to', "Calls To..."); + this.checked = false; + } + } +} + +class LayoutInfo { + + static store(info: LayoutInfo, storageService: IStorageService): void { + storageService.store('callHierarchyPeekLayout', JSON.stringify(info), StorageScope.GLOBAL); + } + + static retrieve(storageService: IStorageService): LayoutInfo { + const value = storageService.get('callHierarchyPeekLayout', StorageScope.GLOBAL, '{}'); + const defaultInfo: LayoutInfo = { ratio: 0.7, height: 17 }; + try { + return { ...defaultInfo, ...JSON.parse(value) }; + } catch { + return defaultInfo; + } + } + + constructor( + public ratio: number, + public height: number + ) { } +} + +export class CallHierarchyTreePeekWidget extends PeekViewWidget { + + private _toggleDirection: ToggleHierarchyDirectionAction; + private _parent: HTMLElement; + private _message: HTMLElement; + private _splitView: SplitView; + private _tree: WorkbenchAsyncDataTree; + private _editor: EmbeddedCodeEditorWidget; + private _dim: Dimension; + private _layoutInfo: LayoutInfo; + + constructor( + editor: ICodeEditor, + private readonly _where: IPosition, + private readonly _provider: CallHierarchyProvider, + private _direction: CallHierarchyDirection, + @IThemeService themeService: IThemeService, + @IEditorService private readonly _editorService: IEditorService, + @ITextModelService private readonly _textModelService: ITextModelService, + @ILabelService private readonly _labelService: ILabelService, + @IStorageService private readonly _storageService: IStorageService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }); + this.create(); + this._applyTheme(themeService.getTheme()); + themeService.onThemeChange(this._applyTheme, this, this._disposables); + } + + dispose(): void { + LayoutInfo.store(this._layoutInfo, this._storageService); + this._splitView.dispose(); + this._tree.dispose(); + this._editor.dispose(); + super.dispose(); + } + + private _applyTheme(theme: ITheme) { + const borderColor = theme.getColor(referencesWidget.peekViewBorder) || Color.transparent; + this.style({ + arrowColor: borderColor, + frameColor: borderColor, + headerBackgroundColor: theme.getColor(referencesWidget.peekViewTitleBackground) || Color.transparent, + primaryHeadingColor: theme.getColor(referencesWidget.peekViewTitleForeground), + secondaryHeadingColor: theme.getColor(referencesWidget.peekViewTitleInfoForeground) + }); + } + + protected _getActionBarOptions(): IActionBarOptions { + return { + orientation: ActionsOrientation.HORIZONTAL_REVERSE + }; + } + + protected _fillBody(parent: HTMLElement): void { + + this._layoutInfo = LayoutInfo.retrieve(this._storageService); + this._dim = { height: 0, width: 0 }; + + this._parent = parent; + addClass(parent, 'call-hierarchy'); + + const message = document.createElement('div'); + addClass(message, 'message'); + parent.appendChild(message); + this._message = message; + + const container = document.createElement('div'); + addClass(container, 'results'); + parent.appendChild(container); + + this._splitView = new SplitView(container, { orientation: Orientation.HORIZONTAL }); + + // editor stuff + const editorContainer = document.createElement('div'); + addClass(editorContainer, 'editor'); + container.appendChild(editorContainer); + let editorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false + }, + overviewRulerLanes: 2, + fixedOverflowWidgets: true, + minimap: { + enabled: false + } + }; + this._editor = this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + editorContainer, + editorOptions, + this.editor + ); + + // tree stuff + const treeContainer = document.createElement('div'); + addClass(treeContainer, 'tree'); + container.appendChild(treeContainer); + const options: IAsyncDataTreeOptions = { + identityProvider: new callHTree.IdentityProvider(), + ariaLabel: localize('tree.aria', "Call Hierarchy"), + expandOnlyOnTwistieClick: true, + }; + this._tree = this._instantiationService.createInstance( + WorkbenchAsyncDataTree, + treeContainer, + new callHTree.VirtualDelegate(), + [this._instantiationService.createInstance(callHTree.CallRenderer)], + new callHTree.SingleDirectionDataSource(this._provider, () => this._direction), + options + ); + + // split stuff + this._splitView.addView({ + onDidChange: Event.None, + element: editorContainer, + minimumSize: 200, + maximumSize: Number.MAX_VALUE, + layout: (width) => { + this._editor.layout({ height: this._dim.height, width }); + } + }, Sizing.Distribute); + + this._splitView.addView({ + onDidChange: Event.None, + element: treeContainer, + minimumSize: 100, + maximumSize: Number.MAX_VALUE, + layout: (width) => { + this._tree.layout(this._dim.height, width); + } + }, Sizing.Distribute); + + this._splitView.onDidSashChange(() => { + if (this._dim.width) { + this._layoutInfo.ratio = this._splitView.getViewSize(0) / this._dim.width; + } + }, undefined, this._disposables); + + // session state + let localDispose: IDisposable[] = []; + this._disposables.push({ dispose() { dispose(localDispose); } }); + + // update editor + this._tree.onDidChangeFocus(e => { + const [element] = e.elements; + if (element && isNonEmptyArray(element.locations)) { + + localDispose = dispose(localDispose); + + const options: IModelDecorationOptions = { + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'call-decoration', + overviewRuler: { + color: themeColorFromId(referencesWidget.peekViewEditorMatchHighlight), + position: OverviewRulerLane.Center + }, + }; + let decorations: IModelDeltaDecoration[] = []; + let fullRange: IRange | undefined; + for (const { range } of element.locations) { + decorations.push({ range, options }); + fullRange = !fullRange ? range : Range.plusRange(range, fullRange); + } + + this._textModelService.createModelReference(element.item.uri).then(value => { + this._editor.setModel(value.object.textEditorModel); + this._editor.revealRangeInCenter(fullRange!, ScrollType.Smooth); + this._editor.revealLine(element.item.range.startLineNumber, ScrollType.Smooth); + const ids = this._editor.deltaDecorations([], decorations); + localDispose.push({ dispose: () => this._editor.deltaDecorations(ids, []) }); + localDispose.push(value); + }); + } + }, undefined, this._disposables); + + this._editor.onMouseDown(e => { + const { event, target } = e; + if (event.detail !== 2) { + return; + } + const [focus] = this._tree.getFocus(); + if (!focus) { + return; + } + this.dispose(); + this._editorService.openEditor({ + resource: focus.item.uri, + options: { selection: target.range! } + }); + + }, undefined, this._disposables); + + this._tree.onMouseDblClick(e => { + if (e.element && isNonEmptyArray(e.element.locations)) { + this.dispose(); + this._editorService.openEditor({ + resource: e.element.item.uri, + options: { selection: e.element.locations[0].range } + }); + } + }, undefined, this._disposables); + + this._tree.onDidChangeSelection(e => { + const [element] = e.elements; + // don't close on click + if (element && !(e.browserEvent instanceof MouseEvent)) { + this.dispose(); + this._editorService.openEditor({ + resource: element.item.uri, + options: { selection: element.locations[0].range } + }); + } + }); + } + + showLoading(): void { + this._parent.dataset['state'] = State.Loading; + this.setTitle(localize('title.loading', "Loading...")); + this._show(); + } + + showMessage(message: string): void { + this._parent.dataset['state'] = State.Message; + this.setTitle(''); + this.setMetaTitle(''); + this._message.innerText = message; + this._show(); + } + + showItem(item: CallHierarchyItem) { + this._parent.dataset['state'] = State.Data; + + this._show(); + this._tree.setInput(item).then(() => { + + if (!this._tree.getFirstElementChild(item)) { + // + this.showMessage(this._direction === CallHierarchyDirection.CallsFrom + ? localize('empt.callsFrom', "No calls from '{0}'", item.name) + : localize('empt.callsTo', "No calls to '{0}'", item.name)); + + } else { + this._tree.domFocus(); + this._tree.focusFirst(); + this.setTitle( + item.name, + item.detail || this._labelService.getUriLabel(item.uri, { relative: true }), + ); + this.setMetaTitle(this._direction === CallHierarchyDirection.CallsFrom + ? localize('title.from', " – calls from '{0}'", item.name) + : localize('title.to', " – calls to '{0}'", item.name)); + } + }); + + if (!this._toggleDirection) { + this._toggleDirection = new ToggleHierarchyDirectionAction( + () => this._direction, + () => { + let newDirection = this._direction === CallHierarchyDirection.CallsFrom ? CallHierarchyDirection.CallsTo : CallHierarchyDirection.CallsFrom; + this._direction = newDirection; + this.showItem(item); + } + ); + this._actionbarWidget.push(this._toggleDirection, { label: false, icon: true }); + this._disposables.push(this._toggleDirection); + } + } + + private _show() { + if (!this._isShowing) { + this.editor.revealLineInCenterIfOutsideViewport(this._where.lineNumber, ScrollType.Smooth); + super.show(Range.fromPositions(this._where), this._layoutInfo.height); + } + } + + protected _onWidth(width: number) { + if (this._dim) { + this._doLayoutBody(this._dim.height, width); + } + } + + protected _doLayoutBody(height: number, width: number): void { + super._doLayoutBody(height, width); + this._dim = { height, width }; + this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height; + this._splitView.layout(width); + this._splitView.resizeView(0, width * this._layoutInfo.ratio); + } +} + +registerThemingParticipant((theme, collector) => { + const referenceHighlightColor = theme.getColor(referencesWidget.peekViewEditorMatchHighlight); + if (referenceHighlightColor) { + collector.addRule(`.monaco-editor .call-hierarchy .call-decoration { background-color: ${referenceHighlightColor}; }`); + } + const referenceHighlightBorder = theme.getColor(referencesWidget.peekViewEditorMatchHighlightBorder); + if (referenceHighlightBorder) { + collector.addRule(`.monaco-editor .call-hierarchy .call-decoration { border: 2px solid ${referenceHighlightBorder}; box-sizing: border-box; }`); + } + const resultsBackground = theme.getColor(referencesWidget.peekViewResultsBackground); + if (resultsBackground) { + collector.addRule(`.monaco-editor .call-hierarchy .tree { background-color: ${resultsBackground}; }`); + } + const resultsMatchForeground = theme.getColor(referencesWidget.peekViewResultsFileForeground); + if (resultsMatchForeground) { + collector.addRule(`.monaco-editor .call-hierarchy .tree { color: ${resultsMatchForeground}; }`); + } + const resultsSelectedBackground = theme.getColor(referencesWidget.peekViewResultsSelectionBackground); + if (resultsSelectedBackground) { + collector.addRule(`.monaco-editor .call-hierarchy .tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`); + } + const resultsSelectedForeground = theme.getColor(referencesWidget.peekViewResultsSelectionForeground); + if (resultsSelectedForeground) { + collector.addRule(`.monaco-editor .call-hierarchy .tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`); + } + const editorBackground = theme.getColor(referencesWidget.peekViewEditorBackground); + if (editorBackground) { + collector.addRule( + `.monaco-editor .call-hierarchy .editor .monaco-editor .monaco-editor-background,` + + `.monaco-editor .call-hierarchy .editor .monaco-editor .inputarea.ime-input {` + + ` background-color: ${editorBackground};` + + `}` + ); + } + const editorGutterBackground = theme.getColor(referencesWidget.peekViewEditorGutterBackground); + if (editorGutterBackground) { + collector.addRule( + `.monaco-editor .call-hierarchy .editor .monaco-editor .margin {` + + ` background-color: ${editorGutterBackground};` + + `}` + ); + } +}); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts new file mode 100644 index 0000000000000..1c7a043ab6305 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAsyncDataSource, ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { CallHierarchyItem, CallHierarchyDirection, CallHierarchyProvider } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { symbolKindToCssClass, Location } from 'vs/editor/common/modes'; +import { ILabelService } from 'vs/platform/label/common/label'; + +export class Call { + constructor( + readonly direction: CallHierarchyDirection, + readonly item: CallHierarchyItem, + readonly locations: Location[] + ) { } +} + +export class SingleDirectionDataSource implements IAsyncDataSource { + + constructor( + public provider: CallHierarchyProvider, + public direction: () => CallHierarchyDirection + ) { } + + hasChildren(_element: CallHierarchyItem): boolean { + return true; + } + + async getChildren(element: CallHierarchyItem | Call): Promise { + if (element instanceof Call) { + element = element.item; + } + const direction = this.direction(); + const calls = await this.provider.resolveCallHierarchyItem(element, direction, CancellationToken.None); + return calls + ? calls.map(([item, locations]) => new Call(direction, item, locations)) + : []; + } +} + +export class IdentityProvider implements IIdentityProvider { + getId(element: Call): { toString(): string; } { + return element.item._id; + } +} + +class CallRenderingTemplate { + iconLabel: IconLabel; +} + +export class CallRenderer implements ITreeRenderer { + + static id = 'CallRenderer'; + + templateId: string = CallRenderer.id; + + constructor(@ILabelService private readonly _labelService: ILabelService) { } + + renderTemplate(container: HTMLElement): CallRenderingTemplate { + const iconLabel = new IconLabel(container, { supportHighlights: true }); + return { iconLabel }; + } + renderElement(node: ITreeNode, _index: number, template: CallRenderingTemplate): void { + const { element, filterData } = node; + const detail = element.item.detail || this._labelService.getUriLabel(element.item.uri, { relative: true }); + + template.iconLabel.setLabel( + element.item.name, + detail, + { + labelEscapeNewLines: true, + matches: createMatches(filterData), + extraClasses: [symbolKindToCssClass(element.item.kind, true)] + } + ); + } + disposeTemplate(template: CallRenderingTemplate): void { + template.iconLabel.dispose(); + } +} + +export class VirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: Call): number { + return 22; + } + + getTemplateId(_element: Call): string { + return CallRenderer.id; + } +} diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x.svg new file mode 100644 index 0000000000000..7ae4f3a56e911 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x_dark.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x_dark.svg new file mode 100644 index 0000000000000..a6f9a99b55750 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/CallerOrCalleeView_16x_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css new file mode 100644 index 0000000000000..20491c29c1127 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .call-hierarchy .results, +.monaco-workbench .call-hierarchy .message { + display: none; +} + +.monaco-workbench .call-hierarchy[data-state="data"] .results { + display: inherit; +} + +.monaco-workbench .call-hierarchy[data-state="message"] .message { + display: inherit; + text-align: center; + padding-top: 3em; +} + +.monaco-workbench .call-hierarchy-toggle { + background-image: url(CallerOrCalleeView_16x.svg); + background-size: 14px 14px; + background-repeat: no-repeat; + background-position: left center; +} + +.vs-dark .monaco-workbench .call-hierarchy-toggle, +.hc-dark .monaco-workbench .call-hierarchy-toggle { + background-image: url(CallerOrCalleeView_16x_dark.svg); +} + +.monaco-workbench .call-hierarchy .monaco-split-view2.horizontal > .split-view-container > .split-view-view{ + /* this is a little bizare.. */ + height: unset; +} +.monaco-workbench .call-hierarchy .tree{ + height: 100%; +} diff --git a/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts b/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts new file mode 100644 index 0000000000000..958763746396d --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { SymbolKind, ProviderResult, Location } from 'vs/editor/common/modes'; +import { ITextModel } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { URI } from 'vs/base/common/uri'; + +export const enum CallHierarchyDirection { + CallsFrom = 1, + CallsTo = 2 +} + +export interface CallHierarchyItem { + _id: number; + kind: SymbolKind; + name: string; + detail?: string; + uri: URI; + range: IRange; + selectionRange: IRange; +} + +export interface CallHierarchyProvider { + + provideCallHierarchyItem( + document: ITextModel, + postion: IPosition, + token: CancellationToken + ): ProviderResult; + + resolveCallHierarchyItem( + item: CallHierarchyItem, + direction: CallHierarchyDirection, + token: CancellationToken + ): ProviderResult<[CallHierarchyItem, Location[]][]>; +} + +export const CallHierarchyProviderRegistry = new LanguageFeatureRegistry(); + diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 1e98a2c723404..93efd8ba05a73 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -309,6 +309,9 @@ import 'vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStar import 'vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay'; import 'vs/workbench/contrib/welcome/page/browser/welcomePage.contribution'; +// Call Hierarchy +import 'vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution'; + // Outline import 'vs/workbench/contrib/outline/browser/outline.contribution';