Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.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 * as dom from '../../../../../base/browser/dom.js';
7
import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js';
8
import { Orientation } from '../../../../../base/browser/ui/sash/sash.js';
9
import { Sizing, SplitView } from '../../../../../base/browser/ui/splitview/splitview.js';
10
import { Color } from '../../../../../base/common/color.js';
11
import { Emitter, Event } from '../../../../../base/common/event.js';
12
import { FuzzyScore } from '../../../../../base/common/filters.js';
13
import { KeyCode } from '../../../../../base/common/keyCodes.js';
14
import { DisposableStore, dispose, IDisposable, IReference } from '../../../../../base/common/lifecycle.js';
15
import { Schemas } from '../../../../../base/common/network.js';
16
import { basenameOrAuthority, dirname } from '../../../../../base/common/resources.js';
17
import './referencesWidget.css';
18
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
19
import { EmbeddedCodeEditorWidget } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js';
20
import { IEditorOptions } from '../../../../common/config/editorOptions.js';
21
import { IRange, Range } from '../../../../common/core/range.js';
22
import { ScrollType } from '../../../../common/editorCommon.js';
23
import { IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../common/model.js';
24
import { ModelDecorationOptions, TextModel } from '../../../../common/model/textModel.js';
25
import { Location } from '../../../../common/languages.js';
26
import { PLAINTEXT_LANGUAGE_ID } from '../../../../common/languages/modesRegistry.js';
27
import { ITextEditorModel, ITextModelService } from '../../../../common/services/resolverService.js';
28
import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from './referencesTree.js';
29
import * as peekView from '../../../peekView/browser/peekView.js';
30
import * as nls from '../../../../../nls.js';
31
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
32
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
33
import { ILabelService } from '../../../../../platform/label/common/label.js';
34
import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from '../../../../../platform/list/browser/listService.js';
35
import { IColorTheme, IThemeService } from '../../../../../platform/theme/common/themeService.js';
36
import { FileReferences, OneReference, ReferencesModel } from '../referencesModel.js';
37
import { ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';
38
import { DataTransfers, IDragAndDropData } from '../../../../../base/browser/dnd.js';
39
import { ElementsDragAndDropData } from '../../../../../base/browser/ui/list/listView.js';
40
import { withSelection } from '../../../../../platform/opener/common/opener.js';
41
42
class DecorationsManager implements IDisposable {
43
44
private static readonly DecorationOptions = ModelDecorationOptions.register({
45
description: 'reference-decoration',
46
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
47
className: 'reference-decoration'
48
});
49
50
private _decorations = new Map<string, OneReference>();
51
private _decorationIgnoreSet = new Set<string>();
52
private readonly _callOnDispose = new DisposableStore();
53
private readonly _callOnModelChange = new DisposableStore();
54
55
constructor(private _editor: ICodeEditor, private _model: ReferencesModel) {
56
this._callOnDispose.add(this._editor.onDidChangeModel(() => this._onModelChanged()));
57
this._onModelChanged();
58
}
59
60
dispose(): void {
61
this._callOnModelChange.dispose();
62
this._callOnDispose.dispose();
63
this.removeDecorations();
64
}
65
66
private _onModelChanged(): void {
67
this._callOnModelChange.clear();
68
const model = this._editor.getModel();
69
if (!model) {
70
return;
71
}
72
for (const ref of this._model.references) {
73
if (ref.uri.toString() === model.uri.toString()) {
74
this._addDecorations(ref.parent);
75
return;
76
}
77
}
78
}
79
80
private _addDecorations(reference: FileReferences): void {
81
if (!this._editor.hasModel()) {
82
return;
83
}
84
this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations(() => this._onDecorationChanged()));
85
86
const newDecorations: IModelDeltaDecoration[] = [];
87
const newDecorationsActualIndex: number[] = [];
88
89
for (let i = 0, len = reference.children.length; i < len; i++) {
90
const oneReference = reference.children[i];
91
if (this._decorationIgnoreSet.has(oneReference.id)) {
92
continue;
93
}
94
if (oneReference.uri.toString() !== this._editor.getModel().uri.toString()) {
95
continue;
96
}
97
newDecorations.push({
98
range: oneReference.range,
99
options: DecorationsManager.DecorationOptions
100
});
101
newDecorationsActualIndex.push(i);
102
}
103
104
this._editor.changeDecorations((changeAccessor) => {
105
const decorations = changeAccessor.deltaDecorations([], newDecorations);
106
for (let i = 0; i < decorations.length; i++) {
107
this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]);
108
}
109
});
110
}
111
112
private _onDecorationChanged(): void {
113
const toRemove: string[] = [];
114
115
const model = this._editor.getModel();
116
if (!model) {
117
return;
118
}
119
120
for (const [decorationId, reference] of this._decorations) {
121
122
const newRange = model.getDecorationRange(decorationId);
123
124
if (!newRange) {
125
continue;
126
}
127
128
let ignore = false;
129
if (Range.equalsRange(newRange, reference.range)) {
130
continue;
131
132
}
133
134
if (Range.spansMultipleLines(newRange)) {
135
ignore = true;
136
137
} else {
138
const lineLength = reference.range.endColumn - reference.range.startColumn;
139
const newLineLength = newRange.endColumn - newRange.startColumn;
140
141
if (lineLength !== newLineLength) {
142
ignore = true;
143
}
144
}
145
146
if (ignore) {
147
this._decorationIgnoreSet.add(reference.id);
148
toRemove.push(decorationId);
149
} else {
150
reference.range = newRange;
151
}
152
}
153
154
for (let i = 0, len = toRemove.length; i < len; i++) {
155
this._decorations.delete(toRemove[i]);
156
}
157
this._editor.removeDecorations(toRemove);
158
}
159
160
removeDecorations(): void {
161
this._editor.removeDecorations([...this._decorations.keys()]);
162
this._decorations.clear();
163
}
164
}
165
166
export class LayoutData {
167
ratio: number = 0.7;
168
heightInLines: number = 18;
169
170
static fromJSON(raw: string): LayoutData {
171
let ratio: number | undefined;
172
let heightInLines: number | undefined;
173
try {
174
const data = <LayoutData>JSON.parse(raw);
175
ratio = data.ratio;
176
heightInLines = data.heightInLines;
177
} catch {
178
//
179
}
180
return {
181
ratio: ratio || 0.7,
182
heightInLines: heightInLines || 18
183
};
184
}
185
}
186
187
export interface SelectionEvent {
188
readonly kind: 'goto' | 'show' | 'side' | 'open';
189
readonly source: 'editor' | 'tree' | 'title';
190
readonly element?: Location;
191
}
192
193
class ReferencesTree extends WorkbenchAsyncDataTree<ReferencesModel | FileReferences, TreeElement, FuzzyScore> { }
194
195
class ReferencesDragAndDrop implements ITreeDragAndDrop<TreeElement> {
196
197
private readonly disposables = new DisposableStore();
198
199
constructor(@ILabelService private readonly labelService: ILabelService) { }
200
201
getDragURI(element: TreeElement): string | null {
202
if (element instanceof FileReferences) {
203
return element.uri.toString();
204
} else if (element instanceof OneReference) {
205
return withSelection(element.uri, element.range).toString();
206
}
207
return null;
208
}
209
210
getDragLabel(elements: TreeElement[]): string | undefined {
211
if (elements.length === 0) {
212
return undefined;
213
}
214
const labels = elements.map(e => this.labelService.getUriBasenameLabel(e.uri));
215
return labels.join(', ');
216
}
217
218
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
219
if (!originalEvent.dataTransfer) {
220
return;
221
}
222
223
const elements = (data as ElementsDragAndDropData<TreeElement, TreeElement[]>).elements;
224
const resources = elements.map(e => this.getDragURI(e)).filter(Boolean);
225
226
if (resources.length) {
227
// Apply resources as resource-list
228
originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(resources));
229
230
// Also add as plain text for outside consumers
231
originalEvent.dataTransfer.setData(DataTransfers.TEXT, resources.join('\n'));
232
}
233
}
234
235
onDragOver(): boolean | ITreeDragOverReaction { return false; }
236
drop(): void { }
237
dispose(): void { this.disposables.dispose(); }
238
}
239
240
/**
241
* ZoneWidget that is shown inside the editor
242
*/
243
export class ReferenceWidget extends peekView.PeekViewWidget {
244
245
private _model?: ReferencesModel;
246
private _decorationsManager?: DecorationsManager;
247
248
private readonly _disposeOnNewModel = new DisposableStore();
249
private readonly _callOnDispose = new DisposableStore();
250
251
private readonly _onDidSelectReference = new Emitter<SelectionEvent>();
252
readonly onDidSelectReference = this._onDidSelectReference.event;
253
254
private _tree!: ReferencesTree;
255
private _treeContainer!: HTMLElement;
256
private _splitView!: SplitView;
257
private _preview!: ICodeEditor;
258
private _previewModelReference!: IReference<ITextEditorModel>;
259
private _previewNotAvailableMessage!: TextModel;
260
private _previewContainer!: HTMLElement;
261
private _messageContainer!: HTMLElement;
262
private _dim = new dom.Dimension(0, 0);
263
private _isClosing = false; // whether or not a dispose is already in progress
264
265
constructor(
266
editor: ICodeEditor,
267
private _defaultTreeKeyboardSupport: boolean,
268
public layoutData: LayoutData,
269
@IThemeService themeService: IThemeService,
270
@ITextModelService private readonly _textModelResolverService: ITextModelService,
271
@IInstantiationService private readonly _instantiationService: IInstantiationService,
272
@peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService,
273
@ILabelService private readonly _uriLabel: ILabelService,
274
@IKeybindingService private readonly _keybindingService: IKeybindingService,
275
) {
276
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService);
277
278
this._applyTheme(themeService.getColorTheme());
279
this._callOnDispose.add(themeService.onDidColorThemeChange(this._applyTheme.bind(this)));
280
this._peekViewService.addExclusiveWidget(editor, this);
281
this.create();
282
}
283
284
get isClosing() {
285
return this._isClosing;
286
}
287
288
override dispose(): void {
289
this._isClosing = true;
290
this.setModel(undefined);
291
this._callOnDispose.dispose();
292
this._disposeOnNewModel.dispose();
293
dispose(this._preview);
294
dispose(this._previewNotAvailableMessage);
295
dispose(this._tree);
296
dispose(this._previewModelReference);
297
this._splitView.dispose();
298
super.dispose();
299
}
300
301
private _applyTheme(theme: IColorTheme) {
302
const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent;
303
this.style({
304
arrowColor: borderColor,
305
frameColor: borderColor,
306
headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent,
307
primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground),
308
secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground)
309
});
310
}
311
312
override show(where: IRange) {
313
super.show(where, this.layoutData.heightInLines || 18);
314
}
315
316
focusOnReferenceTree(): void {
317
this._tree.domFocus();
318
}
319
320
focusOnPreviewEditor(): void {
321
this._preview.focus();
322
}
323
324
isPreviewEditorFocused(): boolean {
325
return this._preview.hasTextFocus();
326
}
327
328
protected override _onTitleClick(e: IMouseEvent): void {
329
if (this._preview && this._preview.getModel()) {
330
this._onDidSelectReference.fire({
331
element: this._getFocusedReference(),
332
kind: e.ctrlKey || e.metaKey || e.altKey ? 'side' : 'open',
333
source: 'title'
334
});
335
}
336
}
337
338
protected _fillBody(containerElement: HTMLElement): void {
339
this.setCssClass('reference-zone-widget');
340
341
// message pane
342
this._messageContainer = dom.append(containerElement, dom.$('div.messages'));
343
dom.hide(this._messageContainer);
344
345
this._splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL });
346
347
// editor
348
this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline'));
349
const options: IEditorOptions = {
350
scrollBeyondLastLine: false,
351
scrollbar: {
352
verticalScrollbarSize: 14,
353
horizontal: 'auto',
354
useShadows: true,
355
verticalHasArrows: false,
356
horizontalHasArrows: false,
357
alwaysConsumeMouseWheel: true
358
},
359
overviewRulerLanes: 2,
360
fixedOverflowWidgets: true,
361
minimap: {
362
enabled: false
363
}
364
};
365
this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor);
366
dom.hide(this._previewContainer);
367
this._previewNotAvailableMessage = this._instantiationService.createInstance(TextModel, nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null);
368
369
// tree
370
this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline'));
371
const treeOptions: IWorkbenchAsyncDataTreeOptions<TreeElement, FuzzyScore> = {
372
keyboardSupport: this._defaultTreeKeyboardSupport,
373
accessibilityProvider: new AccessibilityProvider(),
374
keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider),
375
identityProvider: new IdentityProvider(),
376
openOnSingleClick: true,
377
selectionNavigation: true,
378
overrideStyles: {
379
listBackground: peekView.peekViewResultsBackground
380
},
381
dnd: this._instantiationService.createInstance(ReferencesDragAndDrop)
382
};
383
if (this._defaultTreeKeyboardSupport) {
384
// the tree will consume `Escape` and prevent the widget from closing
385
this._callOnDispose.add(dom.addStandardDisposableListener(this._treeContainer, 'keydown', (e) => {
386
if (e.equals(KeyCode.Escape)) {
387
this._keybindingService.dispatchEvent(e, e.target);
388
e.stopPropagation();
389
}
390
}, true));
391
}
392
this._tree = this._instantiationService.createInstance(
393
ReferencesTree,
394
'ReferencesWidget',
395
this._treeContainer,
396
new Delegate(),
397
[
398
this._instantiationService.createInstance(FileReferencesRenderer),
399
this._instantiationService.createInstance(OneReferenceRenderer),
400
],
401
this._instantiationService.createInstance(DataSource),
402
treeOptions,
403
);
404
405
// split stuff
406
this._splitView.addView({
407
onDidChange: Event.None,
408
element: this._previewContainer,
409
minimumSize: 200,
410
maximumSize: Number.MAX_VALUE,
411
layout: (width) => {
412
this._preview.layout({ height: this._dim.height, width });
413
}
414
}, Sizing.Distribute);
415
416
this._splitView.addView({
417
onDidChange: Event.None,
418
element: this._treeContainer,
419
minimumSize: 100,
420
maximumSize: Number.MAX_VALUE,
421
layout: (width) => {
422
this._treeContainer.style.height = `${this._dim.height}px`;
423
this._treeContainer.style.width = `${width}px`;
424
this._tree.layout(this._dim.height, width);
425
}
426
}, Sizing.Distribute);
427
428
this._disposables.add(this._splitView.onDidSashChange(() => {
429
if (this._dim.width) {
430
this.layoutData.ratio = this._splitView.getViewSize(0) / this._dim.width;
431
}
432
}, undefined));
433
434
// listen on selection and focus
435
const onEvent = (element: any, kind: 'show' | 'goto' | 'side') => {
436
if (element instanceof OneReference) {
437
if (kind === 'show') {
438
this._revealReference(element, false);
439
}
440
this._onDidSelectReference.fire({ element, kind, source: 'tree' });
441
}
442
};
443
this._disposables.add(this._tree.onDidOpen(e => {
444
if (e.sideBySide) {
445
onEvent(e.element, 'side');
446
} else if (e.editorOptions.pinned) {
447
onEvent(e.element, 'goto');
448
} else {
449
onEvent(e.element, 'show');
450
}
451
}));
452
453
dom.hide(this._treeContainer);
454
}
455
456
protected override _onWidth(width: number) {
457
if (this._dim) {
458
this._doLayoutBody(this._dim.height, width);
459
}
460
}
461
462
protected override _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
463
super._doLayoutBody(heightInPixel, widthInPixel);
464
this._dim = new dom.Dimension(widthInPixel, heightInPixel);
465
this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines;
466
this._splitView.layout(widthInPixel);
467
this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio);
468
}
469
470
setSelection(selection: OneReference): Promise<any> {
471
return this._revealReference(selection, true).then(() => {
472
if (!this._model) {
473
// disposed
474
return;
475
}
476
// show in tree
477
this._tree.setSelection([selection]);
478
this._tree.setFocus([selection]);
479
});
480
}
481
482
setModel(newModel: ReferencesModel | undefined): Promise<any> {
483
// clean up
484
this._disposeOnNewModel.clear();
485
this._model = newModel;
486
if (this._model) {
487
return this._onNewModel();
488
}
489
return Promise.resolve();
490
}
491
492
private _onNewModel(): Promise<any> {
493
if (!this._model) {
494
return Promise.resolve(undefined);
495
}
496
497
if (this._model.isEmpty) {
498
this.setTitle('');
499
this._messageContainer.innerText = nls.localize('noResults', "No results");
500
dom.show(this._messageContainer);
501
return Promise.resolve(undefined);
502
}
503
504
dom.hide(this._messageContainer);
505
this._decorationsManager = new DecorationsManager(this._preview, this._model);
506
this._disposeOnNewModel.add(this._decorationsManager);
507
508
// listen on model changes
509
this._disposeOnNewModel.add(this._model.onDidChangeReferenceRange(reference => this._tree.rerender(reference)));
510
511
// listen on editor
512
this._disposeOnNewModel.add(this._preview.onMouseDown(e => {
513
const { event, target } = e;
514
if (event.detail !== 2) {
515
return;
516
}
517
const element = this._getFocusedReference();
518
if (!element) {
519
return;
520
}
521
this._onDidSelectReference.fire({
522
element: { uri: element.uri, range: target.range! },
523
kind: (event.ctrlKey || event.metaKey || event.altKey) ? 'side' : 'open',
524
source: 'editor'
525
});
526
}));
527
528
// make sure things are rendered
529
this.container!.classList.add('results-loaded');
530
dom.show(this._treeContainer);
531
dom.show(this._previewContainer);
532
this._splitView.layout(this._dim.width);
533
this.focusOnReferenceTree();
534
535
// pick input and a reference to begin with
536
return this._tree.setInput(this._model.groups.length === 1 ? this._model.groups[0] : this._model);
537
}
538
539
private _getFocusedReference(): OneReference | undefined {
540
const [element] = this._tree.getFocus();
541
if (element instanceof OneReference) {
542
return element;
543
} else if (element instanceof FileReferences) {
544
if (element.children.length > 0) {
545
return element.children[0];
546
}
547
}
548
return undefined;
549
}
550
551
async revealReference(reference: OneReference): Promise<void> {
552
await this._revealReference(reference, false);
553
this._onDidSelectReference.fire({ element: reference, kind: 'goto', source: 'tree' });
554
}
555
556
private _revealedReference?: OneReference;
557
558
private async _revealReference(reference: OneReference, revealParent: boolean): Promise<void> {
559
560
// check if there is anything to do...
561
if (this._revealedReference === reference) {
562
return;
563
}
564
this._revealedReference = reference;
565
566
// Update widget header
567
if (reference.uri.scheme !== Schemas.inMemory) {
568
this.setTitle(basenameOrAuthority(reference.uri), this._uriLabel.getUriLabel(dirname(reference.uri)));
569
} else {
570
this.setTitle(nls.localize('peekView.alternateTitle', "References"));
571
}
572
573
const promise = this._textModelResolverService.createModelReference(reference.uri);
574
575
if (this._tree.getInput() === reference.parent) {
576
this._tree.reveal(reference);
577
} else {
578
if (revealParent) {
579
this._tree.reveal(reference.parent);
580
}
581
await this._tree.expand(reference.parent);
582
this._tree.reveal(reference);
583
}
584
585
const ref = await promise;
586
587
if (!this._model) {
588
// disposed
589
ref.dispose();
590
return;
591
}
592
593
dispose(this._previewModelReference);
594
595
// show in editor
596
const model = ref.object;
597
if (model) {
598
const scrollType = this._preview.getModel() === model.textEditorModel ? ScrollType.Smooth : ScrollType.Immediate;
599
const sel = Range.lift(reference.range).collapseToStart();
600
this._previewModelReference = ref;
601
this._preview.setModel(model.textEditorModel);
602
this._preview.setSelection(sel);
603
this._preview.revealRangeInCenter(sel, scrollType);
604
} else {
605
this._preview.setModel(this._previewNotAvailableMessage);
606
ref.dispose();
607
}
608
}
609
}
610
611