From 3a3b62c1cbc6243c19f2adda14722e0fe44cf085 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 15 Mar 2019 15:55:09 +0100 Subject: [PATCH] show calls as columns --- .../ui/breadcrumbs/breadcrumbsWidget.ts | 4 + src/vs/base/browser/ui/splitview/splitview.ts | 4 + .../mainThreadLanguageFeatures.ts | 7 +- .../browser/callHierarchy.contribution.ts | 45 ++-- .../callHierarchy/browser/callHierarchy.css | 31 +++ .../browser/callHierarchyList.ts | 241 ++++++++++++++++++ .../browser/callHierarchyPeek.ts | 122 ++++++++- .../browser/callHierarchyTree.ts | 13 +- 8 files changed, 431 insertions(+), 36 deletions(-) create mode 100644 src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.css create mode 100644 src/vs/workbench/contrib/callHierarchy/browser/callHierarchyList.ts diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index d6f63b99a62..ed13af7d141 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -17,6 +17,7 @@ export abstract class BreadcrumbsItem { dispose(): void { } abstract equals(other: BreadcrumbsItem): boolean; abstract render(container: HTMLElement): void; + layout(height: number | undefined): void { } } export class SimpleBreadcrumbsItem extends BreadcrumbsItem { @@ -140,6 +141,8 @@ export class BreadcrumbsWidget { this._domNode.style.width = `${dim.width}px`; this._domNode.style.height = `${dim.height}px`; disposables.push(this._updateScrollbar()); + + this._items.forEach(item => item.layout(this._dimension.height)); })); return combinedDisposable(disposables); } @@ -327,6 +330,7 @@ export class BreadcrumbsWidget { dom.clearNode(container); container.className = ''; item.render(container); + item.layout(this._dimension && this._dimension.height); container.tabIndex = -1; container.setAttribute('role', 'listitem'); dom.addClass(container, 'monaco-breadcrumb-item'); diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index b4142731b13..5f44fc9a363 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -117,6 +117,10 @@ export class SplitView extends Disposable { private _onDidSashReset = this._register(new Emitter()); readonly onDidSashReset = this._onDidSashReset.event; + get items(): ReadonlyArray { + return this.viewItems.map(item => item.view); + } + get length(): number { return this.viewItems.length; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index bf0ca33f5ba..c88aa4c2e46 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -480,7 +480,12 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return this._proxy.$provideCallHierarchyItem(handle, document.uri, position, token); }, resolveCallHierarchyItem: (item, direction, token) => { - return this._proxy.$resolveCallHierarchyItem(handle, item, direction, token); + return this._proxy.$resolveCallHierarchyItem(handle, item, direction, token).then(data => { + if (data) { + data.forEach(tuple => tuple[1] = tuple[1].map(l => MainThreadLanguageFeatures._reviveLocationDto(l))); + } + return data; + }); } }); } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index e56b846433a..cb91ee6a68e 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -9,10 +9,12 @@ 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 { CallHierarchyTreePeekWidget, CallHierarchyColumnPeekWidget } from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek'; import { Range } from 'vs/editor/common/core/range'; import { Event } from 'vs/base/common/event'; +const tree = false; + registerAction({ id: 'editor.showCallHierarchy', title: { @@ -20,7 +22,7 @@ registerAction({ original: 'Show Call Hierarchy' }, menu: { - menuId: MenuId.EditorContext + menuId: MenuId.CommandPalette }, handler: async function (accessor) { @@ -45,22 +47,29 @@ registerAction({ return; } - 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())); + if (tree) { + const widget = instaService.createInstance(CallHierarchyTreePeekWidget, 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); + } + }); - widget.onDidClose(() => { - console.log('DONE'); - listener.dispose(); - }); - - widget.tree.onDidOpen(e => { - const [element] = e.elements; - if (element) { - console.log(element); - } - }); + } else { + const widget = instaService.createInstance(CallHierarchyColumnPeekWidget, 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(); + }); + } } }); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.css b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.css new file mode 100644 index 00000000000..83e5c22d5df --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.css @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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-columns .split-view-view .column { + height: 100%; + padding: 0 2px; +} + +.monaco-workbench .call-hierarchy-columns .split-view-view:not(:last-child) .column { + border-right: solid 1px rgba(128, 128, 128, 0.527); +} + +.monaco-workbench .call-hierarchy-columns .monaco-list-row .monaco-icon-label.call { + background-color: rgba(162, 241, 193, 0.38); + font-weight: 400; +} + +.monaco-workbench .call-hierarchy-columns .monaco-list-row.focused.selected .monaco-icon-label.call::after{ + content: '▶'; /*todo@joh use octicon*/ + opacity: 0.5; +} + +.monaco-workbench .call-hierarchy-columns .monaco-list-row .monaco-icon-label.location { + margin-left: 12px; +} + +.monaco-workbench .call-hierarchy-columns .monaco-list-row:not(.focused):not(.selected) .monaco-icon-label.location { + opacity: 0.7; +} diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyList.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyList.ts new file mode 100644 index 00000000000..773cfda1384 --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyList.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CallHierarchyProvider, CallHierarchyDirection } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import * as callHierarchyTree from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyTree'; +import { Location, symbolKindToCssClass } from 'vs/editor/common/modes'; +import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IView, Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { Dimension, addClass } from 'vs/base/browser/dom'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export type ListElement = callHierarchyTree.Call | Location; + +class LocationTemplate { + label: IconLabel; +} + +export class LocationRenderer implements IListRenderer { + + static id = 'LocationRenderer'; + + templateId: string = LocationRenderer.id; + + constructor( + @ITextModelService private readonly _textModelService: ITextModelService, + ) { } + + renderTemplate(container: HTMLElement): LocationTemplate { + const label = new IconLabel(container, { supportHighlights: true }); + return { label }; + } + + renderElement(element: Location, _index: number, template: LocationTemplate): void { + this._textModelService.createModelReference(element.uri).then(reference => { + const model = reference.object.textEditorModel; + const text = model.getLineContent(element.range.startLineNumber); + const indent = model.getLineFirstNonWhitespaceColumn(element.range.startLineNumber) - 1; + + const prefix = String(element.range.startLineNumber); + const shift = (1 + indent /*left*/) - (prefix.length + 2 /*right*/); + + template.label.setLabel(`${prefix}: ${text.substr(indent)}`, undefined, { + matches: [{ start: element.range.startColumn - shift, end: element.range.endColumn - shift }], + extraClasses: ['location'] + }); + reference.dispose(); + }); + } + + disposeTemplate(template: LocationTemplate): void { + template.label.dispose(); + } +} + +class CallRenderingTemplate { + iconLabel: IconLabel; +} + +export class CallRenderer implements IListRenderer { + + static id = 'CallRenderer'; + + templateId: string = CallRenderer.id; + + renderTemplate(container: HTMLElement): CallRenderingTemplate { + const iconLabel = new IconLabel(container, { supportHighlights: true }); + return { iconLabel }; + } + + renderElement(element: callHierarchyTree.Call, _index: number, template: CallRenderingTemplate): void { + template.iconLabel.setLabel( + element.item.name, + element.item.detail, + { + labelEscapeNewLines: true, + extraClasses: ['call', symbolKindToCssClass(element.item.kind, true)] + } + ); + } + + disposeTemplate(template: CallRenderingTemplate): void { + template.iconLabel.dispose(); + } +} + +export class Delegate implements IListVirtualDelegate { + getHeight(element: ListElement): number { + return 23; + } + getTemplateId(element: ListElement): string { + if (element instanceof callHierarchyTree.Call) { + return CallRenderer.id; + } else { + return LocationRenderer.id; + } + } +} + + +export class CallColumn extends Disposable implements IView { + + private _list: WorkbenchList; + private _token: CancellationTokenSource; + + readonly element: HTMLElement = document.createElement('div'); + readonly minimumSize: number = 100; + readonly maximumSize: number = 1000; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + readonly index: number, + readonly root: callHierarchyTree.Call, + private readonly _provider: CallHierarchyProvider, + private readonly _direction: CallHierarchyDirection, + private readonly _emitter: Emitter<{ column: CallColumn, element: ListElement }>, + private readonly _getDim: () => Dimension, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + addClass(this.element, 'column'); + + this._list = this._register(>this._instantiationService.createInstance( + WorkbenchList, + this.element, + new Delegate(), + [ + new CallRenderer(), + this._instantiationService.createInstance(LocationRenderer) + ], + {} + )); + + this._list.onDidOpen(e => { + this._emitter.fire({ column: this, element: e.elements[0] }); + }); + + this._token = this._register(new CancellationTokenSource()); + + Promise.resolve(this._provider.resolveCallHierarchyItem(this.root.item, this._direction, this._token.token)).then(calls => { + if (calls) { + const input: ListElement[] = []; + for (const [item, locations] of calls) { + input.push(new callHierarchyTree.Call(this._direction, item, locations)); + input.push(...locations); + } + this._list.splice(0, this._list.length, input); + } else { + // show message + } + }).catch(err => { + console.error(err); + }); + } + + layout(size: number, orientation: Orientation): void { + const { height, width } = this._getDim(); + this._list.layout(height, Math.min(size, Math.ceil(width / 6))); + } + + focus(): void { + this._list.domFocus(); + this._list.focusFirst(); + } +} + + +export class LocationColumn extends Disposable implements IView { + + readonly element: HTMLElement = document.createElement('div'); + readonly minimumSize: number = 370; + readonly maximumSize: number = Number.MAX_VALUE; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private _editor: EmbeddedCodeEditorWidget; + + constructor( + private _location: Location, + private readonly _getDim: () => Dimension, + editor: ICodeEditor, + @ITextModelService private readonly _textModelService: ITextModelService, + ) { + super(); + addClass(this.element, 'column'); + + // todo@joh pretty random selection of options + let options: IEditorOptions = { + readOnly: true, + scrollBeyondLastLine: false, + lineNumbers: 'off', + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false + }, + overviewRulerLanes: 2, + fixedOverflowWidgets: true, + minimap: { + enabled: false + }, + codeLens: false, + glyphMargin: false, + }; + + this._editor = editor.invokeWithinContext(accessor => { + return this._register(accessor.get(IInstantiationService).createInstance(EmbeddedCodeEditorWidget, this.element, options, editor)); + }); + + this._textModelService.createModelReference(this._location.uri).then(reference => { + this._editor.setModel(reference.object.textEditorModel); + this._editor.revealRangeInCenter(this._location.range); + this._editor.setSelection(this._location.range); + this._register(reference); + }); + } + + layout(size: number) { + this._editor.layout({ height: this._getDim().height, width: size }); + } + + focus(): void { + this._editor.focus(); + } +} + diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 2450220afc9..ef9bcad6da4 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./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 callHierarchyTree from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyTree'; +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 } 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 { CallColumn, ListElement, LocationColumn } from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyList'; +import { Emitter } from 'vs/base/common/event'; +export class CallHierarchyTreePeekWidget extends PeekViewWidget { -export class CallHierarchyPeekWidget extends PeekViewWidget { - - private _tree: WorkbenchAsyncDataTree; + private _tree: WorkbenchAsyncDataTree; constructor( editor: ICodeEditor, @@ -33,8 +37,9 @@ export class CallHierarchyPeekWidget extends PeekViewWidget { protected _fillBody(container: HTMLElement): void { - const options: IAsyncDataTreeOptions = { - identityProvider: new callHierarchyTree.IdentityProvider(), + + const options: IAsyncDataTreeOptions = { + identityProvider: new callHTree.IdentityProvider(), ariaLabel: localize('tree.aria', "Call Hierarchy"), expandOnlyOnTwistieClick: true, }; @@ -42,14 +47,14 @@ export class CallHierarchyPeekWidget extends PeekViewWidget { this._tree = this._instantiationService.createInstance( WorkbenchAsyncDataTree, container, - new callHierarchyTree.VirtualDelegate(), - [new callHierarchyTree.CallRenderer()], - new callHierarchyTree.SingleDirectionDataSource(this._provider, this._direction), + new callHTree.VirtualDelegate(), + [new callHTree.CallRenderer()], + new callHTree.SingleDirectionDataSource(this._provider, this._direction), options ); } - get tree(): WorkbenchAsyncDataTree { + get tree(): WorkbenchAsyncDataTree { return this._tree; } @@ -67,3 +72,100 @@ export class CallHierarchyPeekWidget extends PeekViewWidget { this._tree.layout(height, width); } } + + +export class CallHierarchyColumnPeekWidget extends PeekViewWidget { + + private readonly _emitter = new Emitter<{ column: CallColumn, element: ListElement }>(); + private _splitView: SplitView; + private _dim: Dimension; + + + constructor( + editor: ICodeEditor, + private readonly _provider: CallHierarchyProvider, + private readonly _direction: CallHierarchyDirection, + private readonly _root: CallHierarchyItem, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }); + this.create(); + } + + protected _fillBody(container: HTMLElement): void { + addClass(container, 'call-hierarchy-columns'); + + this._splitView = new SplitView(container, { orientation: Orientation.HORIZONTAL }); + this._emitter.event(e => { + const { element, column } = e; + + // remove old + while (column.index + 1 < this._splitView.length) { + this._splitView.removeView(this._splitView.length - 1); + } + const getDim = () => this._dim || { height: undefined, width: undefined }; + + // add new + let newColumn: CallColumn | LocationColumn; + if (element instanceof callHTree.Call) { + newColumn = this._instantiationService.createInstance( + CallColumn, + column.index + 1, + element, + this._provider, + this._direction, + this._emitter, + getDim + ); + } else { + newColumn = this._instantiationService.createInstance( + LocationColumn, + element, + getDim, + this.editor + ); + } + + this._disposables.push(newColumn); + this._splitView.addView(newColumn, Sizing.Distribute); + + setTimeout(() => newColumn.focus()); + + let parts = this._splitView.items.map(column => column instanceof CallColumn ? column.root.item.name : undefined).filter(e => Boolean(e)); + this.setTitle(localize('title', "Call Hierarchy for '{0}'", parts.join(' > '))); + + }); + } + + show(where: IRange) { + this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth); + super.show(where, 16); + this.setTitle(localize('title', "Call Hierarchy for '{0}'", this._root.name)); + + // add root items... + const item = this._instantiationService.createInstance( + CallColumn, + 0, + new callHTree.Call(this._direction, this._root, []), + this._provider, + this._direction, + this._emitter, + () => this._dim || { height: undefined, width: undefined } + ); + this._disposables.push(item); + this._splitView.addView(item, item.minimumSize); + item.focus(); + } + + 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._splitView.layout(width); + } +} diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index d3577b021bd..aa844924bdc 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -5,19 +5,18 @@ 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 { symbolKindToCssClass, Location } from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; export class Call { constructor( readonly direction: CallHierarchyDirection, readonly item: CallHierarchyItem, - readonly ranges: IRange[] | undefined + readonly locations: Location[] | undefined ) { } } @@ -36,7 +35,7 @@ export class SingleDirectionDataSource implements IAsyncDataSource new Call(this.direction, item, locations.map(l => l.range))) + ? calls.map(([item, locations]) => new Call(this.direction, item, locations)) : []; } else { return [new Call(this.direction, element, undefined)]; @@ -67,13 +66,13 @@ export class CallRenderer implements ITreeRenderer, _index: number, template: CallRenderingTemplate): void { const { element, filterData } = node; let detail: string | undefined; - if (!element.ranges) { + if (!element.locations) { // root detail = element.item.detail; } else { - detail = element.ranges.length === 1 + detail = element.locations.length === 1 ? localize('label.1', "(1 usage)") - : localize('label.n', "({0} usages)", element.ranges.length); + : localize('label.n', "({0} usages)", element.locations.length); } template.iconLabel.setLabel( element.item.name, -- GitLab