Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.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 { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';6import { CancelablePromise, createCancelablePromise } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { onUnexpectedError } from '../../../../../base/common/errors.js';9import { MarkdownString } from '../../../../../base/common/htmlContent.js';10import { DisposableStore } from '../../../../../base/common/lifecycle.js';11import './goToDefinitionAtPosition.css';12import { CodeEditorStateFlag, EditorState } from '../../../editorState/browser/editorState.js';13import { ICodeEditor, MouseTargetType } from '../../../../browser/editorBrowser.js';14import { EditorContributionInstantiation, registerEditorContribution } from '../../../../browser/editorExtensions.js';15import { EditorOption } from '../../../../common/config/editorOptions.js';16import { Position } from '../../../../common/core/position.js';17import { IRange, Range } from '../../../../common/core/range.js';18import { IEditorContribution, IEditorDecorationsCollection } from '../../../../common/editorCommon.js';19import { IModelDeltaDecoration, ITextModel } from '../../../../common/model.js';20import { LocationLink } from '../../../../common/languages.js';21import { ILanguageService } from '../../../../common/languages/language.js';22import { ITextModelService } from '../../../../common/services/resolverService.js';23import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from './clickLinkGesture.js';24import { PeekContext } from '../../../peekView/browser/peekView.js';25import * as nls from '../../../../../nls.js';26import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';27import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';28import { DefinitionAction } from '../goToCommands.js';29import { getDefinitionsAtPosition } from '../goToSymbol.js';30import { IWordAtPosition } from '../../../../common/core/wordHelper.js';31import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';32import { ModelDecorationInjectedTextOptions } from '../../../../common/model/textModel.js';3334export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution {3536public static readonly ID = 'editor.contrib.gotodefinitionatposition';37static readonly MAX_SOURCE_PREVIEW_LINES = 8;3839private readonly editor: ICodeEditor;40private readonly toUnhook = new DisposableStore();41private readonly toUnhookForKeyboard = new DisposableStore();42private readonly linkDecorations: IEditorDecorationsCollection;43private currentWordAtPosition: IWordAtPosition | null = null;44private previousPromise: CancelablePromise<LocationLink[] | null> | null = null;4546constructor(47editor: ICodeEditor,48@ITextModelService private readonly textModelResolverService: ITextModelService,49@ILanguageService private readonly languageService: ILanguageService,50@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,51) {52this.editor = editor;53this.linkDecorations = this.editor.createDecorationsCollection();5455const linkGesture = new ClickLinkGesture(editor);56this.toUnhook.add(linkGesture);5758this.toUnhook.add(linkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {59this.startFindDefinitionFromMouse(mouseEvent, keyboardEvent ?? undefined);60}));6162this.toUnhook.add(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => {63if (this.isEnabled(mouseEvent)) {64this.gotoDefinition(mouseEvent.target.position!, mouseEvent.hasSideBySideModifier)65.catch((error: Error) => {66onUnexpectedError(error);67})68.finally(() => {69this.removeLinkDecorations();70});71}72}));7374this.toUnhook.add(linkGesture.onCancel(() => {75this.removeLinkDecorations();76this.currentWordAtPosition = null;77}));78}7980static get(editor: ICodeEditor): GotoDefinitionAtPositionEditorContribution | null {81return editor.getContribution<GotoDefinitionAtPositionEditorContribution>(GotoDefinitionAtPositionEditorContribution.ID);82}8384async startFindDefinitionFromCursor(position: Position) {85// For issue: https://github.com/microsoft/vscode/issues/4625786// equivalent to mouse move with meta/ctrl key8788// First find the definition and add decorations89// to the editor to be shown with the content hover widget90await this.startFindDefinition(position);91// Add listeners for editor cursor move and key down events92// Dismiss the "extended" editor decorations when the user hides93// the hover widget. There is no event for the widget itself so these94// serve as a best effort. After removing the link decorations, the hover95// widget is clean and will only show declarations per next request.96this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(() => {97this.currentWordAtPosition = null;98this.removeLinkDecorations();99this.toUnhookForKeyboard.clear();100}));101this.toUnhookForKeyboard.add(this.editor.onKeyDown((e: IKeyboardEvent) => {102if (e) {103this.currentWordAtPosition = null;104this.removeLinkDecorations();105this.toUnhookForKeyboard.clear();106}107}));108}109110private startFindDefinitionFromMouse(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void {111112// check if we are active and on a content widget113if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && this.linkDecorations.length > 0) {114return;115}116117if (!this.editor.hasModel() || !this.isEnabled(mouseEvent, withKey)) {118this.currentWordAtPosition = null;119this.removeLinkDecorations();120return;121}122123const position = mouseEvent.target.position!;124125this.startFindDefinition(position);126}127128private async startFindDefinition(position: Position): Promise<void> {129130// Dispose listeners for updating decorations when using keyboard to show definition hover131this.toUnhookForKeyboard.clear();132133// Find word at mouse position134const word = position ? this.editor.getModel()?.getWordAtPosition(position) : null;135if (!word) {136this.currentWordAtPosition = null;137this.removeLinkDecorations();138return;139}140141// Return early if word at position is still the same142if (this.currentWordAtPosition && this.currentWordAtPosition.startColumn === word.startColumn && this.currentWordAtPosition.endColumn === word.endColumn && this.currentWordAtPosition.word === word.word) {143return;144}145146this.currentWordAtPosition = word;147148// Find definition and decorate word if found149const state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll);150151if (this.previousPromise) {152this.previousPromise.cancel();153this.previousPromise = null;154}155156this.previousPromise = createCancelablePromise(token => this.findDefinition(position, token));157158let results: LocationLink[] | null;159try {160results = await this.previousPromise;161162} catch (error) {163onUnexpectedError(error);164return;165}166167if (!results || !results.length || !state.validate(this.editor)) {168this.removeLinkDecorations();169return;170}171172const linkRange = results[0].originSelectionRange173? Range.lift(results[0].originSelectionRange)174: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);175176// Multiple results177if (results.length > 1) {178179let combinedRange = linkRange;180for (const { originSelectionRange } of results) {181if (originSelectionRange) {182combinedRange = Range.plusRange(combinedRange, originSelectionRange);183}184}185186this.addDecoration(187combinedRange,188new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length))189);190} else {191// Single result192const result = results[0];193194if (!result.uri) {195return;196}197198return this.textModelResolverService.createModelReference(result.uri).then(ref => {199200if (!ref.object || !ref.object.textEditorModel) {201ref.dispose();202return;203}204205const { object: { textEditorModel } } = ref;206const { startLineNumber } = result.range;207208if (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) {209// invalid range210ref.dispose();211return;212}213214const previewValue = this.getPreviewValue(textEditorModel, startLineNumber, result);215const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(textEditorModel.uri);216this.addDecoration(217linkRange,218previewValue ? new MarkdownString().appendCodeblock(languageId ? languageId : '', previewValue) : undefined219);220ref.dispose();221});222}223}224225private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number, result: LocationLink) {226let rangeToUse = result.range;227const numberOfLinesInRange = rangeToUse.endLineNumber - rangeToUse.startLineNumber;228if (numberOfLinesInRange >= GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES) {229rangeToUse = this.getPreviewRangeBasedOnIndentation(textEditorModel, startLineNumber);230}231rangeToUse = textEditorModel.validateRange(rangeToUse);232const previewValue = this.stripIndentationFromPreviewRange(textEditorModel, startLineNumber, rangeToUse);233return previewValue;234}235236private stripIndentationFromPreviewRange(textEditorModel: ITextModel, startLineNumber: number, previewRange: IRange) {237const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);238let minIndent = startIndent;239240for (let endLineNumber = startLineNumber + 1; endLineNumber < previewRange.endLineNumber; endLineNumber++) {241const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);242minIndent = Math.min(minIndent, endIndent);243}244245const previewValue = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim();246return previewValue;247}248249private getPreviewRangeBasedOnIndentation(textEditorModel: ITextModel, startLineNumber: number) {250const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);251const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES);252let endLineNumber = startLineNumber + 1;253254for (; endLineNumber < maxLineNumber; endLineNumber++) {255const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);256257if (startIndent === endIndent) {258break;259}260}261262return new Range(startLineNumber, 1, endLineNumber + 1, 1);263}264265private addDecoration(range: Range, hoverMessage: MarkdownString | undefined): void {266267const newDecorations: IModelDeltaDecoration = {268range: range,269options: {270description: 'goto-definition-link',271inlineClassName: 'goto-definition-link',272hoverMessage273}274};275276this.linkDecorations.set([newDecorations]);277}278279private removeLinkDecorations(): void {280this.linkDecorations.clear();281}282283private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): boolean {284return this.editor.hasModel()285&& mouseEvent.isLeftClick286&& mouseEvent.isNoneOrSingleMouseDown287&& mouseEvent.target.type === MouseTargetType.CONTENT_TEXT288&& !(mouseEvent.target.detail.injectedText?.options instanceof ModelDecorationInjectedTextOptions)289&& (mouseEvent.hasTriggerModifier || (withKey ? withKey.keyCodeIsTriggerKey : false))290&& this.languageFeaturesService.definitionProvider.has(this.editor.getModel());291}292293private findDefinition(position: Position, token: CancellationToken): Promise<LocationLink[] | null> {294const model = this.editor.getModel();295if (!model) {296return Promise.resolve(null);297}298299return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, false, token);300}301302private gotoDefinition(position: Position, openToSide: boolean): Promise<any> {303this.editor.setPosition(position);304return this.editor.invokeWithinContext((accessor) => {305const canPeek = !openToSide && this.editor.getOption(EditorOption.definitionLinkOpensInPeek) && !this.isInPeekEditor(accessor);306const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined });307return action.run(accessor);308});309}310311private isInPeekEditor(accessor: ServicesAccessor): boolean | undefined {312const contextKeyService = accessor.get(IContextKeyService);313return PeekContext.inPeekEditor.getValue(contextKeyService);314}315316public dispose(): void {317this.toUnhook.dispose();318this.toUnhookForKeyboard.dispose();319}320}321322registerEditorContribution(GotoDefinitionAtPositionEditorContribution.ID, GotoDefinitionAtPositionEditorContribution, EditorContributionInstantiation.BeforeFirstInteraction);323324325