Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
7
import { CancelablePromise, createCancelablePromise } from '../../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { onUnexpectedError } from '../../../../../base/common/errors.js';
10
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
11
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
12
import './goToDefinitionAtPosition.css';
13
import { CodeEditorStateFlag, EditorState } from '../../../editorState/browser/editorState.js';
14
import { ICodeEditor, MouseTargetType } from '../../../../browser/editorBrowser.js';
15
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../browser/editorExtensions.js';
16
import { EditorOption } from '../../../../common/config/editorOptions.js';
17
import { Position } from '../../../../common/core/position.js';
18
import { IRange, Range } from '../../../../common/core/range.js';
19
import { IEditorContribution, IEditorDecorationsCollection } from '../../../../common/editorCommon.js';
20
import { IModelDeltaDecoration, ITextModel } from '../../../../common/model.js';
21
import { LocationLink } from '../../../../common/languages.js';
22
import { ILanguageService } from '../../../../common/languages/language.js';
23
import { ITextModelService } from '../../../../common/services/resolverService.js';
24
import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from './clickLinkGesture.js';
25
import { PeekContext } from '../../../peekView/browser/peekView.js';
26
import * as nls from '../../../../../nls.js';
27
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
28
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
29
import { DefinitionAction } from '../goToCommands.js';
30
import { getDefinitionsAtPosition } from '../goToSymbol.js';
31
import { IWordAtPosition } from '../../../../common/core/wordHelper.js';
32
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
33
import { ModelDecorationInjectedTextOptions } from '../../../../common/model/textModel.js';
34
35
export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution {
36
37
public static readonly ID = 'editor.contrib.gotodefinitionatposition';
38
static readonly MAX_SOURCE_PREVIEW_LINES = 8;
39
40
private readonly editor: ICodeEditor;
41
private readonly toUnhook = new DisposableStore();
42
private readonly toUnhookForKeyboard = new DisposableStore();
43
private readonly linkDecorations: IEditorDecorationsCollection;
44
private currentWordAtPosition: IWordAtPosition | null = null;
45
private previousPromise: CancelablePromise<LocationLink[] | null> | null = null;
46
47
constructor(
48
editor: ICodeEditor,
49
@ITextModelService private readonly textModelResolverService: ITextModelService,
50
@ILanguageService private readonly languageService: ILanguageService,
51
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
52
) {
53
this.editor = editor;
54
this.linkDecorations = this.editor.createDecorationsCollection();
55
56
const linkGesture = new ClickLinkGesture(editor);
57
this.toUnhook.add(linkGesture);
58
59
this.toUnhook.add(linkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
60
this.startFindDefinitionFromMouse(mouseEvent, keyboardEvent ?? undefined);
61
}));
62
63
this.toUnhook.add(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => {
64
if (this.isEnabled(mouseEvent)) {
65
this.gotoDefinition(mouseEvent.target.position!, mouseEvent.hasSideBySideModifier)
66
.catch((error: Error) => {
67
onUnexpectedError(error);
68
})
69
.finally(() => {
70
this.removeLinkDecorations();
71
});
72
}
73
}));
74
75
this.toUnhook.add(linkGesture.onCancel(() => {
76
this.removeLinkDecorations();
77
this.currentWordAtPosition = null;
78
}));
79
}
80
81
static get(editor: ICodeEditor): GotoDefinitionAtPositionEditorContribution | null {
82
return editor.getContribution<GotoDefinitionAtPositionEditorContribution>(GotoDefinitionAtPositionEditorContribution.ID);
83
}
84
85
async startFindDefinitionFromCursor(position: Position) {
86
// For issue: https://github.com/microsoft/vscode/issues/46257
87
// equivalent to mouse move with meta/ctrl key
88
89
// First find the definition and add decorations
90
// to the editor to be shown with the content hover widget
91
await this.startFindDefinition(position);
92
// Add listeners for editor cursor move and key down events
93
// Dismiss the "extended" editor decorations when the user hides
94
// the hover widget. There is no event for the widget itself so these
95
// serve as a best effort. After removing the link decorations, the hover
96
// widget is clean and will only show declarations per next request.
97
this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(() => {
98
this.currentWordAtPosition = null;
99
this.removeLinkDecorations();
100
this.toUnhookForKeyboard.clear();
101
}));
102
this.toUnhookForKeyboard.add(this.editor.onKeyDown((e: IKeyboardEvent) => {
103
if (e) {
104
this.currentWordAtPosition = null;
105
this.removeLinkDecorations();
106
this.toUnhookForKeyboard.clear();
107
}
108
}));
109
}
110
111
private startFindDefinitionFromMouse(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void {
112
113
// check if we are active and on a content widget
114
if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && this.linkDecorations.length > 0) {
115
return;
116
}
117
118
if (!this.editor.hasModel() || !this.isEnabled(mouseEvent, withKey)) {
119
this.currentWordAtPosition = null;
120
this.removeLinkDecorations();
121
return;
122
}
123
124
const position = mouseEvent.target.position!;
125
126
this.startFindDefinition(position);
127
}
128
129
private async startFindDefinition(position: Position): Promise<void> {
130
131
// Dispose listeners for updating decorations when using keyboard to show definition hover
132
this.toUnhookForKeyboard.clear();
133
134
// Find word at mouse position
135
const word = position ? this.editor.getModel()?.getWordAtPosition(position) : null;
136
if (!word) {
137
this.currentWordAtPosition = null;
138
this.removeLinkDecorations();
139
return;
140
}
141
142
// Return early if word at position is still the same
143
if (this.currentWordAtPosition && this.currentWordAtPosition.startColumn === word.startColumn && this.currentWordAtPosition.endColumn === word.endColumn && this.currentWordAtPosition.word === word.word) {
144
return;
145
}
146
147
this.currentWordAtPosition = word;
148
149
// Find definition and decorate word if found
150
const state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll);
151
152
if (this.previousPromise) {
153
this.previousPromise.cancel();
154
this.previousPromise = null;
155
}
156
157
this.previousPromise = createCancelablePromise(token => this.findDefinition(position, token));
158
159
let results: LocationLink[] | null;
160
try {
161
results = await this.previousPromise;
162
163
} catch (error) {
164
onUnexpectedError(error);
165
return;
166
}
167
168
if (!results || !results.length || !state.validate(this.editor)) {
169
this.removeLinkDecorations();
170
return;
171
}
172
173
const linkRange = results[0].originSelectionRange
174
? Range.lift(results[0].originSelectionRange)
175
: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
176
177
// Multiple results
178
if (results.length > 1) {
179
180
let combinedRange = linkRange;
181
for (const { originSelectionRange } of results) {
182
if (originSelectionRange) {
183
combinedRange = Range.plusRange(combinedRange, originSelectionRange);
184
}
185
}
186
187
this.addDecoration(
188
combinedRange,
189
new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length))
190
);
191
} else {
192
// Single result
193
const result = results[0];
194
195
if (!result.uri) {
196
return;
197
}
198
199
return this.textModelResolverService.createModelReference(result.uri).then(ref => {
200
201
if (!ref.object || !ref.object.textEditorModel) {
202
ref.dispose();
203
return;
204
}
205
206
const { object: { textEditorModel } } = ref;
207
const { startLineNumber } = result.range;
208
209
if (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) {
210
// invalid range
211
ref.dispose();
212
return;
213
}
214
215
const previewValue = this.getPreviewValue(textEditorModel, startLineNumber, result);
216
const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(textEditorModel.uri);
217
this.addDecoration(
218
linkRange,
219
previewValue ? new MarkdownString().appendCodeblock(languageId ? languageId : '', previewValue) : undefined
220
);
221
ref.dispose();
222
});
223
}
224
}
225
226
private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number, result: LocationLink) {
227
let rangeToUse = result.range;
228
const numberOfLinesInRange = rangeToUse.endLineNumber - rangeToUse.startLineNumber;
229
if (numberOfLinesInRange >= GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES) {
230
rangeToUse = this.getPreviewRangeBasedOnIndentation(textEditorModel, startLineNumber);
231
}
232
rangeToUse = textEditorModel.validateRange(rangeToUse);
233
const previewValue = this.stripIndentationFromPreviewRange(textEditorModel, startLineNumber, rangeToUse);
234
return previewValue;
235
}
236
237
private stripIndentationFromPreviewRange(textEditorModel: ITextModel, startLineNumber: number, previewRange: IRange) {
238
const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);
239
let minIndent = startIndent;
240
241
for (let endLineNumber = startLineNumber + 1; endLineNumber < previewRange.endLineNumber; endLineNumber++) {
242
const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);
243
minIndent = Math.min(minIndent, endIndent);
244
}
245
246
const previewValue = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim();
247
return previewValue;
248
}
249
250
private getPreviewRangeBasedOnIndentation(textEditorModel: ITextModel, startLineNumber: number) {
251
const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);
252
const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES);
253
let endLineNumber = startLineNumber + 1;
254
255
for (; endLineNumber < maxLineNumber; endLineNumber++) {
256
const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);
257
258
if (startIndent === endIndent) {
259
break;
260
}
261
}
262
263
return new Range(startLineNumber, 1, endLineNumber + 1, 1);
264
}
265
266
private addDecoration(range: Range, hoverMessage: MarkdownString | undefined): void {
267
268
const newDecorations: IModelDeltaDecoration = {
269
range: range,
270
options: {
271
description: 'goto-definition-link',
272
inlineClassName: 'goto-definition-link',
273
hoverMessage
274
}
275
};
276
277
this.linkDecorations.set([newDecorations]);
278
}
279
280
private removeLinkDecorations(): void {
281
this.linkDecorations.clear();
282
}
283
284
private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): boolean {
285
return this.editor.hasModel()
286
&& mouseEvent.isLeftClick
287
&& mouseEvent.isNoneOrSingleMouseDown
288
&& mouseEvent.target.type === MouseTargetType.CONTENT_TEXT
289
&& !(mouseEvent.target.detail.injectedText?.options instanceof ModelDecorationInjectedTextOptions)
290
&& (mouseEvent.hasTriggerModifier || (withKey ? withKey.keyCodeIsTriggerKey : false))
291
&& this.languageFeaturesService.definitionProvider.has(this.editor.getModel());
292
}
293
294
private findDefinition(position: Position, token: CancellationToken): Promise<LocationLink[] | null> {
295
const model = this.editor.getModel();
296
if (!model) {
297
return Promise.resolve(null);
298
}
299
300
return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, false, token);
301
}
302
303
private gotoDefinition(position: Position, openToSide: boolean): Promise<any> {
304
this.editor.setPosition(position);
305
return this.editor.invokeWithinContext((accessor) => {
306
const canPeek = !openToSide && this.editor.getOption(EditorOption.definitionLinkOpensInPeek) && !this.isInPeekEditor(accessor);
307
const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined });
308
return action.run(accessor);
309
});
310
}
311
312
private isInPeekEditor(accessor: ServicesAccessor): boolean | undefined {
313
const contextKeyService = accessor.get(IContextKeyService);
314
return PeekContext.inPeekEditor.getValue(contextKeyService);
315
}
316
317
public dispose(): void {
318
this.toUnhook.dispose();
319
this.toUnhookForKeyboard.dispose();
320
}
321
}
322
323
registerEditorContribution(GotoDefinitionAtPositionEditorContribution.ID, GotoDefinitionAtPositionEditorContribution, EditorContributionInstantiation.BeforeFirstInteraction);
324
325