Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.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 { CancelablePromise, createCancelablePromise } from '../../../../../base/common/async.js';
7
import { onUnexpectedError } from '../../../../../base/common/errors.js';
8
import { KeyChord, KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
9
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
10
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
11
import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';
12
import { EditorOption } from '../../../../common/config/editorOptions.js';
13
import { Position } from '../../../../common/core/position.js';
14
import { Range } from '../../../../common/core/range.js';
15
import { IEditorContribution } from '../../../../common/editorCommon.js';
16
import { Location } from '../../../../common/languages.js';
17
import { PeekContext } from '../../../peekView/browser/peekView.js';
18
import { getOuterEditor } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js';
19
import * as nls from '../../../../../nls.js';
20
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
21
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
22
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
23
import { TextEditorSelectionSource } from '../../../../../platform/editor/common/editor.js';
24
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
25
import { KeybindingsRegistry, KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
26
import { IListService, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse, WorkbenchTreeElementCanExpand } from '../../../../../platform/list/browser/listService.js';
27
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
28
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
29
import { OneReference, ReferencesModel } from '../referencesModel.js';
30
import { LayoutData, ReferenceWidget } from './referencesWidget.js';
31
import { EditorContextKeys } from '../../../../common/editorContextKeys.js';
32
import { InputFocusedContext } from '../../../../../platform/contextkey/common/contextkeys.js';
33
34
export const ctxReferenceSearchVisible = new RawContextKey<boolean>('referenceSearchVisible', false, nls.localize('referenceSearchVisible', "Whether reference peek is visible, like 'Peek References' or 'Peek Definition'"));
35
36
export abstract class ReferencesController implements IEditorContribution {
37
38
static readonly ID = 'editor.contrib.referencesController';
39
40
private readonly _disposables = new DisposableStore();
41
42
private _widget?: ReferenceWidget;
43
private _model?: ReferencesModel;
44
private _peekMode?: boolean;
45
private _requestIdPool = 0;
46
private _ignoreModelChangeEvent = false;
47
48
private readonly _referenceSearchVisible: IContextKey<boolean>;
49
50
static get(editor: ICodeEditor): ReferencesController | null {
51
return editor.getContribution<ReferencesController>(ReferencesController.ID);
52
}
53
54
constructor(
55
private readonly _defaultTreeKeyboardSupport: boolean,
56
private readonly _editor: ICodeEditor,
57
@IContextKeyService contextKeyService: IContextKeyService,
58
@ICodeEditorService private readonly _editorService: ICodeEditorService,
59
@INotificationService private readonly _notificationService: INotificationService,
60
@IInstantiationService private readonly _instantiationService: IInstantiationService,
61
@IStorageService private readonly _storageService: IStorageService,
62
@IConfigurationService private readonly _configurationService: IConfigurationService,
63
) {
64
65
this._referenceSearchVisible = ctxReferenceSearchVisible.bindTo(contextKeyService);
66
}
67
68
dispose(): void {
69
this._referenceSearchVisible.reset();
70
this._disposables.dispose();
71
this._widget?.dispose();
72
this._model?.dispose();
73
this._widget = undefined;
74
this._model = undefined;
75
}
76
77
toggleWidget(range: Range, modelPromise: CancelablePromise<ReferencesModel>, peekMode: boolean): void {
78
79
// close current widget and return early is position didn't change
80
let widgetPosition: Position | undefined;
81
if (this._widget) {
82
widgetPosition = this._widget.position;
83
}
84
this.closeWidget();
85
if (!!widgetPosition && range.containsPosition(widgetPosition)) {
86
return;
87
}
88
89
this._peekMode = peekMode;
90
this._referenceSearchVisible.set(true);
91
92
// close the widget on model/mode changes
93
this._disposables.add(this._editor.onDidChangeModelLanguage(() => { this.closeWidget(); }));
94
this._disposables.add(this._editor.onDidChangeModel(() => {
95
if (!this._ignoreModelChangeEvent) {
96
this.closeWidget();
97
}
98
}));
99
const storageKey = 'peekViewLayout';
100
const data = LayoutData.fromJSON(this._storageService.get(storageKey, StorageScope.PROFILE, '{}'));
101
this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data);
102
this._widget.setTitle(nls.localize('labelLoading', "Loading..."));
103
this._widget.show(range);
104
105
this._disposables.add(this._widget.onDidClose(() => {
106
modelPromise.cancel();
107
if (this._widget) {
108
this._storageService.store(storageKey, JSON.stringify(this._widget.layoutData), StorageScope.PROFILE, StorageTarget.MACHINE);
109
if (!this._widget.isClosing) {
110
// to prevent calling this too many times, check whether it was already closing.
111
this.closeWidget();
112
}
113
this._widget = undefined;
114
} else {
115
this.closeWidget();
116
}
117
}));
118
119
this._disposables.add(this._widget.onDidSelectReference(event => {
120
const { element, kind } = event;
121
if (!element) {
122
return;
123
}
124
switch (kind) {
125
case 'open':
126
if (event.source !== 'editor' || !this._configurationService.getValue('editor.stablePeek')) {
127
// when stable peek is configured we don't close
128
// the peek window on selecting the editor
129
this.openReference(element, false, false);
130
}
131
break;
132
case 'side':
133
this.openReference(element, true, false);
134
break;
135
case 'goto':
136
if (peekMode) {
137
this._gotoReference(element, true);
138
} else {
139
this.openReference(element, false, true);
140
}
141
break;
142
}
143
}));
144
145
const requestId = ++this._requestIdPool;
146
147
modelPromise.then(model => {
148
149
// still current request? widget still open?
150
if (requestId !== this._requestIdPool || !this._widget) {
151
model.dispose();
152
return undefined;
153
}
154
155
this._model?.dispose();
156
this._model = model;
157
158
// show widget
159
return this._widget.setModel(this._model).then(() => {
160
if (this._widget && this._model && this._editor.hasModel()) { // might have been closed
161
162
// set title
163
if (!this._model.isEmpty) {
164
this._widget.setMetaTitle(nls.localize('metaTitle.N', "{0} ({1})", this._model.title, this._model.references.length));
165
} else {
166
this._widget.setMetaTitle('');
167
}
168
169
// set 'best' selection
170
const uri = this._editor.getModel().uri;
171
const pos = new Position(range.startLineNumber, range.startColumn);
172
const selection = this._model.nearestReference(uri, pos);
173
if (selection) {
174
return this._widget.setSelection(selection).then(() => {
175
if (this._widget && this._editor.getOption(EditorOption.peekWidgetDefaultFocus) === 'editor') {
176
this._widget.focusOnPreviewEditor();
177
}
178
});
179
}
180
}
181
return undefined;
182
});
183
184
}, error => {
185
this._notificationService.error(error);
186
});
187
}
188
189
changeFocusBetweenPreviewAndReferences() {
190
if (!this._widget) {
191
// can be called while still resolving...
192
return;
193
}
194
if (this._widget.isPreviewEditorFocused()) {
195
this._widget.focusOnReferenceTree();
196
} else {
197
this._widget.focusOnPreviewEditor();
198
}
199
}
200
201
async goToNextOrPreviousReference(fwd: boolean) {
202
if (!this._editor.hasModel() || !this._model || !this._widget) {
203
// can be called while still resolving...
204
return;
205
}
206
const currentPosition = this._widget.position;
207
if (!currentPosition) {
208
return;
209
}
210
const source = this._model.nearestReference(this._editor.getModel().uri, currentPosition);
211
if (!source) {
212
return;
213
}
214
const target = this._model.nextOrPreviousReference(source, fwd);
215
const editorFocus = this._editor.hasTextFocus();
216
const previewEditorFocus = this._widget.isPreviewEditorFocused();
217
await this._widget.setSelection(target);
218
await this._gotoReference(target, false);
219
if (editorFocus) {
220
this._editor.focus();
221
} else if (this._widget && previewEditorFocus) {
222
this._widget.focusOnPreviewEditor();
223
}
224
}
225
226
async revealReference(reference: OneReference): Promise<void> {
227
if (!this._editor.hasModel() || !this._model || !this._widget) {
228
// can be called while still resolving...
229
return;
230
}
231
232
await this._widget.revealReference(reference);
233
}
234
235
closeWidget(focusEditor = true): void {
236
this._widget?.dispose();
237
this._model?.dispose();
238
this._referenceSearchVisible.reset();
239
this._disposables.clear();
240
this._widget = undefined;
241
this._model = undefined;
242
if (focusEditor) {
243
this._editor.focus();
244
}
245
this._requestIdPool += 1; // Cancel pending requests
246
}
247
248
private _gotoReference(ref: Location, pinned: boolean): Promise<any> {
249
this._widget?.hide();
250
251
this._ignoreModelChangeEvent = true;
252
const range = Range.lift(ref.range).collapseToStart();
253
254
return this._editorService.openCodeEditor({
255
resource: ref.uri,
256
options: { selection: range, selectionSource: TextEditorSelectionSource.JUMP, pinned }
257
}, this._editor).then(openedEditor => {
258
this._ignoreModelChangeEvent = false;
259
260
if (!openedEditor || !this._widget) {
261
// something went wrong...
262
this.closeWidget();
263
return;
264
}
265
266
if (this._editor === openedEditor) {
267
//
268
this._widget.show(range);
269
this._widget.focusOnReferenceTree();
270
271
} else {
272
// we opened a different editor instance which means a different controller instance.
273
// therefore we stop with this controller and continue with the other
274
const other = ReferencesController.get(openedEditor);
275
const model = this._model!.clone();
276
277
this.closeWidget();
278
openedEditor.focus();
279
280
other?.toggleWidget(
281
range,
282
createCancelablePromise(_ => Promise.resolve(model)),
283
this._peekMode ?? false
284
);
285
}
286
287
}, (err) => {
288
this._ignoreModelChangeEvent = false;
289
onUnexpectedError(err);
290
});
291
}
292
293
openReference(ref: Location, sideBySide: boolean, pinned: boolean): void {
294
// clear stage
295
if (!sideBySide) {
296
this.closeWidget();
297
}
298
299
const { uri, range } = ref;
300
this._editorService.openCodeEditor({
301
resource: uri,
302
options: { selection: range, selectionSource: TextEditorSelectionSource.JUMP, pinned }
303
}, this._editor, sideBySide);
304
}
305
}
306
307
function withController(accessor: ServicesAccessor, fn: (controller: ReferencesController) => void): void {
308
const outerEditor = getOuterEditor(accessor);
309
if (!outerEditor) {
310
return;
311
}
312
const controller = ReferencesController.get(outerEditor);
313
if (controller) {
314
fn(controller);
315
}
316
}
317
318
KeybindingsRegistry.registerCommandAndKeybindingRule({
319
id: 'togglePeekWidgetFocus',
320
weight: KeybindingWeight.EditorContrib,
321
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.F2),
322
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
323
handler(accessor) {
324
withController(accessor, controller => {
325
controller.changeFocusBetweenPreviewAndReferences();
326
});
327
}
328
});
329
330
KeybindingsRegistry.registerCommandAndKeybindingRule({
331
id: 'goToNextReference',
332
weight: KeybindingWeight.EditorContrib - 10,
333
primary: KeyCode.F4,
334
secondary: [KeyCode.F12],
335
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
336
handler(accessor) {
337
withController(accessor, controller => {
338
controller.goToNextOrPreviousReference(true);
339
});
340
}
341
});
342
343
KeybindingsRegistry.registerCommandAndKeybindingRule({
344
id: 'goToPreviousReference',
345
weight: KeybindingWeight.EditorContrib - 10,
346
primary: KeyMod.Shift | KeyCode.F4,
347
secondary: [KeyMod.Shift | KeyCode.F12],
348
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
349
handler(accessor) {
350
withController(accessor, controller => {
351
controller.goToNextOrPreviousReference(false);
352
});
353
}
354
});
355
356
// commands that aren't needed anymore because there is now ContextKeyExpr.OR
357
CommandsRegistry.registerCommandAlias('goToNextReferenceFromEmbeddedEditor', 'goToNextReference');
358
CommandsRegistry.registerCommandAlias('goToPreviousReferenceFromEmbeddedEditor', 'goToPreviousReference');
359
360
// close
361
CommandsRegistry.registerCommandAlias('closeReferenceSearchEditor', 'closeReferenceSearch');
362
CommandsRegistry.registerCommand(
363
'closeReferenceSearch',
364
accessor => withController(accessor, controller => controller.closeWidget())
365
);
366
KeybindingsRegistry.registerKeybindingRule({
367
id: 'closeReferenceSearch',
368
weight: KeybindingWeight.EditorContrib - 101,
369
primary: KeyCode.Escape,
370
secondary: [KeyMod.Shift | KeyCode.Escape],
371
when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek'))
372
});
373
KeybindingsRegistry.registerKeybindingRule({
374
id: 'closeReferenceSearch',
375
weight: KeybindingWeight.WorkbenchContrib + 50,
376
primary: KeyCode.Escape,
377
secondary: [KeyMod.Shift | KeyCode.Escape],
378
when: ContextKeyExpr.and(
379
ctxReferenceSearchVisible,
380
ContextKeyExpr.not('config.editor.stablePeek'),
381
ContextKeyExpr.or(
382
EditorContextKeys.editorTextFocus,
383
InputFocusedContext.negate()
384
)
385
)
386
});
387
388
389
KeybindingsRegistry.registerCommandAndKeybindingRule({
390
id: 'revealReference',
391
weight: KeybindingWeight.WorkbenchContrib,
392
primary: KeyCode.Enter,
393
mac: {
394
primary: KeyCode.Enter,
395
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow]
396
},
397
when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse.negate(), WorkbenchTreeElementCanExpand.negate()),
398
handler(accessor: ServicesAccessor) {
399
const listService = accessor.get(IListService);
400
const focus = <any[]>listService.lastFocusedList?.getFocus();
401
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
402
withController(accessor, controller => controller.revealReference(focus[0]));
403
}
404
}
405
});
406
407
KeybindingsRegistry.registerCommandAndKeybindingRule({
408
id: 'openReferenceToSide',
409
weight: KeybindingWeight.EditorContrib,
410
primary: KeyMod.CtrlCmd | KeyCode.Enter,
411
mac: {
412
primary: KeyMod.WinCtrl | KeyCode.Enter
413
},
414
when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey, WorkbenchTreeElementCanCollapse.negate(), WorkbenchTreeElementCanExpand.negate()),
415
handler(accessor: ServicesAccessor) {
416
const listService = accessor.get(IListService);
417
const focus = <any[]>listService.lastFocusedList?.getFocus();
418
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
419
withController(accessor, controller => controller.openReference(focus[0], true, true));
420
}
421
}
422
});
423
424
CommandsRegistry.registerCommand('openReference', (accessor) => {
425
const listService = accessor.get(IListService);
426
const focus = <any[]>listService.lastFocusedList?.getFocus();
427
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
428
withController(accessor, controller => controller.openReference(focus[0], false, true));
429
}
430
});
431
432