Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/outline/browser/outlinePane.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 './outlinePane.css';
7
import * as dom from '../../../../base/browser/dom.js';
8
import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';
9
import { TimeoutTimer, timeout } from '../../../../base/common/async.js';
10
import { IDisposable, toDisposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
11
import { LRUCache } from '../../../../base/common/map.js';
12
import { localize } from '../../../../nls.js';
13
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
15
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
16
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
18
import { WorkbenchDataTree } from '../../../../platform/list/browser/listService.js';
19
import { IStorageService } from '../../../../platform/storage/common/storage.js';
20
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
21
import { ViewPane } from '../../../browser/parts/views/viewPane.js';
22
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
23
import { IEditorService } from '../../../services/editor/common/editorService.js';
24
import { FuzzyScore } from '../../../../base/common/filters.js';
25
import { basename } from '../../../../base/common/resources.js';
26
import { IViewDescriptorService } from '../../../common/views.js';
27
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
28
import { OutlineViewState } from './outlineViewState.js';
29
import { IOutline, IOutlineComparator, IOutlineService, OutlineTarget } from '../../../services/outline/browser/outline.js';
30
import { EditorResourceAccessor, IEditorPane } from '../../../common/editor.js';
31
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
32
import { Event } from '../../../../base/common/event.js';
33
import { ITreeSorter } from '../../../../base/browser/ui/tree/tree.js';
34
import { AbstractTreeViewState, IAbstractTreeViewState, TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js';
35
import { URI } from '../../../../base/common/uri.js';
36
import { ctxAllCollapsed, ctxFilterOnType, ctxFocused, ctxFollowsCursor, ctxSortMode, IOutlinePane, OutlineSortOrder } from './outline.js';
37
import { defaultProgressBarStyles } from '../../../../platform/theme/browser/defaultStyles.js';
38
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
39
40
class OutlineTreeSorter<E> implements ITreeSorter<E> {
41
42
constructor(
43
private _comparator: IOutlineComparator<E>,
44
public order: OutlineSortOrder
45
) { }
46
47
compare(a: E, b: E): number {
48
if (this.order === OutlineSortOrder.ByKind) {
49
return this._comparator.compareByType(a, b);
50
} else if (this.order === OutlineSortOrder.ByName) {
51
return this._comparator.compareByName(a, b);
52
} else {
53
return this._comparator.compareByPosition(a, b);
54
}
55
}
56
}
57
58
export class OutlinePane extends ViewPane implements IOutlinePane {
59
60
static readonly Id = 'outline';
61
62
private readonly _disposables = new DisposableStore();
63
64
private readonly _editorControlDisposables = new DisposableStore();
65
private readonly _editorPaneDisposables = new DisposableStore();
66
private readonly _outlineViewState = new OutlineViewState();
67
68
private readonly _editorListener = new MutableDisposable();
69
70
private _domNode!: HTMLElement;
71
private _message!: HTMLDivElement;
72
private _progressBar!: ProgressBar;
73
private _treeContainer!: HTMLElement;
74
private _tree?: WorkbenchDataTree<IOutline<any> | undefined, any, FuzzyScore>;
75
private _treeDimensions?: dom.Dimension;
76
private _treeStates = new LRUCache<string, IAbstractTreeViewState>(10);
77
78
private _ctxFollowsCursor!: IContextKey<boolean>;
79
private _ctxFilterOnType!: IContextKey<boolean>;
80
private _ctxSortMode!: IContextKey<OutlineSortOrder>;
81
private _ctxAllCollapsed!: IContextKey<boolean>;
82
83
constructor(
84
options: IViewletViewOptions,
85
@IOutlineService private readonly _outlineService: IOutlineService,
86
@IInstantiationService private readonly _instantiationService: IInstantiationService,
87
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
88
@IStorageService private readonly _storageService: IStorageService,
89
@IEditorService private readonly _editorService: IEditorService,
90
@IConfigurationService configurationService: IConfigurationService,
91
@IKeybindingService keybindingService: IKeybindingService,
92
@IContextKeyService contextKeyService: IContextKeyService,
93
@IContextMenuService contextMenuService: IContextMenuService,
94
@IOpenerService openerService: IOpenerService,
95
@IThemeService themeService: IThemeService,
96
@IHoverService hoverService: IHoverService,
97
) {
98
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, hoverService);
99
this._outlineViewState.restore(this._storageService);
100
this._disposables.add(this._outlineViewState);
101
102
contextKeyService.bufferChangeEvents(() => {
103
this._ctxFollowsCursor = ctxFollowsCursor.bindTo(contextKeyService);
104
this._ctxFilterOnType = ctxFilterOnType.bindTo(contextKeyService);
105
this._ctxSortMode = ctxSortMode.bindTo(contextKeyService);
106
this._ctxAllCollapsed = ctxAllCollapsed.bindTo(contextKeyService);
107
});
108
109
const updateContext = () => {
110
this._ctxFollowsCursor.set(this._outlineViewState.followCursor);
111
this._ctxFilterOnType.set(this._outlineViewState.filterOnType);
112
this._ctxSortMode.set(this._outlineViewState.sortBy);
113
};
114
updateContext();
115
this._disposables.add(this._outlineViewState.onDidChange(updateContext));
116
}
117
118
override dispose(): void {
119
this._disposables.dispose();
120
this._editorPaneDisposables.dispose();
121
this._editorControlDisposables.dispose();
122
this._editorListener.dispose();
123
super.dispose();
124
}
125
126
override focus(): void {
127
this._editorControlChangePromise.then(() => {
128
super.focus();
129
this._tree?.domFocus();
130
});
131
}
132
133
protected override renderBody(container: HTMLElement): void {
134
super.renderBody(container);
135
136
this._domNode = container;
137
container.classList.add('outline-pane');
138
139
const progressContainer = dom.$('.outline-progress');
140
this._message = dom.$('.outline-message');
141
142
this._progressBar = new ProgressBar(progressContainer, defaultProgressBarStyles);
143
144
this._treeContainer = dom.$('.outline-tree');
145
dom.append(container, progressContainer, this._message, this._treeContainer);
146
147
this._disposables.add(this.onDidChangeBodyVisibility(visible => {
148
if (!visible) {
149
// stop everything when not visible
150
this._editorListener.clear();
151
this._editorPaneDisposables.clear();
152
this._editorControlDisposables.clear();
153
154
} else if (!this._editorListener.value) {
155
const event = Event.any(this._editorService.onDidActiveEditorChange, this._outlineService.onDidChange);
156
this._editorListener.value = event(() => this._handleEditorChanged(this._editorService.activeEditorPane));
157
this._handleEditorChanged(this._editorService.activeEditorPane);
158
}
159
}));
160
}
161
162
protected override layoutBody(height: number, width: number): void {
163
super.layoutBody(height, width);
164
this._tree?.layout(height, width);
165
this._treeDimensions = new dom.Dimension(width, height);
166
}
167
168
collapseAll(): void {
169
this._tree?.collapseAll();
170
}
171
172
expandAll(): void {
173
this._tree?.expandAll();
174
}
175
176
get outlineViewState() {
177
return this._outlineViewState;
178
}
179
180
private _showMessage(message: string) {
181
this._domNode.classList.add('message');
182
this._progressBar.stop().hide();
183
this._message.textContent = message;
184
}
185
186
private _captureViewState(uri?: URI): boolean {
187
if (this._tree) {
188
const oldOutline = this._tree.getInput();
189
if (!uri) {
190
uri = oldOutline?.uri;
191
}
192
if (oldOutline && uri) {
193
this._treeStates.set(`${oldOutline.outlineKind}/${uri}`, this._tree.getViewState());
194
return true;
195
}
196
}
197
return false;
198
}
199
200
private _editorControlChangePromise: Promise<void> = Promise.resolve();
201
private _handleEditorChanged(pane: IEditorPane | undefined): void {
202
this._editorPaneDisposables.clear();
203
204
if (pane) {
205
// react to control changes from within pane (https://github.com/microsoft/vscode/issues/134008)
206
this._editorPaneDisposables.add(pane.onDidChangeControl(() => {
207
this._editorControlChangePromise = this._handleEditorControlChanged(pane);
208
}));
209
}
210
211
this._editorControlChangePromise = this._handleEditorControlChanged(pane);
212
}
213
214
private async _handleEditorControlChanged(pane: IEditorPane | undefined): Promise<void> {
215
216
// persist state
217
const resource = EditorResourceAccessor.getOriginalUri(pane?.input);
218
const didCapture = this._captureViewState();
219
220
this._editorControlDisposables.clear();
221
222
if (!pane || !this._outlineService.canCreateOutline(pane) || !resource) {
223
return this._showMessage(localize('no-editor', "The active editor cannot provide outline information."));
224
}
225
226
let loadingMessage: IDisposable | undefined;
227
if (!didCapture) {
228
loadingMessage = new TimeoutTimer(() => {
229
this._showMessage(localize('loading', "Loading document symbols for '{0}'...", basename(resource)));
230
}, 100);
231
}
232
233
this._progressBar.infinite().show(500);
234
235
const cts = new CancellationTokenSource();
236
this._editorControlDisposables.add(toDisposable(() => cts.dispose(true)));
237
238
const newOutline = await this._outlineService.createOutline(pane, OutlineTarget.OutlinePane, cts.token);
239
loadingMessage?.dispose();
240
241
if (!newOutline) {
242
return;
243
}
244
245
if (cts.token.isCancellationRequested) {
246
newOutline?.dispose();
247
return;
248
}
249
250
this._editorControlDisposables.add(newOutline);
251
this._progressBar.stop().hide();
252
253
const sorter = new OutlineTreeSorter(newOutline.config.comparator, this._outlineViewState.sortBy);
254
255
const tree = this._instantiationService.createInstance(
256
WorkbenchDataTree<IOutline<any> | undefined, any, FuzzyScore>,
257
'OutlinePane',
258
this._treeContainer,
259
newOutline.config.delegate,
260
newOutline.config.renderers,
261
newOutline.config.treeDataSource,
262
{
263
...newOutline.config.options,
264
sorter,
265
expandOnDoubleClick: false,
266
expandOnlyOnTwistieClick: true,
267
multipleSelectionSupport: false,
268
hideTwistiesOfChildlessElements: true,
269
defaultFindMode: this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight,
270
overrideStyles: this.getLocationBasedColors().listOverrideStyles
271
}
272
);
273
274
ctxFocused.bindTo(tree.contextKeyService);
275
276
// update tree, listen to changes
277
const updateTree = () => {
278
if (newOutline.isEmpty) {
279
// no more elements
280
this._showMessage(localize('no-symbols', "No symbols found in document '{0}'", basename(resource)));
281
this._captureViewState(resource);
282
tree.setInput(undefined);
283
284
} else if (!tree.getInput()) {
285
// first: init tree
286
this._domNode.classList.remove('message');
287
const state = this._treeStates.get(`${newOutline.outlineKind}/${newOutline.uri}`);
288
tree.setInput(newOutline, state && AbstractTreeViewState.lift(state));
289
290
} else {
291
// update: refresh tree
292
this._domNode.classList.remove('message');
293
tree.updateChildren();
294
}
295
};
296
updateTree();
297
this._editorControlDisposables.add(newOutline.onDidChange(updateTree));
298
tree.findMode = this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight;
299
300
// feature: apply panel background to tree
301
this._editorControlDisposables.add(this.viewDescriptorService.onDidChangeLocation(({ views }) => {
302
if (views.some(v => v.id === this.id)) {
303
tree.updateOptions({ overrideStyles: this.getLocationBasedColors().listOverrideStyles });
304
}
305
}));
306
307
// feature: filter on type - keep tree and menu in sync
308
this._editorControlDisposables.add(tree.onDidChangeFindMode(mode => this._outlineViewState.filterOnType = mode === TreeFindMode.Filter));
309
310
// feature: reveal outline selection in editor
311
// on change -> reveal/select defining range
312
let idPool = 0;
313
this._editorControlDisposables.add(tree.onDidOpen(async e => {
314
const myId = ++idPool;
315
const isDoubleClick = e.browserEvent?.type === 'dblclick';
316
if (!isDoubleClick) {
317
// workaround for https://github.com/microsoft/vscode/issues/206424
318
await timeout(150);
319
if (myId !== idPool) {
320
return;
321
}
322
}
323
await newOutline.reveal(e.element, e.editorOptions, e.sideBySide, isDoubleClick);
324
}));
325
// feature: reveal editor selection in outline
326
const revealActiveElement = () => {
327
if (!this._outlineViewState.followCursor || !newOutline.activeElement) {
328
return;
329
}
330
let item = newOutline.activeElement;
331
while (item) {
332
const top = tree.getRelativeTop(item);
333
if (top === null) {
334
// not visible -> reveal
335
tree.reveal(item, 0.5);
336
}
337
if (tree.getRelativeTop(item) !== null) {
338
tree.setFocus([item]);
339
tree.setSelection([item]);
340
break;
341
}
342
// STILL not visible -> try parent
343
item = tree.getParentElement(item);
344
}
345
};
346
revealActiveElement();
347
this._editorControlDisposables.add(newOutline.onDidChange(revealActiveElement));
348
349
// feature: update view when user state changes
350
this._editorControlDisposables.add(this._outlineViewState.onDidChange((e: { followCursor?: boolean; sortBy?: boolean; filterOnType?: boolean }) => {
351
this._outlineViewState.persist(this._storageService);
352
if (e.filterOnType) {
353
tree.findMode = this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight;
354
}
355
if (e.followCursor) {
356
revealActiveElement();
357
}
358
if (e.sortBy) {
359
sorter.order = this._outlineViewState.sortBy;
360
tree.resort();
361
}
362
}));
363
364
// feature: expand all nodes when filtering (not when finding)
365
let viewState: AbstractTreeViewState | undefined;
366
this._editorControlDisposables.add(tree.onDidChangeFindPattern(pattern => {
367
if (tree.findMode === TreeFindMode.Highlight) {
368
return;
369
}
370
if (!viewState && pattern) {
371
viewState = tree.getViewState();
372
tree.expandAll();
373
} else if (!pattern && viewState) {
374
tree.setInput(tree.getInput()!, viewState);
375
viewState = undefined;
376
}
377
}));
378
379
// feature: update all-collapsed context key
380
const updateAllCollapsedCtx = () => {
381
this._ctxAllCollapsed.set(tree.getNode(null).children.every(node => !node.collapsible || node.collapsed));
382
};
383
this._editorControlDisposables.add(tree.onDidChangeCollapseState(updateAllCollapsedCtx));
384
this._editorControlDisposables.add(tree.onDidChangeModel(updateAllCollapsedCtx));
385
updateAllCollapsedCtx();
386
387
// last: set tree property and wire it up to one of our context keys
388
tree.layout(this._treeDimensions?.height, this._treeDimensions?.width);
389
this._tree = tree;
390
this._editorControlDisposables.add(toDisposable(() => {
391
tree.dispose();
392
this._tree = undefined;
393
}));
394
}
395
}
396
397