Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts
4780 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 './media/callHierarchy.css';
7
import * as peekView from '../../../../editor/contrib/peekView/browser/peekView.js';
8
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
9
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
10
import { CallHierarchyDirection, CallHierarchyModel } from '../common/callHierarchy.js';
11
import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from '../../../../platform/list/browser/listService.js';
12
import { FuzzyScore } from '../../../../base/common/filters.js';
13
import * as callHTree from './callHierarchyTree.js';
14
import { IAsyncDataTreeViewState } from '../../../../base/browser/ui/tree/asyncDataTree.js';
15
import { localize } from '../../../../nls.js';
16
import { ScrollType } from '../../../../editor/common/editorCommon.js';
17
import { IRange, Range } from '../../../../editor/common/core/range.js';
18
import { SplitView, Orientation, Sizing } from '../../../../base/browser/ui/splitview/splitview.js';
19
import { Dimension, isKeyboardEvent } from '../../../../base/browser/dom.js';
20
import { Event } from '../../../../base/common/event.js';
21
import { IEditorService } from '../../../services/editor/common/editorService.js';
22
import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';
23
import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js';
24
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
25
import { toDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';
26
import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions, OverviewRulerLane } from '../../../../editor/common/model.js';
27
import { themeColorFromId, IThemeService, IColorTheme } from '../../../../platform/theme/common/themeService.js';
28
import { IPosition } from '../../../../editor/common/core/position.js';
29
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
30
import { Color } from '../../../../base/common/color.js';
31
import { TreeMouseEventTarget, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';
32
import { URI } from '../../../../base/common/uri.js';
33
import { MenuId, IMenuService } from '../../../../platform/actions/common/actions.js';
34
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
35
import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
36
37
const enum State {
38
Loading = 'loading',
39
Message = 'message',
40
Data = 'data'
41
}
42
43
class LayoutInfo {
44
45
static store(info: LayoutInfo, storageService: IStorageService): void {
46
storageService.store('callHierarchyPeekLayout', JSON.stringify(info), StorageScope.PROFILE, StorageTarget.MACHINE);
47
}
48
49
static retrieve(storageService: IStorageService): LayoutInfo {
50
const value = storageService.get('callHierarchyPeekLayout', StorageScope.PROFILE, '{}');
51
const defaultInfo: LayoutInfo = { ratio: 0.7, height: 17 };
52
try {
53
return { ...defaultInfo, ...JSON.parse(value) };
54
} catch {
55
return defaultInfo;
56
}
57
}
58
59
constructor(
60
public ratio: number,
61
public height: number
62
) { }
63
}
64
65
class CallHierarchyTree extends WorkbenchAsyncDataTree<CallHierarchyModel, callHTree.Call, FuzzyScore> { }
66
67
export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget {
68
69
static readonly TitleMenu = new MenuId('callhierarchy/title');
70
71
private _parent!: HTMLElement;
72
private _message!: HTMLElement;
73
private _splitView!: SplitView;
74
private _tree!: CallHierarchyTree;
75
private _treeViewStates = new Map<CallHierarchyDirection, IAsyncDataTreeViewState>();
76
private _editor!: EmbeddedCodeEditorWidget;
77
private _dim!: Dimension;
78
private _layoutInfo!: LayoutInfo;
79
80
private readonly _previewDisposable = new DisposableStore();
81
82
constructor(
83
editor: ICodeEditor,
84
private readonly _where: IPosition,
85
private _direction: CallHierarchyDirection,
86
@IThemeService themeService: IThemeService,
87
@peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService,
88
@IEditorService private readonly _editorService: IEditorService,
89
@ITextModelService private readonly _textModelService: ITextModelService,
90
@IStorageService private readonly _storageService: IStorageService,
91
@IMenuService private readonly _menuService: IMenuService,
92
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
93
@IInstantiationService private readonly _instantiationService: IInstantiationService,
94
) {
95
super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService);
96
this.create();
97
this._peekViewService.addExclusiveWidget(editor, this);
98
this._applyTheme(themeService.getColorTheme());
99
this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this));
100
this._disposables.add(this._previewDisposable);
101
}
102
103
override dispose(): void {
104
LayoutInfo.store(this._layoutInfo, this._storageService);
105
this._splitView.dispose();
106
this._tree.dispose();
107
this._editor.dispose();
108
super.dispose();
109
}
110
111
get direction(): CallHierarchyDirection {
112
return this._direction;
113
}
114
115
private _applyTheme(theme: IColorTheme) {
116
const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent;
117
this.style({
118
arrowColor: borderColor,
119
frameColor: borderColor,
120
headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent,
121
primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground),
122
secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground)
123
});
124
}
125
126
protected override _fillHead(container: HTMLElement): void {
127
super._fillHead(container, true);
128
129
const menu = this._menuService.createMenu(CallHierarchyTreePeekWidget.TitleMenu, this._contextKeyService);
130
const updateToolbar = () => {
131
const actions = getFlatActionBarActions(menu.getActions());
132
this._actionbarWidget!.clear();
133
this._actionbarWidget!.push(actions, { label: false, icon: true });
134
};
135
this._disposables.add(menu);
136
this._disposables.add(menu.onDidChange(updateToolbar));
137
updateToolbar();
138
}
139
140
protected _fillBody(parent: HTMLElement): void {
141
142
this._layoutInfo = LayoutInfo.retrieve(this._storageService);
143
this._dim = new Dimension(0, 0);
144
145
this._parent = parent;
146
parent.classList.add('call-hierarchy');
147
148
const message = document.createElement('div');
149
message.classList.add('message');
150
parent.appendChild(message);
151
this._message = message;
152
this._message.tabIndex = 0;
153
154
const container = document.createElement('div');
155
container.classList.add('results');
156
parent.appendChild(container);
157
158
this._splitView = new SplitView(container, { orientation: Orientation.HORIZONTAL });
159
160
// editor stuff
161
const editorContainer = document.createElement('div');
162
editorContainer.classList.add('editor');
163
container.appendChild(editorContainer);
164
const editorOptions: IEditorOptions = {
165
scrollBeyondLastLine: false,
166
scrollbar: {
167
verticalScrollbarSize: 14,
168
horizontal: 'auto',
169
useShadows: true,
170
verticalHasArrows: false,
171
horizontalHasArrows: false,
172
alwaysConsumeMouseWheel: false
173
},
174
overviewRulerLanes: 2,
175
fixedOverflowWidgets: true,
176
minimap: {
177
enabled: false
178
}
179
};
180
this._editor = this._instantiationService.createInstance(
181
EmbeddedCodeEditorWidget,
182
editorContainer,
183
editorOptions,
184
{},
185
this.editor
186
);
187
188
// tree stuff
189
const treeContainer = document.createElement('div');
190
treeContainer.classList.add('tree');
191
container.appendChild(treeContainer);
192
const options: IWorkbenchAsyncDataTreeOptions<callHTree.Call, FuzzyScore> = {
193
sorter: new callHTree.Sorter(),
194
accessibilityProvider: new callHTree.AccessibilityProvider(() => this._direction),
195
identityProvider: new callHTree.IdentityProvider(() => this._direction),
196
expandOnlyOnTwistieClick: true,
197
overrideStyles: {
198
listBackground: peekView.peekViewResultsBackground
199
}
200
};
201
this._tree = this._instantiationService.createInstance(
202
CallHierarchyTree,
203
'CallHierarchyPeek',
204
treeContainer,
205
new callHTree.VirtualDelegate(),
206
[this._instantiationService.createInstance(callHTree.CallRenderer)],
207
this._instantiationService.createInstance(callHTree.DataSource, () => this._direction),
208
options
209
);
210
211
// split stuff
212
this._splitView.addView({
213
onDidChange: Event.None,
214
element: editorContainer,
215
minimumSize: 200,
216
maximumSize: Number.MAX_VALUE,
217
layout: (width) => {
218
if (this._dim.height) {
219
this._editor.layout({ height: this._dim.height, width });
220
}
221
}
222
}, Sizing.Distribute);
223
224
this._splitView.addView({
225
onDidChange: Event.None,
226
element: treeContainer,
227
minimumSize: 100,
228
maximumSize: Number.MAX_VALUE,
229
layout: (width) => {
230
if (this._dim.height) {
231
this._tree.layout(this._dim.height, width);
232
}
233
}
234
}, Sizing.Distribute);
235
236
this._disposables.add(this._splitView.onDidSashChange(() => {
237
if (this._dim.width) {
238
this._layoutInfo.ratio = this._splitView.getViewSize(0) / this._dim.width;
239
}
240
}));
241
242
// update editor
243
this._disposables.add(this._tree.onDidChangeFocus(this._updatePreview, this));
244
245
this._disposables.add(this._editor.onMouseDown(e => {
246
const { event, target } = e;
247
if (event.detail !== 2) {
248
return;
249
}
250
const [focus] = this._tree.getFocus();
251
if (!focus) {
252
return;
253
}
254
this.dispose();
255
this._editorService.openEditor({
256
resource: focus.item.uri,
257
options: { selection: target.range! }
258
});
259
260
}));
261
262
this._disposables.add(this._tree.onMouseDblClick(e => {
263
if (e.target === TreeMouseEventTarget.Twistie) {
264
return;
265
}
266
267
if (e.element) {
268
this.dispose();
269
this._editorService.openEditor({
270
resource: e.element.item.uri,
271
options: { selection: e.element.item.selectionRange, pinned: true }
272
});
273
}
274
}));
275
276
this._disposables.add(this._tree.onDidChangeSelection(e => {
277
const [element] = e.elements;
278
// don't close on click
279
if (element && isKeyboardEvent(e.browserEvent)) {
280
this.dispose();
281
this._editorService.openEditor({
282
resource: element.item.uri,
283
options: { selection: element.item.selectionRange, pinned: true }
284
});
285
}
286
}));
287
}
288
289
private async _updatePreview() {
290
const [element] = this._tree.getFocus();
291
if (!element) {
292
return;
293
}
294
295
this._previewDisposable.clear();
296
297
// update: editor and editor highlights
298
const options: IModelDecorationOptions = {
299
description: 'call-hierarchy-decoration',
300
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
301
className: 'call-decoration',
302
overviewRuler: {
303
color: themeColorFromId(peekView.peekViewEditorMatchHighlight),
304
position: OverviewRulerLane.Center
305
},
306
};
307
308
let previewUri: URI;
309
if (this._direction === CallHierarchyDirection.CallsFrom) {
310
// outgoing calls: show caller and highlight focused calls
311
previewUri = element.parent ? element.parent.item.uri : element.model.root.uri;
312
313
} else {
314
// incoming calls: show caller and highlight focused calls
315
previewUri = element.item.uri;
316
}
317
318
const value = await this._textModelService.createModelReference(previewUri);
319
this._editor.setModel(value.object.textEditorModel);
320
321
// set decorations for caller ranges (if in the same file)
322
const decorations: IModelDeltaDecoration[] = [];
323
let fullRange: IRange | undefined;
324
let locations = element.locations;
325
if (!locations) {
326
locations = [{ uri: element.item.uri, range: element.item.selectionRange }];
327
}
328
for (const loc of locations) {
329
if (loc.uri.toString() === previewUri.toString()) {
330
decorations.push({ range: loc.range, options });
331
fullRange = !fullRange ? loc.range : Range.plusRange(loc.range, fullRange);
332
}
333
}
334
if (fullRange) {
335
this._editor.revealRangeInCenter(fullRange, ScrollType.Immediate);
336
const decorationsCollection = this._editor.createDecorationsCollection(decorations);
337
this._previewDisposable.add(toDisposable(() => decorationsCollection.clear()));
338
}
339
this._previewDisposable.add(value);
340
341
// update: title
342
const title = this._direction === CallHierarchyDirection.CallsFrom
343
? localize('callFrom', "Calls from '{0}'", element.model.root.name)
344
: localize('callsTo', "Callers of '{0}'", element.model.root.name);
345
this.setTitle(title);
346
}
347
348
showLoading(): void {
349
this._parent.dataset['state'] = State.Loading;
350
this.setTitle(localize('title.loading', "Loading..."));
351
this._show();
352
}
353
354
showMessage(message: string): void {
355
this._parent.dataset['state'] = State.Message;
356
this.setTitle('');
357
this.setMetaTitle('');
358
this._message.innerText = message;
359
this._show();
360
this._message.focus();
361
}
362
363
async showModel(model: CallHierarchyModel): Promise<void> {
364
365
this._show();
366
const viewState = this._treeViewStates.get(this._direction);
367
368
await this._tree.setInput(model, viewState);
369
370
const root = <ITreeNode<callHTree.Call, FuzzyScore>>this._tree.getNode(model).children[0];
371
await this._tree.expand(root.element);
372
373
if (root.children.length === 0) {
374
//
375
this.showMessage(this._direction === CallHierarchyDirection.CallsFrom
376
? localize('empt.callsFrom', "No calls from '{0}'", model.root.name)
377
: localize('empt.callsTo', "No callers of '{0}'", model.root.name));
378
379
} else {
380
this._parent.dataset['state'] = State.Data;
381
if (!viewState || this._tree.getFocus().length === 0) {
382
this._tree.setFocus([root.children[0].element]);
383
}
384
this._tree.domFocus();
385
this._updatePreview();
386
}
387
}
388
389
getModel(): CallHierarchyModel | undefined {
390
return this._tree.getInput();
391
}
392
393
getFocused(): callHTree.Call | undefined {
394
return this._tree.getFocus()[0];
395
}
396
397
async updateDirection(newDirection: CallHierarchyDirection): Promise<void> {
398
const model = this._tree.getInput();
399
if (model && newDirection !== this._direction) {
400
this._treeViewStates.set(this._direction, this._tree.getViewState());
401
this._direction = newDirection;
402
await this.showModel(model);
403
}
404
}
405
406
private _show() {
407
if (!this._isShowing) {
408
this.editor.revealLineInCenterIfOutsideViewport(this._where.lineNumber, ScrollType.Smooth);
409
super.show(Range.fromPositions(this._where), this._layoutInfo.height);
410
}
411
}
412
413
protected override _onWidth(width: number) {
414
if (this._dim) {
415
this._doLayoutBody(this._dim.height, width);
416
}
417
}
418
419
protected override _doLayoutBody(height: number, width: number): void {
420
if (this._dim.height !== height || this._dim.width !== width) {
421
super._doLayoutBody(height, width);
422
this._dim = new Dimension(width, height);
423
this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height;
424
this._splitView.layout(width);
425
this._splitView.resizeView(0, width * this._layoutInfo.ratio);
426
}
427
}
428
}
429
430