From 4ce24321fb1bbac552b4170a0e03ba81eb5a8723 Mon Sep 17 00:00:00 2001 From: Yan Zhang Date: Tue, 17 Aug 2021 00:40:36 +0800 Subject: [PATCH] peek type hierarchy Signed-off-by: Yan Zhang --- .../browser/media/typeHierarchy.css | 37 ++ .../browser/typeHierarchy.contribution.ts | 270 +++++++++- .../browser/typeHierarchyPeek.ts | 467 ++++++++++++++++++ .../browser/typeHierarchyTree.ts | 157 ++++++ .../typeHierarchy/common/typeHierarchy.ts | 5 +- 5 files changed, 926 insertions(+), 10 deletions(-) create mode 100644 src/vs/workbench/contrib/typeHierarchy/browser/media/typeHierarchy.css create mode 100644 src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts create mode 100644 src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/media/typeHierarchy.css b/src/vs/workbench/contrib/typeHierarchy/browser/media/typeHierarchy.css new file mode 100644 index 0000000000000..3bf798683a7a1 --- /dev/null +++ b/src/vs/workbench/contrib/typeHierarchy/browser/media/typeHierarchy.css @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .type-hierarchy .results, +.monaco-workbench .type-hierarchy .message { + display: none; +} + +.monaco-workbench .type-hierarchy[data-state="data"] .results { + display: inherit; + height: 100%; +} + +.monaco-workbench .type-hierarchy[data-state="message"] .message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.monaco-workbench .type-hierarchy .editor, +.monaco-workbench .type-hierarchy .tree { + height: 100%; +} + +.monaco-workbench .type-hierarchy .tree .typehierarchy-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-workbench .type-hierarchy .tree .typehierarchy-element .monaco-icon-label { + padding-left: 4px; +} diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts index bbd519a11b252..a2f5c1b3902d1 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts @@ -3,17 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { Event } from 'vs/base/common/event'; +import { EditorAction2, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { PeekContext } from 'vs/editor/contrib/peekView/peekView'; import { localize } from 'vs/nls'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { TypeHierarchyProviderRegistry } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { TypeHierarchyTreePeekWidget } from 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek'; +import { TypeHierarchyDirection, TypeHierarchyModel, TypeHierarchyProviderRegistry } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; const _ctxHasTypeHierarchyProvider = new RawContextKey('editorHasTypeHierarchyProvider', false, localize('editorHasTypeHierarchyProvider', 'Whether a type hierarchy provider is available')); +const _ctxTypeHierarchyVisible = new RawContextKey('typeHierarchyVisible', false, localize('typeHierarchyVisible', 'Whether type hierarchy peek is currently showing')); +const _ctxTypeHierarchyDirection = new RawContextKey('typeHierarchyDirection', undefined, { type: 'string', description: localize('typeHierarchyDirection', 'whether type hierarchy shows super types or subtypes') }); + +function sanitizedDirection(candidate: string): TypeHierarchyDirection { + return candidate === TypeHierarchyDirection.Subtypes || candidate === TypeHierarchyDirection.Supertypes + ? candidate + : TypeHierarchyDirection.Subtypes; +} class TypeHierarchyController implements IEditorContribution { static readonly Id = 'typeHierarchy'; @@ -22,26 +43,257 @@ class TypeHierarchyController implements IEditorContribution { return editor.getContribution(TypeHierarchyController.Id); } + private static readonly _storageDirectionKey = 'typeHierarchy/defaultDirection'; + private readonly _ctxHasProvider: IContextKey; - private readonly _dispoables = new DisposableStore(); + private readonly _ctxIsVisible: IContextKey; + private readonly _ctxDirection: IContextKey; + private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); + private _widget?: TypeHierarchyTreePeekWidget; + constructor( readonly _editor: ICodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IStorageService private readonly _storageService: IStorageService, + @ICodeEditorService private readonly _editorService: ICodeEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._ctxHasProvider = _ctxHasTypeHierarchyProvider.bindTo(this._contextKeyService); - this._dispoables.add(Event.any(_editor.onDidChangeModel, _editor.onDidChangeModelLanguage, TypeHierarchyProviderRegistry.onDidChange)(() => { + this._ctxIsVisible = _ctxTypeHierarchyVisible.bindTo(this._contextKeyService); + this._ctxDirection = _ctxTypeHierarchyDirection.bindTo(this._contextKeyService); + this._disposables.add(Event.any(_editor.onDidChangeModel, _editor.onDidChangeModelLanguage, TypeHierarchyProviderRegistry.onDidChange)(() => { this._ctxHasProvider.set(_editor.hasModel() && TypeHierarchyProviderRegistry.has(_editor.getModel())); })); - this._dispoables.add(this._sessionDisposables); + this._disposables.add(this._sessionDisposables); } dispose(): void { - this._dispoables.dispose(); + this._disposables.dispose(); + } + + // Peek + async startTypeHierarchyFromEditor(): Promise { + this._sessionDisposables.clear(); + + if (!this._editor.hasModel()) { + return; + } + + const document = this._editor.getModel(); + const position = this._editor.getPosition(); + if (!TypeHierarchyProviderRegistry.has(document)) { + return; + } + + const cts = new CancellationTokenSource(); + const model = TypeHierarchyModel.create(document, position, cts.token); + const direction = sanitizedDirection(this._storageService.get(TypeHierarchyController._storageDirectionKey, StorageScope.GLOBAL, TypeHierarchyDirection.Subtypes)); + + this._showTypeHierarchyWidget(position, direction, model, cts); + } + + private _showTypeHierarchyWidget(position: Position, direction: TypeHierarchyDirection, model: Promise, cts: CancellationTokenSource) { + + this._ctxIsVisible.set(true); + this._ctxDirection.set(direction); + Event.any(this._editor.onDidChangeModel, this._editor.onDidChangeModelLanguage)(this.endTypeHierarchy, this, this._sessionDisposables); + this._widget = this._instantiationService.createInstance(TypeHierarchyTreePeekWidget, this._editor, position, direction); + this._widget.showLoading(); + this._sessionDisposables.add(this._widget.onDidClose(() => { + this.endTypeHierarchy(); + this._storageService.store(TypeHierarchyController._storageDirectionKey, this._widget!.direction, StorageScope.GLOBAL, StorageTarget.USER); + })); + this._sessionDisposables.add({ dispose() { cts.dispose(true); } }); + this._sessionDisposables.add(this._widget); + + model.then(model => { + if (cts.token.isCancellationRequested) { + return; // nothing + } + if (model) { + this._sessionDisposables.add(model); + this._widget!.showModel(model); + } + else { + this._widget!.showMessage(localize('no.item', "No results")); + } + }).catch(e => { + this._widget!.showMessage(localize('error', "Failed to show type hierarchy")); + console.error(e); + }); + } + + async startTypeHierarchyFromTypeHierarchy(): Promise { + if (!this._widget) { + return; + } + const model = this._widget.getModel(); + const typeItem = this._widget.getFocused(); + if (!typeItem || !model) { + return; + } + const newEditor = await this._editorService.openCodeEditor({ resource: typeItem.item.uri }, this._editor); + if (!newEditor) { + return; + } + const newModel = model.fork(typeItem.item); + this._sessionDisposables.clear(); + + TypeHierarchyController.get(newEditor)._showTypeHierarchyWidget( + Range.lift(newModel.root.selectionRange).getStartPosition(), + this._widget.direction, + Promise.resolve(newModel), + new CancellationTokenSource() + ); + } + + showSupertypes(): void { + this._widget?.updateDirection(TypeHierarchyDirection.Supertypes); + this._ctxDirection.set(TypeHierarchyDirection.Supertypes); + } + + showSubtypes(): void { + this._widget?.updateDirection(TypeHierarchyDirection.Subtypes); + this._ctxDirection.set(TypeHierarchyDirection.Subtypes); + } + + endTypeHierarchy(): void { + this._sessionDisposables.clear(); + this._ctxIsVisible.set(false); + this._editor.focus(); } } registerEditorContribution(TypeHierarchyController.Id, TypeHierarchyController); -// Testing +// Peek +registerAction2(class extends EditorAction2 { + + constructor() { + super({ + id: 'editor.showTypeHierarchy', + title: { value: localize('title', "Peek Type Hierarchy"), original: 'Peek Type Hierarchy' }, + menu: { + id: MenuId.EditorContextPeek, + group: 'navigation', + order: 1000, + when: ContextKeyExpr.and( + _ctxHasTypeHierarchyProvider, + PeekContext.notInPeekEditor + ), + }, + keybinding: { + when: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H // TODO: to specify a proper keybinding? + }, + precondition: ContextKeyExpr.and( + _ctxHasTypeHierarchyProvider, + PeekContext.notInPeekEditor + ) + }); + } + + async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + return TypeHierarchyController.get(editor).startTypeHierarchyFromEditor(); + } +}); + +// actions for peek widget +registerAction2(class extends EditorAction2 { + + constructor() { + super({ + id: 'editor.showSupertypes', + title: { value: localize('title.supertypes', "Show Supertypes"), original: 'Show Supertypes' }, + icon: Codicon.typeHierarchySuper, + precondition: ContextKeyExpr.and(_ctxTypeHierarchyVisible, _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Subtypes)), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H, + }, + menu: { + id: TypeHierarchyTreePeekWidget.TitleMenu, + when: _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Subtypes), + order: 1, + } + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { + return TypeHierarchyController.get(editor).showSupertypes(); + } +}); + +registerAction2(class extends EditorAction2 { + + constructor() { + super({ + id: 'editor.showSubtypes', + title: { value: localize('title.subtypes', "Show Subtypes"), original: 'Show Subtypes' }, + icon: Codicon.typeHierarchySub, + precondition: ContextKeyExpr.and(_ctxTypeHierarchyVisible, _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Supertypes)), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H, + }, + menu: { + id: TypeHierarchyTreePeekWidget.TitleMenu, + when: _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Supertypes), + order: 1, + } + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { + return TypeHierarchyController.get(editor).showSubtypes(); + } +}); + +registerAction2(class extends EditorAction2 { + + constructor() { + super({ + id: 'editor.refocusTypeHierarchy', + title: { value: localize('title.refocusTypeHierarchy', "Refocus Type Hierarchy"), original: 'Refocus Type Hierarchy' }, + precondition: _ctxTypeHierarchyVisible, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Shift + KeyCode.Enter + } + }); + } + + async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + return TypeHierarchyController.get(editor).startTypeHierarchyFromTypeHierarchy(); + } +}); + +registerAction2(class extends EditorAction2 { + + constructor() { + super({ + id: 'editor.closeTypeHierarchy', + title: localize('close', 'Close'), + icon: Codicon.close, + precondition: ContextKeyExpr.and( + _ctxTypeHierarchyVisible, + ContextKeyExpr.not('config.editor.stablePeek') + ), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 10, + primary: KeyCode.Escape + }, + menu: { + id: TypeHierarchyTreePeekWidget.TitleMenu, + order: 1000 + } + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void { + return TypeHierarchyController.get(editor).endTypeHierarchy(); + } +}); diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts new file mode 100644 index 0000000000000..f1f67b58e3f94 --- /dev/null +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -0,0 +1,467 @@ +/*--------------------------------------------------------------------------------------------- + * 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/typeHierarchy'; +import { Dimension } from 'vs/base/browser/dom'; +import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ITreeNode, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree'; +import { IAction } from 'vs/base/common/actions'; +import { Color } from 'vs/base/common/color'; +import { Event } from 'vs/base/common/event'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IModelDecorationOptions, TrackedRangeStickiness, IModelDeltaDecoration, OverviewRulerLane } from 'vs/editor/common/model'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import * as peekView from 'vs/editor/contrib/peekView/peekView'; +import { localize } from 'vs/nls'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IColorTheme, IThemeService, registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; +import * as typeHTree from 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree'; +import { TypeHierarchyDirection, TypeHierarchyModel } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +// Todo: copied from call hierarchy, to extract +const enum State { + Loading = 'loading', + Message = 'message', + Data = 'data' +} + +class LayoutInfo { + + static store(info: LayoutInfo, storageService: IStorageService): void { + storageService.store('typeHierarchyPeekLayout', JSON.stringify(info), StorageScope.GLOBAL, StorageTarget.MACHINE); + } + + static retrieve(storageService: IStorageService): LayoutInfo { + const value = storageService.get('typeHierarchyPeekLayout', 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 + ) { } +} + +class TypeHierarchyTree extends WorkbenchAsyncDataTree{ } + +export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { + + static readonly TitleMenu = new MenuId('typehierarchy/title'); + + private _parent!: HTMLElement; + private _message!: HTMLElement; + private _splitView!: SplitView; + private _tree!: TypeHierarchyTree; + private _treeViewStates = new Map(); + private _editor!: EmbeddedCodeEditorWidget; + private _dim!: Dimension; + private _layoutInfo!: LayoutInfo; + + private readonly _previewDisposable = new DisposableStore(); + + constructor( + editor: ICodeEditor, + private readonly _where: IPosition, + private _direction: TypeHierarchyDirection, + @IThemeService themeService: IThemeService, + @peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService, + @IEditorService private readonly _editorService: IEditorService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IStorageService private readonly _storageService: IStorageService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService); + this.create(); + this._peekViewService.addExclusiveWidget(editor, this); + this._applyTheme(themeService.getColorTheme()); + this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); + this._disposables.add(this._previewDisposable); + } + + override dispose(): void { + LayoutInfo.store(this._layoutInfo, this._storageService); + this._splitView.dispose(); + this._tree.dispose(); + this._editor.dispose(); + super.dispose(); + } + + get direction(): TypeHierarchyDirection { + return this._direction; + } + + private _applyTheme(theme: IColorTheme) { + const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent; + this.style({ + arrowColor: borderColor, + frameColor: borderColor, + headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent, + primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground), + secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground) + }); + } + + protected override _fillHead(container: HTMLElement): void { + super._fillHead(container, true); + + const menu = this._menuService.createMenu(TypeHierarchyTreePeekWidget.TitleMenu, this._contextKeyService); + const updateToolbar = () => { + const actions: IAction[] = []; + createAndFillInActionBarActions(menu, undefined, actions); + this._actionbarWidget!.clear(); + this._actionbarWidget!.push(actions, { label: false, icon: true }); + }; + this._disposables.add(menu); + this._disposables.add(menu.onDidChange(updateToolbar)); + updateToolbar(); + } + + protected _fillBody(parent: HTMLElement): void { + + this._layoutInfo = LayoutInfo.retrieve(this._storageService); + this._dim = new Dimension(0, 0); + + this._parent = parent; + parent.classList.add('type-hierarchy'); + + const message = document.createElement('div'); + message.classList.add('message'); + parent.appendChild(message); + this._message = message; + this._message.tabIndex = 0; + + const container = document.createElement('div'); + container.classList.add('results'); + parent.appendChild(container); + + this._splitView = new SplitView(container, { orientation: Orientation.HORIZONTAL }); + + // editor stuff + const editorContainer = document.createElement('div'); + editorContainer.classList.add('editor'); + container.appendChild(editorContainer); + let editorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: 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'); + treeContainer.classList.add('tree'); + container.appendChild(treeContainer); + const options: IWorkbenchAsyncDataTreeOptions = { + sorter: new typeHTree.Sorter(), + accessibilityProvider: new typeHTree.AccessibilityProvider(() => this._direction), + identityProvider: new typeHTree.IdentityProvider(() => this._direction), + expandOnlyOnTwistieClick: true, + overrideStyles: { + listBackground: peekView.peekViewResultsBackground + } + }; + this._tree = this._instantiationService.createInstance( + TypeHierarchyTree, + 'TypeHierarchyPeek', + treeContainer, + new typeHTree.VirtualDelegate(), + [this._instantiationService.createInstance(typeHTree.TypeRenderer)], + this._instantiationService.createInstance(typeHTree.DataSource, () => this._direction), + options + ); + + // split stuff + this._splitView.addView({ + onDidChange: Event.None, + element: editorContainer, + minimumSize: 200, + maximumSize: Number.MAX_VALUE, + layout: (width) => { + if (this._dim.height) { + 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) => { + if (this._dim.height) { + this._tree.layout(this._dim.height, width); + } + } + }, Sizing.Distribute); + + this._disposables.add(this._splitView.onDidSashChange(() => { + if (this._dim.width) { + this._layoutInfo.ratio = this._splitView.getViewSize(0) / this._dim.width; + } + })); + + // update editor + this._disposables.add(this._tree.onDidChangeFocus(this._updatePreview, this)); + + this._disposables.add(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! } + }); + + })); + + this._disposables.add(this._tree.onMouseDblClick(e => { + if (e.target === TreeMouseEventTarget.Twistie) { + return; + } + + if (e.element) { + this.dispose(); + this._editorService.openEditor({ + resource: e.element.item.uri, + options: { selection: e.element.item.selectionRange, pinned: true } + }); + } + })); + + this._disposables.add(this._tree.onDidChangeSelection(e => { + const [element] = e.elements; + // don't close on click + if (element && e.browserEvent instanceof KeyboardEvent) { + this.dispose(); + this._editorService.openEditor({ + resource: element.item.uri, + options: { selection: element.item.selectionRange, pinned: true } + }); + } + })); + } + + private async _updatePreview() { + const [element] = this._tree.getFocus(); + if (!element) { + return; + } + + this._previewDisposable.clear(); + + // update: editor and editor highlights + const options: IModelDecorationOptions = { + description: 'type-hierarchy-decoration', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'type-decoration', + overviewRuler: { + color: themeColorFromId(peekView.peekViewEditorMatchHighlight), + position: OverviewRulerLane.Center + }, + }; + + let previewUri: URI; + if (this._direction === TypeHierarchyDirection.Supertypes) { + // supertypes: show super types and highlight focused type + previewUri = element.parent ? element.parent.item.uri : element.model.root.uri; + } else { + // subtypes: show sub types and highlight focused type + previewUri = element.item.uri; + } + + const value = await this._textModelService.createModelReference(previewUri); + this._editor.setModel(value.object.textEditorModel); + + // set decorations for type ranges + let decorations: IModelDeltaDecoration[] = []; + let fullRange: IRange | undefined; + const loc = { uri: element.item.uri, range: element.item.selectionRange }; + if (loc.uri.toString() === previewUri.toString()) { + decorations.push({ range: loc.range, options }); + fullRange = !fullRange ? loc.range : Range.plusRange(loc.range, fullRange); + } + if (fullRange) { + this._editor.revealRangeInCenter(fullRange, ScrollType.Immediate); + const ids = this._editor.deltaDecorations([], decorations); + this._previewDisposable.add(toDisposable(() => this._editor.deltaDecorations(ids, []))); + } + this._previewDisposable.add(value); + + // update: title + const title = this._direction === TypeHierarchyDirection.Supertypes + ? localize('supertypes', "Supertypes of '{0}'", element.model.root.name) + : localize('subtypes', "Subtypes of '{0}'", element.model.root.name); + this.setTitle(title); + } + + 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(); + this._message.focus(); + } + + async showModel(model: TypeHierarchyModel): Promise { + + this._show(); + const viewState = this._treeViewStates.get(this._direction); + + await this._tree.setInput(model, viewState); + + const root = >this._tree.getNode(model).children[0]; + await this._tree.expand(root.element); + + if (root.children.length === 0) { + this.showMessage(this._direction === TypeHierarchyDirection.Supertypes + ? localize('empt.supertypes', "No supertypes of '{0}'", model.root.name) + : localize('empt.subtypes', "No subtypes of '{0}'", model.root.name)); + + } else { + this._parent.dataset['state'] = State.Data; + if (!viewState || this._tree.getFocus().length === 0) { + this._tree.setFocus([root.children[0].element]); + } + this._tree.domFocus(); + this._updatePreview(); + } + } + + getModel(): TypeHierarchyModel | undefined { + return this._tree.getInput(); + } + + getFocused(): typeHTree.Type | undefined { + return this._tree.getFocus()[0]; + } + + async updateDirection(newDirection: TypeHierarchyDirection): Promise { + const model = this._tree.getInput(); + if (model && newDirection !== this._direction) { + this._treeViewStates.set(this._direction, this._tree.getViewState()); + this._direction = newDirection; + await this.showModel(model); + } + } + + private _show() { + if (!this._isShowing) { + this.editor.revealLineInCenterIfOutsideViewport(this._where.lineNumber, ScrollType.Smooth); + super.show(Range.fromPositions(this._where), this._layoutInfo.height); + } + } + + protected override _onWidth(width: number) { + if (this._dim) { + this._doLayoutBody(this._dim.height, width); + } + } + + protected override _doLayoutBody(height: number, width: number): void { + if (this._dim.height !== height || this._dim.width !== width) { + super._doLayoutBody(height, width); + this._dim = new Dimension(width, height); + 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(peekView.peekViewEditorMatchHighlight); + if (referenceHighlightColor) { + collector.addRule(`.monaco-editor .type-hierarchy .type-decoration { background-color: ${referenceHighlightColor}; }`); + } + const referenceHighlightBorder = theme.getColor(peekView.peekViewEditorMatchHighlightBorder); + if (referenceHighlightBorder) { + collector.addRule(`.monaco-editor .type-hierarchy .type-decoration { border: 2px solid ${referenceHighlightBorder}; box-sizing: border-box; }`); + } + const resultsBackground = theme.getColor(peekView.peekViewResultsBackground); + if (resultsBackground) { + collector.addRule(`.monaco-editor .type-hierarchy .tree { background-color: ${resultsBackground}; }`); + } + const resultsMatchForeground = theme.getColor(peekView.peekViewResultsFileForeground); + if (resultsMatchForeground) { + collector.addRule(`.monaco-editor .type-hierarchy .tree { color: ${resultsMatchForeground}; }`); + } + const resultsSelectedBackground = theme.getColor(peekView.peekViewResultsSelectionBackground); + if (resultsSelectedBackground) { + collector.addRule(`.monaco-editor .type-hierarchy .tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`); + } + const resultsSelectedForeground = theme.getColor(peekView.peekViewResultsSelectionForeground); + if (resultsSelectedForeground) { + collector.addRule(`.monaco-editor .type-hierarchy .tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`); + } + const editorBackground = theme.getColor(peekView.peekViewEditorBackground); + if (editorBackground) { + collector.addRule( + `.monaco-editor .type-hierarchy .editor .monaco-editor .monaco-editor-background,` + + `.monaco-editor .type-hierarchy .editor .monaco-editor .inputarea.ime-input {` + + ` background-color: ${editorBackground};` + + `}` + ); + } + const editorGutterBackground = theme.getColor(peekView.peekViewEditorGutterBackground); + if (editorGutterBackground) { + collector.addRule( + `.monaco-editor .type-hierarchy .editor .monaco-editor .margin {` + + ` background-color: ${editorGutterBackground};` + + `}` + ); + } +}); diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts new file mode 100644 index 0000000000000..9c1dcff2f397b --- /dev/null +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * 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, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { TypeHierarchyDirection, TypeHierarchyItem, TypeHierarchyModel } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; +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 { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { compare } from 'vs/base/common/strings'; +import { Range } from 'vs/editor/common/core/range'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { localize } from 'vs/nls'; + +export class Type { + constructor( + readonly item: TypeHierarchyItem, + readonly model: TypeHierarchyModel, + readonly parent: Type | undefined + ) { } + + static compare(a: Type, b: Type): number { + let res = compare(a.item.uri.toString(), b.item.uri.toString()); + if (res === 0) { + res = Range.compareRangesUsingStarts(a.item.range, b.item.range); + } + return res; + } +} + +export class DataSource implements IAsyncDataSource { + + constructor( + public getDirection: () => TypeHierarchyDirection, + ) { } + + hasChildren(): boolean { + return true; + } + + async getChildren(element: TypeHierarchyModel | Type): Promise { + if (element instanceof TypeHierarchyModel) { + return element.roots.map(root => new Type(root, element, undefined)); + } + + const { model, item } = element; + + if (this.getDirection() === TypeHierarchyDirection.Supertypes) { + return (await model.provideSupertypes(item, CancellationToken.None)).map(item => { + return new Type( + item, + model, + element + ); + }); + } else { + return (await model.provideSubtypes(item, CancellationToken.None)).map(item => { + return new Type( + item, + model, + element + ); + }); + } + } +} + +export class Sorter implements ITreeSorter { + + compare(element: Type, otherElement: Type): number { + return Type.compare(element, otherElement); + } +} + +export class IdentityProvider implements IIdentityProvider { + + constructor( + public getDirection: () => TypeHierarchyDirection + ) { } + + getId(element: Type): { toString(): string; } { + let res = this.getDirection() + JSON.stringify(element.item.uri) + JSON.stringify(element.item.range); + if (element.parent) { + res += this.getId(element.parent); + } + return res; + } +} + +class TypeRenderingTemplate { + constructor( + readonly icon: HTMLDivElement, + readonly label: IconLabel + ) { } +} + +export class TypeRenderer implements ITreeRenderer { + + static readonly id = 'TypeRenderer'; + + templateId: string = TypeRenderer.id; + + renderTemplate(container: HTMLElement): TypeRenderingTemplate { + container.classList.add('typehierarchy-element'); + let icon = document.createElement('div'); + container.appendChild(icon); + const label = new IconLabel(container, { supportHighlights: true }); + return new TypeRenderingTemplate(icon, label); + } + + renderElement(node: ITreeNode, _index: number, template: TypeRenderingTemplate): void { + const { element, filterData } = node; + const deprecated = element.item.tags?.includes(SymbolTag.Deprecated); + template.icon.className = SymbolKinds.toCssClassName(element.item.kind, true); + template.label.setLabel( + element.item.name, + element.item.detail, + { labelEscapeNewLines: true, matches: createMatches(filterData), strikethrough: deprecated } + ); + } + disposeTemplate(template: TypeRenderingTemplate): void { + template.label.dispose(); + } +} + +export class VirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: Type): number { + return 22; + } + + getTemplateId(_element: Type): string { + return TypeRenderer.id; + } +} + +export class AccessibilityProvider implements IListAccessibilityProvider { + + constructor( + public getDirection: () => TypeHierarchyDirection + ) { } + + getWidgetAriaLabel(): string { + return localize('tree.aria', "Type Hierarchy"); + } + + getAriaLabel(element: Type): string | null { + if (this.getDirection() === TypeHierarchyDirection.Supertypes) { + return localize('supertypes', "supertypes of {0}", element.item.name); + } else { + return localize('subtypes', "subtypes of {0}", element.item.name); + } + } +} diff --git a/src/vs/workbench/contrib/typeHierarchy/common/typeHierarchy.ts b/src/vs/workbench/contrib/typeHierarchy/common/typeHierarchy.ts index d395256a84728..5790c67d40604 100644 --- a/src/vs/workbench/contrib/typeHierarchy/common/typeHierarchy.ts +++ b/src/vs/workbench/contrib/typeHierarchy/common/typeHierarchy.ts @@ -18,7 +18,10 @@ import { assertType } from 'vs/base/common/types'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; - +export const enum TypeHierarchyDirection { + Subtypes = 'subtypes', + Supertypes = 'supertypes' +} export interface TypeHierarchyItem { _sessionId: string;