Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.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 { Emitter, Event } from '../../../../base/common/event.js';6import { KeyCode } from '../../../../base/common/keyCodes.js';7import { combinedDisposable, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js';8import { isEqual } from '../../../../base/common/resources.js';9import { ICodeEditor } from '../../../browser/editorBrowser.js';10import { EditorCommand, registerEditorCommand } from '../../../browser/editorExtensions.js';11import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';12import { Range } from '../../../common/core/range.js';13import { OneReference, ReferencesModel } from './referencesModel.js';14import { localize } from '../../../../nls.js';15import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';16import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';17import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';18import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';19import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';20import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';21import { INotificationService, IStatusHandle } from '../../../../platform/notification/common/notification.js';2223export const ctxHasSymbols = new RawContextKey('hasSymbols', false, localize('hasSymbols', "Whether there are symbol locations that can be navigated via keyboard-only."));2425export const ISymbolNavigationService = createDecorator<ISymbolNavigationService>('ISymbolNavigationService');2627export interface ISymbolNavigationService {28readonly _serviceBrand: undefined;29reset(): void;30put(anchor: OneReference): void;31revealNext(source: ICodeEditor): Promise<any>;32}3334class SymbolNavigationService implements ISymbolNavigationService {3536declare readonly _serviceBrand: undefined;3738private readonly _ctxHasSymbols: IContextKey<boolean>;3940private _currentModel?: ReferencesModel = undefined;41private _currentIdx: number = -1;42private _currentState?: IDisposable;43private _currentMessage?: IStatusHandle;44private _ignoreEditorChange: boolean = false;4546constructor(47@IContextKeyService contextKeyService: IContextKeyService,48@ICodeEditorService private readonly _editorService: ICodeEditorService,49@INotificationService private readonly _notificationService: INotificationService,50@IKeybindingService private readonly _keybindingService: IKeybindingService,51) {52this._ctxHasSymbols = ctxHasSymbols.bindTo(contextKeyService);53}5455reset(): void {56this._ctxHasSymbols.reset();57this._currentState?.dispose();58this._currentMessage?.close();59this._currentModel = undefined;60this._currentIdx = -1;61}6263put(anchor: OneReference): void {64const refModel = anchor.parent.parent;6566if (refModel.references.length <= 1) {67this.reset();68return;69}7071this._currentModel = refModel;72this._currentIdx = refModel.references.indexOf(anchor);73this._ctxHasSymbols.set(true);74this._showMessage();7576const editorState = new EditorState(this._editorService);77const listener = editorState.onDidChange(_ => {7879if (this._ignoreEditorChange) {80return;81}8283const editor = this._editorService.getActiveCodeEditor();84if (!editor) {85return;86}87const model = editor.getModel();88const position = editor.getPosition();89if (!model || !position) {90return;91}9293let seenUri: boolean = false;94let seenPosition: boolean = false;95for (const reference of refModel.references) {96if (isEqual(reference.uri, model.uri)) {97seenUri = true;98seenPosition = seenPosition || Range.containsPosition(reference.range, position);99} else if (seenUri) {100break;101}102}103if (!seenUri || !seenPosition) {104this.reset();105}106});107108this._currentState = combinedDisposable(editorState, listener);109}110111revealNext(source: ICodeEditor): Promise<any> {112if (!this._currentModel) {113return Promise.resolve();114}115116// get next result and advance117this._currentIdx += 1;118this._currentIdx %= this._currentModel.references.length;119const reference = this._currentModel.references[this._currentIdx];120121// status122this._showMessage();123124// open editor, ignore events while that happens125this._ignoreEditorChange = true;126return this._editorService.openCodeEditor({127resource: reference.uri,128options: {129selection: Range.collapseToStart(reference.range),130selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport131}132}, source).finally(() => {133this._ignoreEditorChange = false;134});135136}137138private _showMessage(): void {139140this._currentMessage?.close();141142const kb = this._keybindingService.lookupKeybinding('editor.gotoNextSymbolFromResult');143const message = kb144? localize('location.kb', "Symbol {0} of {1}, {2} for next", this._currentIdx + 1, this._currentModel!.references.length, kb.getLabel())145: localize('location', "Symbol {0} of {1}", this._currentIdx + 1, this._currentModel!.references.length);146147this._currentMessage = this._notificationService.status(message);148}149}150151registerSingleton(ISymbolNavigationService, SymbolNavigationService, InstantiationType.Delayed);152153registerEditorCommand(new class extends EditorCommand {154155constructor() {156super({157id: 'editor.gotoNextSymbolFromResult',158precondition: ctxHasSymbols,159kbOpts: {160weight: KeybindingWeight.EditorContrib,161primary: KeyCode.F12162}163});164}165166runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {167return accessor.get(ISymbolNavigationService).revealNext(editor);168}169});170171KeybindingsRegistry.registerCommandAndKeybindingRule({172id: 'editor.gotoNextSymbolFromResult.cancel',173weight: KeybindingWeight.EditorContrib,174when: ctxHasSymbols,175primary: KeyCode.Escape,176handler(accessor) {177accessor.get(ISymbolNavigationService).reset();178}179});180181//182183class EditorState {184185private readonly _listener = new Map<ICodeEditor, IDisposable>();186private readonly _disposables = new DisposableStore();187188private readonly _onDidChange = new Emitter<{ editor: ICodeEditor }>();189readonly onDidChange: Event<{ editor: ICodeEditor }> = this._onDidChange.event;190191constructor(@ICodeEditorService editorService: ICodeEditorService) {192this._disposables.add(editorService.onCodeEditorRemove(this._onDidRemoveEditor, this));193this._disposables.add(editorService.onCodeEditorAdd(this._onDidAddEditor, this));194editorService.listCodeEditors().forEach(this._onDidAddEditor, this);195}196197dispose(): void {198this._disposables.dispose();199this._onDidChange.dispose();200dispose(this._listener.values());201}202203private _onDidAddEditor(editor: ICodeEditor): void {204this._listener.set(editor, combinedDisposable(205editor.onDidChangeCursorPosition(_ => this._onDidChange.fire({ editor })),206editor.onDidChangeModelContent(_ => this._onDidChange.fire({ editor })),207));208}209210private _onDidRemoveEditor(editor: ICodeEditor): void {211this._listener.get(editor)?.dispose();212this._listener.delete(editor);213}214}215216217