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
5297 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, FileSystemProviderCapabilities, 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: unknown;
47
browserEvent: UIEvent;
48
}
49
50
export abstract class BreadcrumbsPicker<TInput, TElement> {
51
52
protected readonly _disposables = new DisposableStore();
53
protected readonly _domNode: HTMLDivElement;
54
protected _arrow!: HTMLDivElement;
55
protected _treeContainer!: HTMLDivElement;
56
protected _tree!: Tree<TInput, TElement>;
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: unknown): Tree<TInput, TElement>;
149
protected abstract _previewElement(element: unknown): IDisposable;
150
protected abstract _revealElement(element: unknown, 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
@IFileService fileService: IFileService,
277
) {
278
const config = BreadcrumbsConfig.FileExcludes.bindTo(configService);
279
const update = () => {
280
_workspaceService.getWorkspace().folders.forEach(folder => {
281
const excludesConfig = config.getValue({ resource: folder.uri });
282
if (!excludesConfig) {
283
return;
284
}
285
// adjust patterns to be absolute in case they aren't
286
// free floating (**/)
287
const adjustedConfig: glob.IExpression = {};
288
for (const pattern in excludesConfig) {
289
if (typeof excludesConfig[pattern] !== 'boolean') {
290
continue;
291
}
292
const patternAbs = pattern.indexOf('**/') !== 0
293
? posix.join(folder.uri.path, pattern)
294
: pattern;
295
296
adjustedConfig[patternAbs] = excludesConfig[pattern];
297
}
298
const ignoreCase = !fileService.hasCapability(folder.uri, FileSystemProviderCapabilities.PathCaseSensitive);
299
this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig, { ignoreCase }));
300
});
301
};
302
update();
303
this._disposables.add(config);
304
this._disposables.add(config.onDidChange(update));
305
this._disposables.add(_workspaceService.onDidChangeWorkspaceFolders(update));
306
}
307
308
dispose(): void {
309
this._disposables.dispose();
310
}
311
312
filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {
313
if (isWorkspaceFolder(element)) {
314
// not a file
315
return true;
316
}
317
const folder = this._workspaceService.getWorkspaceFolder(element.resource);
318
if (!folder || !this._cachedExpressions.has(folder.uri.toString())) {
319
// no folder or no filer
320
return true;
321
}
322
323
const expression = this._cachedExpressions.get(folder.uri.toString())!;
324
return !expression(relative(folder.uri.path, element.resource.path), basename(element.resource));
325
}
326
}
327
328
329
export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {
330
compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
331
if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) {
332
return a.index - b.index;
333
}
334
if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) {
335
// same type -> compare on names
336
return compareFileNames(a.name, b.name);
337
} else if ((a as IFileStat).isDirectory) {
338
return -1;
339
} else {
340
return 1;
341
}
342
}
343
}
344
345
export class BreadcrumbsFilePicker extends BreadcrumbsPicker<IWorkspace | URI, IWorkspaceFolder | IFileStat> {
346
347
constructor(
348
parent: HTMLElement,
349
resource: URI,
350
@IInstantiationService instantiationService: IInstantiationService,
351
@IThemeService themeService: IThemeService,
352
@IConfigurationService configService: IConfigurationService,
353
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
354
@IEditorService private readonly _editorService: IEditorService,
355
) {
356
super(parent, resource, instantiationService, themeService, configService);
357
}
358
359
protected _createTree(container: HTMLElement) {
360
361
// tree icon theme specials
362
this._treeContainer.classList.add('file-icon-themable-tree');
363
this._treeContainer.classList.add('show-file-icons');
364
const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {
365
this._treeContainer.classList.toggle('align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);
366
this._treeContainer.classList.toggle('hide-arrows', fileIconTheme.hidesExplorerArrows === true);
367
};
368
this._disposables.add(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange));
369
onFileIconThemeChange(this._themeService.getFileIconTheme());
370
371
const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */);
372
this._disposables.add(labels);
373
374
return this._instantiationService.createInstance(
375
WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>,
376
'BreadcrumbsFilePicker',
377
container,
378
new FileVirtualDelegate(),
379
[this._instantiationService.createInstance(FileRenderer, labels)],
380
this._instantiationService.createInstance(FileDataSource),
381
{
382
multipleSelectionSupport: false,
383
sorter: new FileSorter(),
384
filter: this._instantiationService.createInstance(FileFilter),
385
identityProvider: new FileIdentityProvider(),
386
keyboardNavigationLabelProvider: new FileNavigationLabelProvider(),
387
accessibilityProvider: this._instantiationService.createInstance(FileAccessibilityProvider),
388
showNotFoundMessage: false,
389
overrideStyles: {
390
listBackground: breadcrumbsPickerBackground
391
},
392
});
393
}
394
395
protected async _setInput(element: FileElement | OutlineElement2): Promise<void> {
396
const { uri, kind } = (element as FileElement);
397
let input: IWorkspace | URI;
398
if (kind === FileKind.ROOT_FOLDER) {
399
input = this._workspaceService.getWorkspace();
400
} else {
401
input = dirname(uri);
402
}
403
404
const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;
405
await tree.setInput(input);
406
let focusElement: IWorkspaceFolder | IFileStat | undefined;
407
for (const { element } of tree.getNode().children) {
408
if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) {
409
focusElement = element;
410
break;
411
} else if (isEqual((element as IFileStat).resource, uri)) {
412
focusElement = element as IFileStat;
413
break;
414
}
415
}
416
if (focusElement) {
417
tree.reveal(focusElement, 0.5);
418
tree.setFocus([focusElement], this._fakeEvent);
419
}
420
tree.domFocus();
421
}
422
423
protected _previewElement(_element: unknown): IDisposable {
424
return Disposable.None;
425
}
426
427
protected async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
428
if (!isWorkspaceFolder(element) && element.isFile) {
429
this._onWillPickElement.fire();
430
await this._editorService.openEditor({ resource: element.resource, options }, sideBySide ? SIDE_GROUP : undefined);
431
return true;
432
}
433
return false;
434
}
435
}
436
//#endregion
437
438
//#region - Outline
439
440
class OutlineTreeSorter<E> implements ITreeSorter<E> {
441
442
private _order: 'name' | 'type' | 'position';
443
444
constructor(
445
private comparator: IOutlineComparator<E>,
446
uri: URI | undefined,
447
@ITextResourceConfigurationService configService: ITextResourceConfigurationService,
448
) {
449
this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder');
450
}
451
452
compare(a: E, b: E): number {
453
if (this._order === 'name') {
454
return this.comparator.compareByName(a, b);
455
} else if (this._order === 'type') {
456
return this.comparator.compareByType(a, b);
457
} else {
458
return this.comparator.compareByPosition(a, b);
459
}
460
}
461
}
462
463
export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker<IOutline<unknown>, unknown> {
464
465
protected _createTree(container: HTMLElement, input: OutlineElement2) {
466
467
const { config } = input.outline;
468
469
return this._instantiationService.createInstance(
470
WorkbenchDataTree<IOutline<unknown>, unknown, FuzzyScore>,
471
'BreadcrumbsOutlinePicker',
472
container,
473
config.delegate,
474
config.renderers,
475
config.treeDataSource,
476
{
477
...config.options,
478
sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined),
479
collapseByDefault: true,
480
expandOnlyOnTwistieClick: true,
481
multipleSelectionSupport: false,
482
showNotFoundMessage: false
483
}
484
);
485
}
486
487
protected _setInput(input: OutlineElement2): Promise<void> {
488
489
const viewState = input.outline.captureViewState();
490
this.restoreViewState = () => { viewState.dispose(); };
491
492
const tree = this._tree as WorkbenchDataTree<IOutline<unknown>, unknown, FuzzyScore>;
493
494
tree.setInput(input.outline);
495
if (input.element !== input.outline) {
496
tree.reveal(input.element, 0.5);
497
tree.setFocus([input.element], this._fakeEvent);
498
}
499
tree.domFocus();
500
501
return Promise.resolve();
502
}
503
504
protected _previewElement(element: unknown): IDisposable {
505
const outline: IOutline<unknown> = this._tree.getInput()!;
506
return outline.preview(element);
507
}
508
509
protected async _revealElement(element: unknown, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
510
this._onWillPickElement.fire();
511
const outline: IOutline<unknown> = this._tree.getInput()!;
512
await outline.reveal(element, options, sideBySide, false);
513
return true;
514
}
515
}
516
517
//#endregion
518
519