Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.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 { compareFileNames } from '../../../../base/common/comparers.js';
7
import { onUnexpectedError } from '../../../../base/common/errors.js';
8
import { Emitter, Event } from '../../../../base/common/event.js';
9
import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';
10
import * as glob from '../../../../base/common/glob.js';
11
import { IDisposable, DisposableStore, MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js';
12
import { posix, relative } from '../../../../base/common/path.js';
13
import { basename, dirname, isEqual } from '../../../../base/common/resources.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import './media/breadcrumbscontrol.css';
16
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
17
import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { WorkbenchDataTree, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';
20
import { breadcrumbsPickerBackground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js';
21
import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
22
import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from '../../labels.js';
23
import { BreadcrumbsConfig } from './breadcrumbs.js';
24
import { OutlineElement2, FileElement } from './breadcrumbsModel.js';
25
import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from '../../../../base/browser/ui/tree/tree.js';
26
import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from '../../../../base/browser/ui/list/list.js';
27
import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';
28
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
29
import { localize } from '../../../../nls.js';
30
import { IOutline, IOutlineComparator } from '../../../services/outline/browser/outline.js';
31
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
32
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
33
import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
34
35
interface ILayoutInfo {
36
maxHeight: number;
37
width: number;
38
arrowSize: number;
39
arrowOffset: number;
40
inputHeight: number;
41
}
42
43
type Tree<I, E> = WorkbenchDataTree<I, E, FuzzyScore> | WorkbenchAsyncDataTree<I, E, FuzzyScore>;
44
45
export interface SelectEvent {
46
target: any;
47
browserEvent: UIEvent;
48
}
49
50
export abstract class BreadcrumbsPicker {
51
52
protected readonly _disposables = new DisposableStore();
53
protected readonly _domNode: HTMLDivElement;
54
protected _arrow!: HTMLDivElement;
55
protected _treeContainer!: HTMLDivElement;
56
protected _tree!: Tree<any, any>;
57
protected _fakeEvent = new UIEvent('fakeEvent');
58
protected _layoutInfo!: ILayoutInfo;
59
60
protected readonly _onWillPickElement = new Emitter<void>();
61
readonly onWillPickElement: Event<void> = this._onWillPickElement.event;
62
63
private readonly _previewDispoables = new MutableDisposable();
64
65
constructor(
66
parent: HTMLElement,
67
protected resource: URI,
68
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
69
@IThemeService protected readonly _themeService: IThemeService,
70
@IConfigurationService protected readonly _configurationService: IConfigurationService,
71
) {
72
this._domNode = document.createElement('div');
73
this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';
74
parent.appendChild(this._domNode);
75
}
76
77
dispose(): void {
78
this._disposables.dispose();
79
this._previewDispoables.dispose();
80
this._onWillPickElement.dispose();
81
this._domNode.remove();
82
setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened...
83
}
84
85
async show(input: FileElement | OutlineElement2, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise<void> {
86
87
const theme = this._themeService.getColorTheme();
88
const color = theme.getColor(breadcrumbsPickerBackground);
89
90
this._arrow = document.createElement('div');
91
this._arrow.className = 'arrow';
92
this._arrow.style.borderColor = `transparent transparent ${color ? color.toString() : ''}`;
93
this._domNode.appendChild(this._arrow);
94
95
this._treeContainer = document.createElement('div');
96
this._treeContainer.style.background = color ? color.toString() : '';
97
this._treeContainer.style.paddingTop = '2px';
98
this._treeContainer.style.borderRadius = '3px';
99
this._treeContainer.style.boxShadow = `0 0 8px 2px ${this._themeService.getColorTheme().getColor(widgetShadow)}`;
100
this._treeContainer.style.border = `1px solid ${this._themeService.getColorTheme().getColor(widgetBorder)}`;
101
this._domNode.appendChild(this._treeContainer);
102
103
this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };
104
this._tree = this._createTree(this._treeContainer, input);
105
106
this._disposables.add(this._tree.onDidOpen(async e => {
107
const { element, editorOptions, sideBySide } = e;
108
const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide);
109
if (!didReveal) {
110
return;
111
}
112
}));
113
this._disposables.add(this._tree.onDidChangeFocus(e => {
114
this._previewDispoables.value = this._previewElement(e.elements[0]);
115
}));
116
this._disposables.add(this._tree.onDidChangeContentHeight(() => {
117
this._layout();
118
}));
119
120
this._domNode.focus();
121
try {
122
await this._setInput(input);
123
this._layout();
124
} catch (err) {
125
onUnexpectedError(err);
126
}
127
}
128
129
protected _layout(): void {
130
131
const headerHeight = 2 * this._layoutInfo.arrowSize;
132
const treeHeight = Math.min(this._layoutInfo.maxHeight - headerHeight, this._tree.contentHeight);
133
const totalHeight = treeHeight + headerHeight;
134
135
this._domNode.style.height = `${totalHeight}px`;
136
this._domNode.style.width = `${this._layoutInfo.width}px`;
137
this._arrow.style.top = `-${2 * this._layoutInfo.arrowSize}px`;
138
this._arrow.style.borderWidth = `${this._layoutInfo.arrowSize}px`;
139
this._arrow.style.marginLeft = `${this._layoutInfo.arrowOffset}px`;
140
this._treeContainer.style.height = `${treeHeight}px`;
141
this._treeContainer.style.width = `${this._layoutInfo.width}px`;
142
this._tree.layout(treeHeight, this._layoutInfo.width);
143
}
144
145
restoreViewState(): void { }
146
147
protected abstract _setInput(element: FileElement | OutlineElement2): Promise<void>;
148
protected abstract _createTree(container: HTMLElement, input: any): Tree<any, any>;
149
protected abstract _previewElement(element: any): IDisposable;
150
protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean>;
151
152
}
153
154
//#region - Files
155
156
class FileVirtualDelegate implements IListVirtualDelegate<IFileStat | IWorkspaceFolder> {
157
getHeight(_element: IFileStat | IWorkspaceFolder) {
158
return 22;
159
}
160
getTemplateId(_element: IFileStat | IWorkspaceFolder): string {
161
return 'FileStat';
162
}
163
}
164
165
class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceFolder | IFileStat | URI> {
166
getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string } {
167
if (URI.isUri(element)) {
168
return element.toString();
169
} else if (isWorkspace(element)) {
170
return element.id;
171
} else if (isWorkspaceFolder(element)) {
172
return element.uri.toString();
173
} else {
174
return element.resource.toString();
175
}
176
}
177
}
178
179
180
class FileDataSource implements IAsyncDataSource<IWorkspace | URI, IWorkspaceFolder | IFileStat> {
181
182
constructor(
183
@IFileService private readonly _fileService: IFileService,
184
) { }
185
186
hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean {
187
return URI.isUri(element)
188
|| isWorkspace(element)
189
|| isWorkspaceFolder(element)
190
|| element.isDirectory;
191
}
192
193
async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> {
194
if (isWorkspace(element)) {
195
return element.folders;
196
}
197
let uri: URI;
198
if (isWorkspaceFolder(element)) {
199
uri = element.uri;
200
} else if (URI.isUri(element)) {
201
uri = element;
202
} else {
203
uri = element.resource;
204
}
205
const stat = await this._fileService.resolve(uri);
206
return stat.children ?? [];
207
}
208
}
209
210
class FileRenderer implements ITreeRenderer<IFileStat | IWorkspaceFolder, FuzzyScore, IResourceLabel> {
211
212
readonly templateId: string = 'FileStat';
213
214
constructor(
215
private readonly _labels: ResourceLabels,
216
@IConfigurationService private readonly _configService: IConfigurationService,
217
) { }
218
219
220
renderTemplate(container: HTMLElement): IResourceLabel {
221
return this._labels.create(container, { supportHighlights: true });
222
}
223
224
renderElement(node: ITreeNode<IWorkspaceFolder | IFileStat, [number, number, number]>, index: number, templateData: IResourceLabel): void {
225
const fileDecorations = this._configService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
226
const { element } = node;
227
let resource: URI;
228
let fileKind: FileKind;
229
if (isWorkspaceFolder(element)) {
230
resource = element.uri;
231
fileKind = FileKind.ROOT_FOLDER;
232
} else {
233
resource = element.resource;
234
fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE;
235
}
236
templateData.setFile(resource, {
237
fileKind,
238
hidePath: true,
239
fileDecorations: fileDecorations,
240
matches: createMatches(node.filterData),
241
extraClasses: ['picker-item']
242
});
243
}
244
245
disposeTemplate(templateData: IResourceLabel): void {
246
templateData.dispose();
247
}
248
}
249
250
class FileNavigationLabelProvider implements IKeyboardNavigationLabelProvider<IWorkspaceFolder | IFileStat> {
251
252
getKeyboardNavigationLabel(element: IWorkspaceFolder | IFileStat): { toString(): string } {
253
return element.name;
254
}
255
}
256
257
class FileAccessibilityProvider implements IListAccessibilityProvider<IWorkspaceFolder | IFileStat> {
258
259
getWidgetAriaLabel(): string {
260
return localize('breadcrumbs', "Breadcrumbs");
261
}
262
263
getAriaLabel(element: IWorkspaceFolder | IFileStat): string | null {
264
return element.name;
265
}
266
}
267
268
class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {
269
270
private readonly _cachedExpressions = new Map<string, glob.ParsedExpression>();
271
private readonly _disposables = new DisposableStore();
272
273
constructor(
274
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
275
@IConfigurationService configService: IConfigurationService,
276
) {
277
const config = BreadcrumbsConfig.FileExcludes.bindTo(configService);
278
const update = () => {
279
_workspaceService.getWorkspace().folders.forEach(folder => {
280
const excludesConfig = config.getValue({ resource: folder.uri });
281
if (!excludesConfig) {
282
return;
283
}
284
// adjust patterns to be absolute in case they aren't
285
// free floating (**/)
286
const adjustedConfig: glob.IExpression = {};
287
for (const pattern in excludesConfig) {
288
if (typeof excludesConfig[pattern] !== 'boolean') {
289
continue;
290
}
291
const patternAbs = pattern.indexOf('**/') !== 0
292
? posix.join(folder.uri.path, pattern)
293
: pattern;
294
295
adjustedConfig[patternAbs] = excludesConfig[pattern];
296
}
297
this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));
298
});
299
};
300
update();
301
this._disposables.add(config);
302
this._disposables.add(config.onDidChange(update));
303
this._disposables.add(_workspaceService.onDidChangeWorkspaceFolders(update));
304
}
305
306
dispose(): void {
307
this._disposables.dispose();
308
}
309
310
filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {
311
if (isWorkspaceFolder(element)) {
312
// not a file
313
return true;
314
}
315
const folder = this._workspaceService.getWorkspaceFolder(element.resource);
316
if (!folder || !this._cachedExpressions.has(folder.uri.toString())) {
317
// no folder or no filer
318
return true;
319
}
320
321
const expression = this._cachedExpressions.get(folder.uri.toString())!;
322
return !expression(relative(folder.uri.path, element.resource.path), basename(element.resource));
323
}
324
}
325
326
327
export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {
328
compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
329
if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) {
330
return a.index - b.index;
331
}
332
if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) {
333
// same type -> compare on names
334
return compareFileNames(a.name, b.name);
335
} else if ((a as IFileStat).isDirectory) {
336
return -1;
337
} else {
338
return 1;
339
}
340
}
341
}
342
343
export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
344
345
constructor(
346
parent: HTMLElement,
347
resource: URI,
348
@IInstantiationService instantiationService: IInstantiationService,
349
@IThemeService themeService: IThemeService,
350
@IConfigurationService configService: IConfigurationService,
351
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
352
@IEditorService private readonly _editorService: IEditorService,
353
) {
354
super(parent, resource, instantiationService, themeService, configService);
355
}
356
357
protected _createTree(container: HTMLElement) {
358
359
// tree icon theme specials
360
this._treeContainer.classList.add('file-icon-themable-tree');
361
this._treeContainer.classList.add('show-file-icons');
362
const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {
363
this._treeContainer.classList.toggle('align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);
364
this._treeContainer.classList.toggle('hide-arrows', fileIconTheme.hidesExplorerArrows === true);
365
};
366
this._disposables.add(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange));
367
onFileIconThemeChange(this._themeService.getFileIconTheme());
368
369
const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */);
370
this._disposables.add(labels);
371
372
return this._instantiationService.createInstance(
373
WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>,
374
'BreadcrumbsFilePicker',
375
container,
376
new FileVirtualDelegate(),
377
[this._instantiationService.createInstance(FileRenderer, labels)],
378
this._instantiationService.createInstance(FileDataSource),
379
{
380
multipleSelectionSupport: false,
381
sorter: new FileSorter(),
382
filter: this._instantiationService.createInstance(FileFilter),
383
identityProvider: new FileIdentityProvider(),
384
keyboardNavigationLabelProvider: new FileNavigationLabelProvider(),
385
accessibilityProvider: this._instantiationService.createInstance(FileAccessibilityProvider),
386
showNotFoundMessage: false,
387
overrideStyles: {
388
listBackground: breadcrumbsPickerBackground
389
},
390
});
391
}
392
393
protected async _setInput(element: FileElement | OutlineElement2): Promise<void> {
394
const { uri, kind } = (element as FileElement);
395
let input: IWorkspace | URI;
396
if (kind === FileKind.ROOT_FOLDER) {
397
input = this._workspaceService.getWorkspace();
398
} else {
399
input = dirname(uri);
400
}
401
402
const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;
403
await tree.setInput(input);
404
let focusElement: IWorkspaceFolder | IFileStat | undefined;
405
for (const { element } of tree.getNode().children) {
406
if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) {
407
focusElement = element;
408
break;
409
} else if (isEqual((element as IFileStat).resource, uri)) {
410
focusElement = element as IFileStat;
411
break;
412
}
413
}
414
if (focusElement) {
415
tree.reveal(focusElement, 0.5);
416
tree.setFocus([focusElement], this._fakeEvent);
417
}
418
tree.domFocus();
419
}
420
421
protected _previewElement(_element: any): IDisposable {
422
return Disposable.None;
423
}
424
425
protected async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
426
if (!isWorkspaceFolder(element) && element.isFile) {
427
this._onWillPickElement.fire();
428
await this._editorService.openEditor({ resource: element.resource, options }, sideBySide ? SIDE_GROUP : undefined);
429
return true;
430
}
431
return false;
432
}
433
}
434
//#endregion
435
436
//#region - Outline
437
438
class OutlineTreeSorter<E> implements ITreeSorter<E> {
439
440
private _order: 'name' | 'type' | 'position';
441
442
constructor(
443
private comparator: IOutlineComparator<E>,
444
uri: URI | undefined,
445
@ITextResourceConfigurationService configService: ITextResourceConfigurationService,
446
) {
447
this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder');
448
}
449
450
compare(a: E, b: E): number {
451
if (this._order === 'name') {
452
return this.comparator.compareByName(a, b);
453
} else if (this._order === 'type') {
454
return this.comparator.compareByType(a, b);
455
} else {
456
return this.comparator.compareByPosition(a, b);
457
}
458
}
459
}
460
461
export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {
462
463
protected _createTree(container: HTMLElement, input: OutlineElement2) {
464
465
const { config } = input.outline;
466
467
return this._instantiationService.createInstance(
468
WorkbenchDataTree<IOutline<any>, any, FuzzyScore>,
469
'BreadcrumbsOutlinePicker',
470
container,
471
config.delegate,
472
config.renderers,
473
config.treeDataSource,
474
{
475
...config.options,
476
sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined),
477
collapseByDefault: true,
478
expandOnlyOnTwistieClick: true,
479
multipleSelectionSupport: false,
480
showNotFoundMessage: false
481
}
482
);
483
}
484
485
protected _setInput(input: OutlineElement2): Promise<void> {
486
487
const viewState = input.outline.captureViewState();
488
this.restoreViewState = () => { viewState.dispose(); };
489
490
const tree = this._tree as WorkbenchDataTree<IOutline<any>, any, FuzzyScore>;
491
492
tree.setInput(input.outline);
493
if (input.element !== input.outline) {
494
tree.reveal(input.element, 0.5);
495
tree.setFocus([input.element], this._fakeEvent);
496
}
497
tree.domFocus();
498
499
return Promise.resolve();
500
}
501
502
protected _previewElement(element: any): IDisposable {
503
const outline: IOutline<any> = this._tree.getInput();
504
return outline.preview(element);
505
}
506
507
protected async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
508
this._onWillPickElement.fire();
509
const outline: IOutline<any> = this._tree.getInput();
510
await outline.reveal(element, options, sideBySide, false);
511
return true;
512
}
513
}
514
515
//#endregion
516
517