Path: blob/main/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.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 { localize, localize2 } from '../../../../nls.js';6import { CallHierarchyProviderRegistry, CallHierarchyDirection, CallHierarchyModel } from '../common/callHierarchy.js';7import { CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';9import { CallHierarchyTreePeekWidget } from './callHierarchyPeek.js';10import { Event } from '../../../../base/common/event.js';11import { registerEditorContribution, EditorAction2, EditorContributionInstantiation } from '../../../../editor/browser/editorExtensions.js';12import { IEditorContribution } from '../../../../editor/common/editorCommon.js';13import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';14import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';15import { DisposableStore } from '../../../../base/common/lifecycle.js';16import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';17import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';18import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';19import { PeekContext } from '../../../../editor/contrib/peekView/browser/peekView.js';20import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';21import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';22import { Range } from '../../../../editor/common/core/range.js';23import { IPosition } from '../../../../editor/common/core/position.js';24import { MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';25import { Codicon } from '../../../../base/common/codicons.js';26import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';27import { isCancellationError } from '../../../../base/common/errors.js';2829const _ctxHasCallHierarchyProvider = new RawContextKey<boolean>('editorHasCallHierarchyProvider', false, localize('editorHasCallHierarchyProvider', 'Whether a call hierarchy provider is available'));30const _ctxCallHierarchyVisible = new RawContextKey<boolean>('callHierarchyVisible', false, localize('callHierarchyVisible', 'Whether call hierarchy peek is currently showing'));31const _ctxCallHierarchyDirection = new RawContextKey<string>('callHierarchyDirection', undefined, { type: 'string', description: localize('callHierarchyDirection', 'Whether call hierarchy shows incoming or outgoing calls') });3233function sanitizedDirection(candidate: string): CallHierarchyDirection {34return candidate === CallHierarchyDirection.CallsFrom || candidate === CallHierarchyDirection.CallsTo35? candidate36: CallHierarchyDirection.CallsTo;37}3839class CallHierarchyController implements IEditorContribution {4041static readonly Id = 'callHierarchy';4243static get(editor: ICodeEditor): CallHierarchyController | null {44return editor.getContribution<CallHierarchyController>(CallHierarchyController.Id);45}4647private static readonly _StorageDirection = 'callHierarchy/defaultDirection';4849private readonly _ctxHasProvider: IContextKey<boolean>;50private readonly _ctxIsVisible: IContextKey<boolean>;51private readonly _ctxDirection: IContextKey<string>;52private readonly _dispoables = new DisposableStore();53private readonly _sessionDisposables = new DisposableStore();5455private _widget?: CallHierarchyTreePeekWidget;5657constructor(58private readonly _editor: ICodeEditor,59@IContextKeyService private readonly _contextKeyService: IContextKeyService,60@IStorageService private readonly _storageService: IStorageService,61@ICodeEditorService private readonly _editorService: ICodeEditorService,62@IInstantiationService private readonly _instantiationService: IInstantiationService,63) {64this._ctxIsVisible = _ctxCallHierarchyVisible.bindTo(this._contextKeyService);65this._ctxHasProvider = _ctxHasCallHierarchyProvider.bindTo(this._contextKeyService);66this._ctxDirection = _ctxCallHierarchyDirection.bindTo(this._contextKeyService);67this._dispoables.add(Event.any<unknown>(_editor.onDidChangeModel, _editor.onDidChangeModelLanguage, CallHierarchyProviderRegistry.onDidChange)(() => {68this._ctxHasProvider.set(_editor.hasModel() && CallHierarchyProviderRegistry.has(_editor.getModel()));69}));70this._dispoables.add(this._sessionDisposables);71}7273dispose(): void {74this._ctxHasProvider.reset();75this._ctxIsVisible.reset();76this._dispoables.dispose();77}7879async startCallHierarchyFromEditor(): Promise<void> {80this._sessionDisposables.clear();8182if (!this._editor.hasModel()) {83return;84}8586const document = this._editor.getModel();87const position = this._editor.getPosition();88if (!CallHierarchyProviderRegistry.has(document)) {89return;90}9192const cts = new CancellationTokenSource();93const model = CallHierarchyModel.create(document, position, cts.token);94const direction = sanitizedDirection(this._storageService.get(CallHierarchyController._StorageDirection, StorageScope.PROFILE, CallHierarchyDirection.CallsTo));9596this._showCallHierarchyWidget(position, direction, model, cts);97}9899async startCallHierarchyFromCallHierarchy(): Promise<void> {100if (!this._widget) {101return;102}103const model = this._widget.getModel();104const call = this._widget.getFocused();105if (!call || !model) {106return;107}108const newEditor = await this._editorService.openCodeEditor({ resource: call.item.uri }, this._editor);109if (!newEditor) {110return;111}112const newModel = model.fork(call.item);113this._sessionDisposables.clear();114115CallHierarchyController.get(newEditor)?._showCallHierarchyWidget(116Range.lift(newModel.root.selectionRange).getStartPosition(),117this._widget.direction,118Promise.resolve(newModel),119new CancellationTokenSource()120);121}122123private _showCallHierarchyWidget(position: IPosition, direction: CallHierarchyDirection, model: Promise<CallHierarchyModel | undefined>, cts: CancellationTokenSource) {124125this._ctxIsVisible.set(true);126this._ctxDirection.set(direction);127Event.any<unknown>(this._editor.onDidChangeModel, this._editor.onDidChangeModelLanguage)(this.endCallHierarchy, this, this._sessionDisposables);128this._widget = this._instantiationService.createInstance(CallHierarchyTreePeekWidget, this._editor, position, direction);129this._widget.showLoading();130this._sessionDisposables.add(this._widget.onDidClose(() => {131this.endCallHierarchy();132this._storageService.store(CallHierarchyController._StorageDirection, this._widget!.direction, StorageScope.PROFILE, StorageTarget.USER);133}));134this._sessionDisposables.add({ dispose() { cts.dispose(true); } });135this._sessionDisposables.add(this._widget);136137model.then(model => {138if (cts.token.isCancellationRequested) {139return; // nothing140}141if (model) {142this._sessionDisposables.add(model);143this._widget!.showModel(model);144}145else {146this._widget!.showMessage(localize('no.item', "No results"));147}148}).catch(err => {149if (isCancellationError(err)) {150this.endCallHierarchy();151return;152}153this._widget!.showMessage(localize('error', "Failed to show call hierarchy"));154});155}156157showOutgoingCalls(): void {158this._widget?.updateDirection(CallHierarchyDirection.CallsFrom);159this._ctxDirection.set(CallHierarchyDirection.CallsFrom);160}161162showIncomingCalls(): void {163this._widget?.updateDirection(CallHierarchyDirection.CallsTo);164this._ctxDirection.set(CallHierarchyDirection.CallsTo);165}166167endCallHierarchy(): void {168this._sessionDisposables.clear();169this._ctxIsVisible.set(false);170this._editor.focus();171}172}173174registerEditorContribution(CallHierarchyController.Id, CallHierarchyController, EditorContributionInstantiation.Eager); // eager because it needs to define a context key175176registerAction2(class PeekCallHierarchyAction extends EditorAction2 {177178constructor() {179super({180id: 'editor.showCallHierarchy',181title: localize2('title', 'Peek Call Hierarchy'),182menu: {183id: MenuId.EditorContextPeek,184group: 'navigation',185order: 1000,186when: ContextKeyExpr.and(187_ctxHasCallHierarchyProvider,188PeekContext.notInPeekEditor,189EditorContextKeys.isInEmbeddedEditor.toNegated(),190),191},192keybinding: {193when: EditorContextKeys.editorTextFocus,194weight: KeybindingWeight.WorkbenchContrib,195primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KeyH196},197precondition: ContextKeyExpr.and(198_ctxHasCallHierarchyProvider,199PeekContext.notInPeekEditor200),201f1: true202});203}204205async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {206return CallHierarchyController.get(editor)?.startCallHierarchyFromEditor();207}208});209210registerAction2(class extends EditorAction2 {211212constructor() {213super({214id: 'editor.showIncomingCalls',215title: localize2('title.incoming', 'Show Incoming Calls'),216icon: registerIcon('callhierarchy-incoming', Codicon.callIncoming, localize('showIncomingCallsIcons', 'Icon for incoming calls in the call hierarchy view.')),217precondition: ContextKeyExpr.and(_ctxCallHierarchyVisible, _ctxCallHierarchyDirection.isEqualTo(CallHierarchyDirection.CallsFrom)),218keybinding: {219weight: KeybindingWeight.WorkbenchContrib,220primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KeyH,221},222menu: {223id: CallHierarchyTreePeekWidget.TitleMenu,224when: _ctxCallHierarchyDirection.isEqualTo(CallHierarchyDirection.CallsFrom),225order: 1,226}227});228}229230runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {231return CallHierarchyController.get(editor)?.showIncomingCalls();232}233});234235registerAction2(class extends EditorAction2 {236237constructor() {238super({239id: 'editor.showOutgoingCalls',240title: localize2('title.outgoing', 'Show Outgoing Calls'),241icon: registerIcon('callhierarchy-outgoing', Codicon.callOutgoing, localize('showOutgoingCallsIcon', 'Icon for outgoing calls in the call hierarchy view.')),242precondition: ContextKeyExpr.and(_ctxCallHierarchyVisible, _ctxCallHierarchyDirection.isEqualTo(CallHierarchyDirection.CallsTo)),243keybinding: {244weight: KeybindingWeight.WorkbenchContrib,245primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KeyH,246},247menu: {248id: CallHierarchyTreePeekWidget.TitleMenu,249when: _ctxCallHierarchyDirection.isEqualTo(CallHierarchyDirection.CallsTo),250order: 1251}252});253}254255runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {256return CallHierarchyController.get(editor)?.showOutgoingCalls();257}258});259260261registerAction2(class extends EditorAction2 {262263constructor() {264super({265id: 'editor.refocusCallHierarchy',266title: localize2('title.refocus', 'Refocus Call Hierarchy'),267precondition: _ctxCallHierarchyVisible,268keybinding: {269weight: KeybindingWeight.WorkbenchContrib,270primary: KeyMod.Shift + KeyCode.Enter271}272});273}274275async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {276return CallHierarchyController.get(editor)?.startCallHierarchyFromCallHierarchy();277}278});279280281registerAction2(class extends EditorAction2 {282283constructor() {284super({285id: 'editor.closeCallHierarchy',286title: localize('close', 'Close'),287icon: Codicon.close,288precondition: _ctxCallHierarchyVisible,289keybinding: {290weight: KeybindingWeight.WorkbenchContrib + 10,291primary: KeyCode.Escape,292when: ContextKeyExpr.not('config.editor.stablePeek')293},294menu: {295id: CallHierarchyTreePeekWidget.TitleMenu,296order: 1000297}298});299}300301runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void {302return CallHierarchyController.get(editor)?.endCallHierarchy();303}304});305306307