From c9a8a1d08fd81ba9d5cbcc5b75d7e1b7a25cc8a4 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 14 Mar 2019 17:56:45 +0100 Subject: [PATCH] simple tree UX --- .../api/node/extHostLanguageFeatures.ts | 2 +- .../browser/callHierarchy.contribution.ts | 33 ++++-- .../browser/callHierarchyPeek.ts | 69 ++++++++++++ .../browser/callHierarchyTree.ts | 102 ++++++++++++++++++ 4 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts create mode 100644 src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index d4e784e37df..0e9b41017b0 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -991,7 +991,7 @@ class CallHierarchyAdapter { resolveCallHierarchyItem(item: callHierarchy.CallHierarchyItem, direction: callHierarchy.CallHierarchyDirection, token: CancellationToken): Promise<[callHierarchy.CallHierarchyItem, modes.Location[]][]> { return asPromise(() => this._provider.resolveCallHierarchyItem( - this._cache.get(item._id), + this._cache.get(item._id)!, direction as number, token) // todo@joh proper convert ).then(data => { if (!data) { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index d1110eef7d7..7099e314b73 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -8,6 +8,10 @@ import { localize } from 'vs/nls'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CallHierarchyProviderRegistry, CallHierarchyDirection } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CallHierarchyPeekWidget } from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek'; +import { Range } from 'vs/editor/common/core/range'; +import { Event } from 'vs/base/common/event'; registerAction({ id: 'editor.showCallHierarchy', @@ -19,8 +23,11 @@ registerAction({ menuId: MenuId.CommandPalette }, handler: async function (accessor) { - const editor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + const instaService = accessor.get(IInstantiationService); + const editorService = accessor.get(ICodeEditorService); + + const editor = editorService.getActiveCodeEditor(); if (!editor || !editor.hasModel()) { console.log('bad editor'); return; @@ -32,14 +39,28 @@ registerAction({ return; } - const data = await provider.provideCallHierarchyItem(editor.getModel(), editor.getPosition(), CancellationToken.None); - if (!data) { + const rootItem = await provider.provideCallHierarchyItem(editor.getModel(), editor.getPosition(), CancellationToken.None); + if (!rootItem) { console.log('no data'); return; } - const callsTo = await provider.resolveCallHierarchyItem(data, CallHierarchyDirection.CallsTo, CancellationToken.None); - console.log(data); - console.log(callsTo); + const widget = instaService.createInstance(CallHierarchyPeekWidget, editor, provider, CallHierarchyDirection.CallsTo, rootItem); + + const listener = Event.any(editor.onDidChangeModel, editor.onDidChangeModelLanguage)(_ => widget.dispose()); + + widget.show(Range.fromPositions(editor.getPosition())); + + widget.onDidClose(() => { + console.log('DONE'); + listener.dispose(); + }); + + widget.tree.onDidOpen(e => { + const [element] = e.elements; + if (element) { + console.log(element); + } + }); } }); 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 00000000000..2450220afc9 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 callHierarchyTree 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 } from 'vs/editor/common/core/range'; + + +export class CallHierarchyPeekWidget extends PeekViewWidget { + + private _tree: WorkbenchAsyncDataTree; + + constructor( + editor: ICodeEditor, + private readonly _provider: CallHierarchyProvider, + private readonly _direction: CallHierarchyDirection, + private readonly _item: CallHierarchyItem, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }); + this.create(); + } + + protected _fillBody(container: HTMLElement): void { + + const options: IAsyncDataTreeOptions = { + identityProvider: new callHierarchyTree.IdentityProvider(), + ariaLabel: localize('tree.aria', "Call Hierarchy"), + expandOnlyOnTwistieClick: true, + }; + + this._tree = this._instantiationService.createInstance( + WorkbenchAsyncDataTree, + container, + new callHierarchyTree.VirtualDelegate(), + [new callHierarchyTree.CallRenderer()], + new callHierarchyTree.SingleDirectionDataSource(this._provider, this._direction), + options + ); + } + + get tree(): WorkbenchAsyncDataTree { + return this._tree; + } + + show(where: IRange) { + this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth); + super.show(where, 12); + this.setTitle(localize('title', "Call Hierarchy for '{0}'", this._item.name)); + this._tree.setInput(this._item); + this._tree.domFocus(); + this._tree.focusFirst(); + } + + protected _doLayoutBody(height: number, width: number): void { + super._doLayoutBody(height, width); + this._tree.layout(height, width); + } +} 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 00000000000..d3577b021bd --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IRange } from 'vs/editor/common/core/range'; +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 } from 'vs/editor/common/modes'; +import { localize } from 'vs/nls'; + +export class Call { + constructor( + readonly direction: CallHierarchyDirection, + readonly item: CallHierarchyItem, + readonly ranges: IRange[] | undefined + ) { } +} + +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) { + const calls = await this.provider.resolveCallHierarchyItem(element.item, this.direction, CancellationToken.None); + return calls + ? calls.map(([item, locations]) => new Call(this.direction, item, locations.map(l => l.range))) + : []; + } else { + return [new Call(this.direction, element, undefined)]; + } + } +} + +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; + + 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; + let detail: string | undefined; + if (!element.ranges) { + // root + detail = element.item.detail; + } else { + detail = element.ranges.length === 1 + ? localize('label.1', "(1 usage)") + : localize('label.n', "({0} usages)", element.ranges.length); + } + 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; + } +}