Path: blob/main/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/callHierarchy.css';6import * as peekView from '../../../../editor/contrib/peekView/browser/peekView.js';7import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';8import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';9import { CallHierarchyDirection, CallHierarchyModel } from '../common/callHierarchy.js';10import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from '../../../../platform/list/browser/listService.js';11import { FuzzyScore } from '../../../../base/common/filters.js';12import * as callHTree from './callHierarchyTree.js';13import { IAsyncDataTreeViewState } from '../../../../base/browser/ui/tree/asyncDataTree.js';14import { localize } from '../../../../nls.js';15import { ScrollType } from '../../../../editor/common/editorCommon.js';16import { IRange, Range } from '../../../../editor/common/core/range.js';17import { SplitView, Orientation, Sizing } from '../../../../base/browser/ui/splitview/splitview.js';18import { Dimension, isKeyboardEvent } from '../../../../base/browser/dom.js';19import { Event } from '../../../../base/common/event.js';20import { IEditorService } from '../../../services/editor/common/editorService.js';21import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';22import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js';23import { ITextModelService } from '../../../../editor/common/services/resolverService.js';24import { toDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';25import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions, OverviewRulerLane } from '../../../../editor/common/model.js';26import { themeColorFromId, IThemeService, IColorTheme } from '../../../../platform/theme/common/themeService.js';27import { IPosition } from '../../../../editor/common/core/position.js';28import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';29import { Color } from '../../../../base/common/color.js';30import { TreeMouseEventTarget, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';31import { URI } from '../../../../base/common/uri.js';32import { MenuId, IMenuService } from '../../../../platform/actions/common/actions.js';33import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';34import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';3536const enum State {37Loading = 'loading',38Message = 'message',39Data = 'data'40}4142class LayoutInfo {4344static store(info: LayoutInfo, storageService: IStorageService): void {45storageService.store('callHierarchyPeekLayout', JSON.stringify(info), StorageScope.PROFILE, StorageTarget.MACHINE);46}4748static retrieve(storageService: IStorageService): LayoutInfo {49const value = storageService.get('callHierarchyPeekLayout', StorageScope.PROFILE, '{}');50const defaultInfo: LayoutInfo = { ratio: 0.7, height: 17 };51try {52return { ...defaultInfo, ...JSON.parse(value) };53} catch {54return defaultInfo;55}56}5758constructor(59public ratio: number,60public height: number61) { }62}6364class CallHierarchyTree extends WorkbenchAsyncDataTree<CallHierarchyModel, callHTree.Call, FuzzyScore> { }6566export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget {6768static readonly TitleMenu = new MenuId('callhierarchy/title');6970private _parent!: HTMLElement;71private _message!: HTMLElement;72private _splitView!: SplitView;73private _tree!: CallHierarchyTree;74private _treeViewStates = new Map<CallHierarchyDirection, IAsyncDataTreeViewState>();75private _editor!: EmbeddedCodeEditorWidget;76private _dim!: Dimension;77private _layoutInfo!: LayoutInfo;7879private readonly _previewDisposable = new DisposableStore();8081constructor(82editor: ICodeEditor,83private readonly _where: IPosition,84private _direction: CallHierarchyDirection,85@IThemeService themeService: IThemeService,86@peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService,87@IEditorService private readonly _editorService: IEditorService,88@ITextModelService private readonly _textModelService: ITextModelService,89@IStorageService private readonly _storageService: IStorageService,90@IMenuService private readonly _menuService: IMenuService,91@IContextKeyService private readonly _contextKeyService: IContextKeyService,92@IInstantiationService private readonly _instantiationService: IInstantiationService,93) {94super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService);95this.create();96this._peekViewService.addExclusiveWidget(editor, this);97this._applyTheme(themeService.getColorTheme());98this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this));99this._disposables.add(this._previewDisposable);100}101102override dispose(): void {103LayoutInfo.store(this._layoutInfo, this._storageService);104this._splitView.dispose();105this._tree.dispose();106this._editor.dispose();107super.dispose();108}109110get direction(): CallHierarchyDirection {111return this._direction;112}113114private _applyTheme(theme: IColorTheme) {115const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent;116this.style({117arrowColor: borderColor,118frameColor: borderColor,119headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent,120primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground),121secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground)122});123}124125protected override _fillHead(container: HTMLElement): void {126super._fillHead(container, true);127128const menu = this._menuService.createMenu(CallHierarchyTreePeekWidget.TitleMenu, this._contextKeyService);129const updateToolbar = () => {130const actions = getFlatActionBarActions(menu.getActions());131this._actionbarWidget!.clear();132this._actionbarWidget!.push(actions, { label: false, icon: true });133};134this._disposables.add(menu);135this._disposables.add(menu.onDidChange(updateToolbar));136updateToolbar();137}138139protected _fillBody(parent: HTMLElement): void {140141this._layoutInfo = LayoutInfo.retrieve(this._storageService);142this._dim = new Dimension(0, 0);143144this._parent = parent;145parent.classList.add('call-hierarchy');146147const message = document.createElement('div');148message.classList.add('message');149parent.appendChild(message);150this._message = message;151this._message.tabIndex = 0;152153const container = document.createElement('div');154container.classList.add('results');155parent.appendChild(container);156157this._splitView = new SplitView(container, { orientation: Orientation.HORIZONTAL });158159// editor stuff160const editorContainer = document.createElement('div');161editorContainer.classList.add('editor');162container.appendChild(editorContainer);163const editorOptions: IEditorOptions = {164scrollBeyondLastLine: false,165scrollbar: {166verticalScrollbarSize: 14,167horizontal: 'auto',168useShadows: true,169verticalHasArrows: false,170horizontalHasArrows: false,171alwaysConsumeMouseWheel: false172},173overviewRulerLanes: 2,174fixedOverflowWidgets: true,175minimap: {176enabled: false177}178};179this._editor = this._instantiationService.createInstance(180EmbeddedCodeEditorWidget,181editorContainer,182editorOptions,183{},184this.editor185);186187// tree stuff188const treeContainer = document.createElement('div');189treeContainer.classList.add('tree');190container.appendChild(treeContainer);191const options: IWorkbenchAsyncDataTreeOptions<callHTree.Call, FuzzyScore> = {192sorter: new callHTree.Sorter(),193accessibilityProvider: new callHTree.AccessibilityProvider(() => this._direction),194identityProvider: new callHTree.IdentityProvider(() => this._direction),195expandOnlyOnTwistieClick: true,196overrideStyles: {197listBackground: peekView.peekViewResultsBackground198}199};200this._tree = this._instantiationService.createInstance(201CallHierarchyTree,202'CallHierarchyPeek',203treeContainer,204new callHTree.VirtualDelegate(),205[this._instantiationService.createInstance(callHTree.CallRenderer)],206this._instantiationService.createInstance(callHTree.DataSource, () => this._direction),207options208);209210// split stuff211this._splitView.addView({212onDidChange: Event.None,213element: editorContainer,214minimumSize: 200,215maximumSize: Number.MAX_VALUE,216layout: (width) => {217if (this._dim.height) {218this._editor.layout({ height: this._dim.height, width });219}220}221}, Sizing.Distribute);222223this._splitView.addView({224onDidChange: Event.None,225element: treeContainer,226minimumSize: 100,227maximumSize: Number.MAX_VALUE,228layout: (width) => {229if (this._dim.height) {230this._tree.layout(this._dim.height, width);231}232}233}, Sizing.Distribute);234235this._disposables.add(this._splitView.onDidSashChange(() => {236if (this._dim.width) {237this._layoutInfo.ratio = this._splitView.getViewSize(0) / this._dim.width;238}239}));240241// update editor242this._disposables.add(this._tree.onDidChangeFocus(this._updatePreview, this));243244this._disposables.add(this._editor.onMouseDown(e => {245const { event, target } = e;246if (event.detail !== 2) {247return;248}249const [focus] = this._tree.getFocus();250if (!focus) {251return;252}253this.dispose();254this._editorService.openEditor({255resource: focus.item.uri,256options: { selection: target.range! }257});258259}));260261this._disposables.add(this._tree.onMouseDblClick(e => {262if (e.target === TreeMouseEventTarget.Twistie) {263return;264}265266if (e.element) {267this.dispose();268this._editorService.openEditor({269resource: e.element.item.uri,270options: { selection: e.element.item.selectionRange, pinned: true }271});272}273}));274275this._disposables.add(this._tree.onDidChangeSelection(e => {276const [element] = e.elements;277// don't close on click278if (element && isKeyboardEvent(e.browserEvent)) {279this.dispose();280this._editorService.openEditor({281resource: element.item.uri,282options: { selection: element.item.selectionRange, pinned: true }283});284}285}));286}287288private async _updatePreview() {289const [element] = this._tree.getFocus();290if (!element) {291return;292}293294this._previewDisposable.clear();295296// update: editor and editor highlights297const options: IModelDecorationOptions = {298description: 'call-hierarchy-decoration',299stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,300className: 'call-decoration',301overviewRuler: {302color: themeColorFromId(peekView.peekViewEditorMatchHighlight),303position: OverviewRulerLane.Center304},305};306307let previewUri: URI;308if (this._direction === CallHierarchyDirection.CallsFrom) {309// outgoing calls: show caller and highlight focused calls310previewUri = element.parent ? element.parent.item.uri : element.model.root.uri;311312} else {313// incoming calls: show caller and highlight focused calls314previewUri = element.item.uri;315}316317const value = await this._textModelService.createModelReference(previewUri);318this._editor.setModel(value.object.textEditorModel);319320// set decorations for caller ranges (if in the same file)321const decorations: IModelDeltaDecoration[] = [];322let fullRange: IRange | undefined;323let locations = element.locations;324if (!locations) {325locations = [{ uri: element.item.uri, range: element.item.selectionRange }];326}327for (const loc of locations) {328if (loc.uri.toString() === previewUri.toString()) {329decorations.push({ range: loc.range, options });330fullRange = !fullRange ? loc.range : Range.plusRange(loc.range, fullRange);331}332}333if (fullRange) {334this._editor.revealRangeInCenter(fullRange, ScrollType.Immediate);335const decorationsCollection = this._editor.createDecorationsCollection(decorations);336this._previewDisposable.add(toDisposable(() => decorationsCollection.clear()));337}338this._previewDisposable.add(value);339340// update: title341const title = this._direction === CallHierarchyDirection.CallsFrom342? localize('callFrom', "Calls from '{0}'", element.model.root.name)343: localize('callsTo', "Callers of '{0}'", element.model.root.name);344this.setTitle(title);345}346347showLoading(): void {348this._parent.dataset['state'] = State.Loading;349this.setTitle(localize('title.loading', "Loading..."));350this._show();351}352353showMessage(message: string): void {354this._parent.dataset['state'] = State.Message;355this.setTitle('');356this.setMetaTitle('');357this._message.innerText = message;358this._show();359this._message.focus();360}361362async showModel(model: CallHierarchyModel): Promise<void> {363364this._show();365const viewState = this._treeViewStates.get(this._direction);366367await this._tree.setInput(model, viewState);368369const root = <ITreeNode<callHTree.Call, FuzzyScore>>this._tree.getNode(model).children[0];370await this._tree.expand(root.element);371372if (root.children.length === 0) {373//374this.showMessage(this._direction === CallHierarchyDirection.CallsFrom375? localize('empt.callsFrom', "No calls from '{0}'", model.root.name)376: localize('empt.callsTo', "No callers of '{0}'", model.root.name));377378} else {379this._parent.dataset['state'] = State.Data;380if (!viewState || this._tree.getFocus().length === 0) {381this._tree.setFocus([root.children[0].element]);382}383this._tree.domFocus();384this._updatePreview();385}386}387388getModel(): CallHierarchyModel | undefined {389return this._tree.getInput();390}391392getFocused(): callHTree.Call | undefined {393return this._tree.getFocus()[0];394}395396async updateDirection(newDirection: CallHierarchyDirection): Promise<void> {397const model = this._tree.getInput();398if (model && newDirection !== this._direction) {399this._treeViewStates.set(this._direction, this._tree.getViewState());400this._direction = newDirection;401await this.showModel(model);402}403}404405private _show() {406if (!this._isShowing) {407this.editor.revealLineInCenterIfOutsideViewport(this._where.lineNumber, ScrollType.Smooth);408super.show(Range.fromPositions(this._where), this._layoutInfo.height);409}410}411412protected override _onWidth(width: number) {413if (this._dim) {414this._doLayoutBody(this._dim.height, width);415}416}417418protected override _doLayoutBody(height: number, width: number): void {419if (this._dim.height !== height || this._dim.width !== width) {420super._doLayoutBody(height, width);421this._dim = new Dimension(width, height);422this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height;423this._splitView.layout(width);424this._splitView.resizeView(0, width * this._layoutInfo.ratio);425}426}427}428429430