Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.ts
3296 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 { CancelablePromise, createCancelablePromise } from '../../../../../base/common/async.js';6import { onUnexpectedError } from '../../../../../base/common/errors.js';7import { KeyChord, KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';8import { DisposableStore } from '../../../../../base/common/lifecycle.js';9import { ICodeEditor } from '../../../../browser/editorBrowser.js';10import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';11import { EditorOption } from '../../../../common/config/editorOptions.js';12import { Position } from '../../../../common/core/position.js';13import { Range } from '../../../../common/core/range.js';14import { IEditorContribution } from '../../../../common/editorCommon.js';15import { Location } from '../../../../common/languages.js';16import { PeekContext } from '../../../peekView/browser/peekView.js';17import { getOuterEditor } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js';18import * as nls from '../../../../../nls.js';19import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';20import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';21import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';22import { TextEditorSelectionSource } from '../../../../../platform/editor/common/editor.js';23import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';24import { KeybindingsRegistry, KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';25import { IListService, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse, WorkbenchTreeElementCanExpand } from '../../../../../platform/list/browser/listService.js';26import { INotificationService } from '../../../../../platform/notification/common/notification.js';27import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';28import { OneReference, ReferencesModel } from '../referencesModel.js';29import { LayoutData, ReferenceWidget } from './referencesWidget.js';30import { EditorContextKeys } from '../../../../common/editorContextKeys.js';31import { InputFocusedContext } from '../../../../../platform/contextkey/common/contextkeys.js';3233export const ctxReferenceSearchVisible = new RawContextKey<boolean>('referenceSearchVisible', false, nls.localize('referenceSearchVisible', "Whether reference peek is visible, like 'Peek References' or 'Peek Definition'"));3435export abstract class ReferencesController implements IEditorContribution {3637static readonly ID = 'editor.contrib.referencesController';3839private readonly _disposables = new DisposableStore();4041private _widget?: ReferenceWidget;42private _model?: ReferencesModel;43private _peekMode?: boolean;44private _requestIdPool = 0;45private _ignoreModelChangeEvent = false;4647private readonly _referenceSearchVisible: IContextKey<boolean>;4849static get(editor: ICodeEditor): ReferencesController | null {50return editor.getContribution<ReferencesController>(ReferencesController.ID);51}5253constructor(54private readonly _defaultTreeKeyboardSupport: boolean,55private readonly _editor: ICodeEditor,56@IContextKeyService contextKeyService: IContextKeyService,57@ICodeEditorService private readonly _editorService: ICodeEditorService,58@INotificationService private readonly _notificationService: INotificationService,59@IInstantiationService private readonly _instantiationService: IInstantiationService,60@IStorageService private readonly _storageService: IStorageService,61@IConfigurationService private readonly _configurationService: IConfigurationService,62) {6364this._referenceSearchVisible = ctxReferenceSearchVisible.bindTo(contextKeyService);65}6667dispose(): void {68this._referenceSearchVisible.reset();69this._disposables.dispose();70this._widget?.dispose();71this._model?.dispose();72this._widget = undefined;73this._model = undefined;74}7576toggleWidget(range: Range, modelPromise: CancelablePromise<ReferencesModel>, peekMode: boolean): void {7778// close current widget and return early is position didn't change79let widgetPosition: Position | undefined;80if (this._widget) {81widgetPosition = this._widget.position;82}83this.closeWidget();84if (!!widgetPosition && range.containsPosition(widgetPosition)) {85return;86}8788this._peekMode = peekMode;89this._referenceSearchVisible.set(true);9091// close the widget on model/mode changes92this._disposables.add(this._editor.onDidChangeModelLanguage(() => { this.closeWidget(); }));93this._disposables.add(this._editor.onDidChangeModel(() => {94if (!this._ignoreModelChangeEvent) {95this.closeWidget();96}97}));98const storageKey = 'peekViewLayout';99const data = LayoutData.fromJSON(this._storageService.get(storageKey, StorageScope.PROFILE, '{}'));100this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data);101this._widget.setTitle(nls.localize('labelLoading', "Loading..."));102this._widget.show(range);103104this._disposables.add(this._widget.onDidClose(() => {105modelPromise.cancel();106if (this._widget) {107this._storageService.store(storageKey, JSON.stringify(this._widget.layoutData), StorageScope.PROFILE, StorageTarget.MACHINE);108if (!this._widget.isClosing) {109// to prevent calling this too many times, check whether it was already closing.110this.closeWidget();111}112this._widget = undefined;113} else {114this.closeWidget();115}116}));117118this._disposables.add(this._widget.onDidSelectReference(event => {119const { element, kind } = event;120if (!element) {121return;122}123switch (kind) {124case 'open':125if (event.source !== 'editor' || !this._configurationService.getValue('editor.stablePeek')) {126// when stable peek is configured we don't close127// the peek window on selecting the editor128this.openReference(element, false, false);129}130break;131case 'side':132this.openReference(element, true, false);133break;134case 'goto':135if (peekMode) {136this._gotoReference(element, true);137} else {138this.openReference(element, false, true);139}140break;141}142}));143144const requestId = ++this._requestIdPool;145146modelPromise.then(model => {147148// still current request? widget still open?149if (requestId !== this._requestIdPool || !this._widget) {150model.dispose();151return undefined;152}153154this._model?.dispose();155this._model = model;156157// show widget158return this._widget.setModel(this._model).then(() => {159if (this._widget && this._model && this._editor.hasModel()) { // might have been closed160161// set title162if (!this._model.isEmpty) {163this._widget.setMetaTitle(nls.localize('metaTitle.N', "{0} ({1})", this._model.title, this._model.references.length));164} else {165this._widget.setMetaTitle('');166}167168// set 'best' selection169const uri = this._editor.getModel().uri;170const pos = new Position(range.startLineNumber, range.startColumn);171const selection = this._model.nearestReference(uri, pos);172if (selection) {173return this._widget.setSelection(selection).then(() => {174if (this._widget && this._editor.getOption(EditorOption.peekWidgetDefaultFocus) === 'editor') {175this._widget.focusOnPreviewEditor();176}177});178}179}180return undefined;181});182183}, error => {184this._notificationService.error(error);185});186}187188changeFocusBetweenPreviewAndReferences() {189if (!this._widget) {190// can be called while still resolving...191return;192}193if (this._widget.isPreviewEditorFocused()) {194this._widget.focusOnReferenceTree();195} else {196this._widget.focusOnPreviewEditor();197}198}199200async goToNextOrPreviousReference(fwd: boolean) {201if (!this._editor.hasModel() || !this._model || !this._widget) {202// can be called while still resolving...203return;204}205const currentPosition = this._widget.position;206if (!currentPosition) {207return;208}209const source = this._model.nearestReference(this._editor.getModel().uri, currentPosition);210if (!source) {211return;212}213const target = this._model.nextOrPreviousReference(source, fwd);214const editorFocus = this._editor.hasTextFocus();215const previewEditorFocus = this._widget.isPreviewEditorFocused();216await this._widget.setSelection(target);217await this._gotoReference(target, false);218if (editorFocus) {219this._editor.focus();220} else if (this._widget && previewEditorFocus) {221this._widget.focusOnPreviewEditor();222}223}224225async revealReference(reference: OneReference): Promise<void> {226if (!this._editor.hasModel() || !this._model || !this._widget) {227// can be called while still resolving...228return;229}230231await this._widget.revealReference(reference);232}233234closeWidget(focusEditor = true): void {235this._widget?.dispose();236this._model?.dispose();237this._referenceSearchVisible.reset();238this._disposables.clear();239this._widget = undefined;240this._model = undefined;241if (focusEditor) {242this._editor.focus();243}244this._requestIdPool += 1; // Cancel pending requests245}246247private _gotoReference(ref: Location, pinned: boolean): Promise<any> {248this._widget?.hide();249250this._ignoreModelChangeEvent = true;251const range = Range.lift(ref.range).collapseToStart();252253return this._editorService.openCodeEditor({254resource: ref.uri,255options: { selection: range, selectionSource: TextEditorSelectionSource.JUMP, pinned }256}, this._editor).then(openedEditor => {257this._ignoreModelChangeEvent = false;258259if (!openedEditor || !this._widget) {260// something went wrong...261this.closeWidget();262return;263}264265if (this._editor === openedEditor) {266//267this._widget.show(range);268this._widget.focusOnReferenceTree();269270} else {271// we opened a different editor instance which means a different controller instance.272// therefore we stop with this controller and continue with the other273const other = ReferencesController.get(openedEditor);274const model = this._model!.clone();275276this.closeWidget();277openedEditor.focus();278279other?.toggleWidget(280range,281createCancelablePromise(_ => Promise.resolve(model)),282this._peekMode ?? false283);284}285286}, (err) => {287this._ignoreModelChangeEvent = false;288onUnexpectedError(err);289});290}291292openReference(ref: Location, sideBySide: boolean, pinned: boolean): void {293// clear stage294if (!sideBySide) {295this.closeWidget();296}297298const { uri, range } = ref;299this._editorService.openCodeEditor({300resource: uri,301options: { selection: range, selectionSource: TextEditorSelectionSource.JUMP, pinned }302}, this._editor, sideBySide);303}304}305306function withController(accessor: ServicesAccessor, fn: (controller: ReferencesController) => void): void {307const outerEditor = getOuterEditor(accessor);308if (!outerEditor) {309return;310}311const controller = ReferencesController.get(outerEditor);312if (controller) {313fn(controller);314}315}316317KeybindingsRegistry.registerCommandAndKeybindingRule({318id: 'togglePeekWidgetFocus',319weight: KeybindingWeight.EditorContrib,320primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.F2),321when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),322handler(accessor) {323withController(accessor, controller => {324controller.changeFocusBetweenPreviewAndReferences();325});326}327});328329KeybindingsRegistry.registerCommandAndKeybindingRule({330id: 'goToNextReference',331weight: KeybindingWeight.EditorContrib - 10,332primary: KeyCode.F4,333secondary: [KeyCode.F12],334when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),335handler(accessor) {336withController(accessor, controller => {337controller.goToNextOrPreviousReference(true);338});339}340});341342KeybindingsRegistry.registerCommandAndKeybindingRule({343id: 'goToPreviousReference',344weight: KeybindingWeight.EditorContrib - 10,345primary: KeyMod.Shift | KeyCode.F4,346secondary: [KeyMod.Shift | KeyCode.F12],347when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),348handler(accessor) {349withController(accessor, controller => {350controller.goToNextOrPreviousReference(false);351});352}353});354355// commands that aren't needed anymore because there is now ContextKeyExpr.OR356CommandsRegistry.registerCommandAlias('goToNextReferenceFromEmbeddedEditor', 'goToNextReference');357CommandsRegistry.registerCommandAlias('goToPreviousReferenceFromEmbeddedEditor', 'goToPreviousReference');358359// close360CommandsRegistry.registerCommandAlias('closeReferenceSearchEditor', 'closeReferenceSearch');361CommandsRegistry.registerCommand(362'closeReferenceSearch',363accessor => withController(accessor, controller => controller.closeWidget())364);365KeybindingsRegistry.registerKeybindingRule({366id: 'closeReferenceSearch',367weight: KeybindingWeight.EditorContrib - 101,368primary: KeyCode.Escape,369secondary: [KeyMod.Shift | KeyCode.Escape],370when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek'))371});372KeybindingsRegistry.registerKeybindingRule({373id: 'closeReferenceSearch',374weight: KeybindingWeight.WorkbenchContrib + 50,375primary: KeyCode.Escape,376secondary: [KeyMod.Shift | KeyCode.Escape],377when: ContextKeyExpr.and(378ctxReferenceSearchVisible,379ContextKeyExpr.not('config.editor.stablePeek'),380ContextKeyExpr.or(381EditorContextKeys.editorTextFocus,382InputFocusedContext.negate()383)384)385});386387388KeybindingsRegistry.registerCommandAndKeybindingRule({389id: 'revealReference',390weight: KeybindingWeight.WorkbenchContrib,391primary: KeyCode.Enter,392mac: {393primary: KeyCode.Enter,394secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow]395},396when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse.negate(), WorkbenchTreeElementCanExpand.negate()),397handler(accessor: ServicesAccessor) {398const listService = accessor.get(IListService);399const focus = <any[]>listService.lastFocusedList?.getFocus();400if (Array.isArray(focus) && focus[0] instanceof OneReference) {401withController(accessor, controller => controller.revealReference(focus[0]));402}403}404});405406KeybindingsRegistry.registerCommandAndKeybindingRule({407id: 'openReferenceToSide',408weight: KeybindingWeight.EditorContrib,409primary: KeyMod.CtrlCmd | KeyCode.Enter,410mac: {411primary: KeyMod.WinCtrl | KeyCode.Enter412},413when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse.negate(), WorkbenchTreeElementCanExpand.negate()),414handler(accessor: ServicesAccessor) {415const listService = accessor.get(IListService);416const focus = <any[]>listService.lastFocusedList?.getFocus();417if (Array.isArray(focus) && focus[0] instanceof OneReference) {418withController(accessor, controller => controller.openReference(focus[0], true, true));419}420}421});422423CommandsRegistry.registerCommand('openReference', (accessor) => {424const listService = accessor.get(IListService);425const focus = <any[]>listService.lastFocusedList?.getFocus();426if (Array.isArray(focus) && focus[0] instanceof OneReference) {427withController(accessor, controller => controller.openReference(focus[0], false, true));428}429});430431432