Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/views/treeView.ts
5260 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 { DataTransfers, IDragAndDropData } from '../../../../base/browser/dnd.js';
7
import * as DOM from '../../../../base/browser/dom.js';
8
import * as cssJs from '../../../../base/browser/cssValue.js';
9
import { IRenderedMarkdown, renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';
10
import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';
11
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
12
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
13
import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
14
import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/browser/ui/list/listView.js';
15
import { IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, ITreeNode, ITreeRenderer, TreeDragOverBubble } from '../../../../base/browser/ui/tree/tree.js';
16
import { CollapseAllAction } from '../../../../base/browser/ui/tree/treeDefaults.js';
17
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
18
import { timeout } from '../../../../base/common/async.js';
19
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
20
import { Codicon } from '../../../../base/common/codicons.js';
21
import { isCancellationError } from '../../../../base/common/errors.js';
22
import { Emitter, Event } from '../../../../base/common/event.js';
23
import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';
24
import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
25
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
26
import { Mimes } from '../../../../base/common/mime.js';
27
import { Schemas } from '../../../../base/common/network.js';
28
import { basename, dirname } from '../../../../base/common/resources.js';
29
import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';
30
import { isString } from '../../../../base/common/types.js';
31
import { URI } from '../../../../base/common/uri.js';
32
import { generateUuid } from '../../../../base/common/uuid.js';
33
import './media/views.css';
34
import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';
35
import { localize } from '../../../../nls.js';
36
import { createActionViewItem, getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
37
import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
38
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
39
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
40
import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyChangeEvent, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
41
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
42
import { FileKind } from '../../../../platform/files/common/files.js';
43
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
44
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
45
import { ILabelService } from '../../../../platform/label/common/label.js';
46
import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';
47
import { ILogService } from '../../../../platform/log/common/log.js';
48
import { INotificationService } from '../../../../platform/notification/common/notification.js';
49
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
50
import { IProgressService } from '../../../../platform/progress/common/progress.js';
51
import { Registry } from '../../../../platform/registry/common/platform.js';
52
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
53
import { isDark } from '../../../../platform/theme/common/theme.js';
54
import { FileThemeIcon, FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
55
import { ThemeIcon } from '../../../../base/common/themables.js';
56
import { fillEditorsDragData } from '../../dnd.js';
57
import { IResourceLabel, ResourceLabels } from '../../labels.js';
58
import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from '../editor/editorCommands.js';
59
import { getLocationBasedViewColors, IViewPaneOptions, ViewPane } from './viewPane.js';
60
import { IViewletViewOptions } from './viewsViewlet.js';
61
import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider, ITreeViewDescriptor, ITreeViewDragAndDropController, IViewBadge, IViewDescriptorService, IViewsRegistry, ResolvableTreeItem, TreeCommand, TreeItemCollapsibleState, TreeViewItemHandleArg, TreeViewPaneHandleArg, ViewContainer, ViewContainerLocation } from '../../../common/views.js';
62
import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js';
63
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
64
import { IHoverService, WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js';
65
import { CodeDataTransfers, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';
66
import { toExternalVSDataTransfer } from '../../../../editor/browser/dataTransfer.js';
67
import { CheckboxStateHandler, TreeItemCheckbox } from './checkbox.js';
68
import { setTimeout0 } from '../../../../base/common/platform.js';
69
import { AriaRole } from '../../../../base/browser/ui/aria/aria.js';
70
import { TelemetryTrustedValue } from '../../../../platform/telemetry/common/telemetryUtils.js';
71
import { ITreeViewsDnDService } from '../../../../editor/common/services/treeViewsDndService.js';
72
import { DraggedTreeItemsIdentifier } from '../../../../editor/common/services/treeViewsDnd.js';
73
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
74
import type { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';
75
import { parseLinkedText } from '../../../../base/common/linkedText.js';
76
import { Button } from '../../../../base/browser/ui/button/button.js';
77
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
78
import { IAccessibleViewInformationService } from '../../../services/accessibility/common/accessibleViewInformationService.js';
79
import { Command } from '../../../../editor/common/languages.js';
80
81
export class TreeViewPane extends ViewPane {
82
83
protected readonly treeView: ITreeView;
84
private _container: HTMLElement | undefined;
85
private _actionRunner: MultipleSelectionActionRunner;
86
87
constructor(
88
options: IViewletViewOptions,
89
@IKeybindingService keybindingService: IKeybindingService,
90
@IContextMenuService contextMenuService: IContextMenuService,
91
@IConfigurationService configurationService: IConfigurationService,
92
@IContextKeyService contextKeyService: IContextKeyService,
93
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
94
@IInstantiationService instantiationService: IInstantiationService,
95
@IOpenerService openerService: IOpenerService,
96
@IThemeService themeService: IThemeService,
97
@INotificationService notificationService: INotificationService,
98
@IHoverService hoverService: IHoverService,
99
@IAccessibleViewInformationService accessibleViewService: IAccessibleViewInformationService,
100
) {
101
super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle, donotForwardArgs: false }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService, accessibleViewService);
102
const { treeView } = (<ITreeViewDescriptor>Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).getView(options.id));
103
this.treeView = treeView;
104
this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this));
105
this._register(this.treeView.onDidChangeTitle((newTitle) => this.updateTitle(newTitle)));
106
this._register(this.treeView.onDidChangeDescription((newDescription) => this.updateTitleDescription(newDescription)));
107
this._register(toDisposable(() => {
108
if (this._container && this.treeView.container && (this._container === this.treeView.container)) {
109
this.treeView.setVisibility(false);
110
}
111
}));
112
this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility()));
113
this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire()));
114
if (options.title !== this.treeView.title) {
115
this.updateTitle(this.treeView.title);
116
}
117
if (options.titleDescription !== this.treeView.description) {
118
this.updateTitleDescription(this.treeView.description);
119
}
120
this._actionRunner = this._register(new MultipleSelectionActionRunner(notificationService, () => this.treeView.getSelection()));
121
122
this.updateTreeVisibility();
123
}
124
125
override focus(): void {
126
super.focus();
127
this.treeView.focus();
128
}
129
130
protected override renderBody(container: HTMLElement): void {
131
this._container = container;
132
super.renderBody(container);
133
this.renderTreeView(container);
134
}
135
136
override shouldShowWelcome(): boolean {
137
return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && ((this.treeView.message === undefined) || (this.treeView.message === ''));
138
}
139
140
protected override layoutBody(height: number, width: number): void {
141
super.layoutBody(height, width);
142
this.layoutTreeView(height, width);
143
}
144
145
override getOptimalWidth(): number {
146
return this.treeView.getOptimalWidth();
147
}
148
149
protected renderTreeView(container: HTMLElement): void {
150
this.treeView.show(container);
151
}
152
153
protected layoutTreeView(height: number, width: number): void {
154
this.treeView.layout(height, width);
155
}
156
157
private updateTreeVisibility(): void {
158
this.treeView.setVisibility(this.isBodyVisible());
159
}
160
161
override getActionRunner() {
162
return this._actionRunner;
163
}
164
165
override getActionsContext(): TreeViewPaneHandleArg {
166
return { $treeViewId: this.id, $focusedTreeItem: true, $selectedTreeItems: true };
167
}
168
169
}
170
171
class Root implements ITreeItem {
172
label = { label: 'root' };
173
handle = '0';
174
parentHandle: string | undefined = undefined;
175
collapsibleState = TreeItemCollapsibleState.Expanded;
176
children: ITreeItem[] | undefined = undefined;
177
}
178
179
function commandPreconditions(commandId: string): ContextKeyExpression | undefined {
180
const command = CommandsRegistry.getCommand(commandId);
181
if (command) {
182
const commandAction = MenuRegistry.getCommand(command.id);
183
return commandAction?.precondition;
184
}
185
return undefined;
186
}
187
188
function isTreeCommandEnabled(treeCommand: TreeCommand | Command, contextKeyService: IContextKeyService): boolean {
189
const commandId: string = (treeCommand as TreeCommand).originalId ? (treeCommand as TreeCommand).originalId! : treeCommand.id;
190
const precondition = commandPreconditions(commandId);
191
if (precondition) {
192
return contextKeyService.contextMatchesRules(precondition);
193
}
194
195
return true;
196
}
197
198
interface RenderedMessage { element: HTMLElement; disposables: DisposableStore }
199
200
function isRenderedMessageValue(messageValue: string | RenderedMessage | undefined): messageValue is RenderedMessage {
201
return !!messageValue && typeof messageValue !== 'string' && !!messageValue.element && !!messageValue.disposables;
202
}
203
204
const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data.");
205
206
export const RawCustomTreeViewContextKey = new RawContextKey<boolean>('customTreeView', false);
207
208
class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }
209
210
abstract class AbstractTreeView extends Disposable implements ITreeView {
211
212
private isVisible: boolean = false;
213
private _hasIconForParentNode = false;
214
private _hasIconForLeafNode = false;
215
216
private collapseAllContextKey: RawContextKey<boolean> | undefined;
217
private collapseAllContext: IContextKey<boolean> | undefined;
218
private collapseAllToggleContextKey: RawContextKey<boolean> | undefined;
219
private collapseAllToggleContext: IContextKey<boolean> | undefined;
220
private refreshContextKey: RawContextKey<boolean> | undefined;
221
private refreshContext: IContextKey<boolean> | undefined;
222
223
private focused: boolean = false;
224
private domNode!: HTMLElement;
225
private treeContainer: HTMLElement | undefined;
226
private _messageValue: string | { element: HTMLElement; disposables: DisposableStore } | undefined;
227
private _canSelectMany: boolean = false;
228
private _manuallyManageCheckboxes: boolean = false;
229
private messageElement: HTMLElement | undefined;
230
private tree: Tree | undefined;
231
private treeLabels: ResourceLabels | undefined;
232
private treeViewDnd: CustomTreeViewDragAndDrop | undefined;
233
private _container: HTMLElement | undefined;
234
235
private root: ITreeItem;
236
private elementsToRefresh: ITreeItem[] = [];
237
private lastSelection: readonly ITreeItem[] = [];
238
private lastActive: ITreeItem;
239
240
private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
241
get onDidExpandItem(): Event<ITreeItem> { return this._onDidExpandItem.event; }
242
243
private readonly _onDidCollapseItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
244
get onDidCollapseItem(): Event<ITreeItem> { return this._onDidCollapseItem.event; }
245
246
private _onDidChangeSelectionAndFocus: Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._register(new Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }>());
247
get onDidChangeSelectionAndFocus(): Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }> { return this._onDidChangeSelectionAndFocus.event; }
248
249
private readonly _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
250
get onDidChangeVisibility(): Event<boolean> { return this._onDidChangeVisibility.event; }
251
252
private readonly _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
253
get onDidChangeActions(): Event<void> { return this._onDidChangeActions.event; }
254
255
private readonly _onDidChangeWelcomeState: Emitter<void> = this._register(new Emitter<void>());
256
get onDidChangeWelcomeState(): Event<void> { return this._onDidChangeWelcomeState.event; }
257
258
private readonly _onDidChangeTitle: Emitter<string> = this._register(new Emitter<string>());
259
get onDidChangeTitle(): Event<string> { return this._onDidChangeTitle.event; }
260
261
private readonly _onDidChangeDescription: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
262
get onDidChangeDescription(): Event<string | undefined> { return this._onDidChangeDescription.event; }
263
264
private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
265
get onDidChangeCheckboxState(): Event<readonly ITreeItem[]> { return this._onDidChangeCheckboxState.event; }
266
267
private readonly _onDidCompleteRefresh: Emitter<void> = this._register(new Emitter<void>());
268
269
constructor(
270
readonly id: string,
271
private _title: string,
272
@IThemeService private readonly themeService: IThemeService,
273
@IInstantiationService private readonly instantiationService: IInstantiationService,
274
@ICommandService private readonly commandService: ICommandService,
275
@IConfigurationService private readonly configurationService: IConfigurationService,
276
@IProgressService protected readonly progressService: IProgressService,
277
@IContextMenuService private readonly contextMenuService: IContextMenuService,
278
@IKeybindingService private readonly keybindingService: IKeybindingService,
279
@INotificationService private readonly notificationService: INotificationService,
280
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
281
@IHoverService private readonly hoverService: IHoverService,
282
@IContextKeyService private readonly contextKeyService: IContextKeyService,
283
@IActivityService private readonly activityService: IActivityService,
284
@ILogService private readonly logService: ILogService,
285
@IOpenerService private readonly openerService: IOpenerService,
286
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
287
) {
288
super();
289
this.root = new Root();
290
this.lastActive = this.root;
291
// Try not to add anything that could be costly to this constructor. It gets called once per tree view
292
// during startup, and anything added here can affect performance.
293
}
294
295
private _isInitialized: boolean = false;
296
private initialize() {
297
if (this._isInitialized) {
298
return;
299
}
300
this._isInitialized = true;
301
302
// Remember when adding to this method that it isn't called until the view is visible, meaning that
303
// properties could be set and events could be fired before we're initialized and that this needs to be handled.
304
305
this.contextKeyService.bufferChangeEvents(() => {
306
this.initializeShowCollapseAllAction();
307
this.initializeCollapseAllToggle();
308
this.initializeShowRefreshAction();
309
});
310
311
this.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id);
312
if (this._dragAndDropController) {
313
this.treeViewDnd.controller = this._dragAndDropController;
314
}
315
316
this._register(this.configurationService.onDidChangeConfiguration(e => {
317
if (e.affectsConfiguration('explorer.decorations')) {
318
this.doRefresh([this.root]); /** soft refresh **/
319
}
320
}));
321
this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => {
322
if (views.some(v => v.id === this.id)) {
323
this.tree?.updateOptions({ overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles });
324
}
325
}));
326
this.registerActions();
327
328
this.create();
329
}
330
331
get viewContainer(): ViewContainer {
332
return this.viewDescriptorService.getViewContainerByViewId(this.id)!;
333
}
334
335
get viewLocation(): ViewContainerLocation {
336
return this.viewDescriptorService.getViewLocationById(this.id)!;
337
}
338
private _dragAndDropController: ITreeViewDragAndDropController | undefined;
339
get dragAndDropController(): ITreeViewDragAndDropController | undefined {
340
return this._dragAndDropController;
341
}
342
set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) {
343
this._dragAndDropController = dnd;
344
if (this.treeViewDnd) {
345
this.treeViewDnd.controller = dnd;
346
}
347
}
348
349
private _dataProvider: ITreeViewDataProvider | undefined;
350
get dataProvider(): ITreeViewDataProvider | undefined {
351
return this._dataProvider;
352
}
353
354
set dataProvider(dataProvider: ITreeViewDataProvider | undefined) {
355
if (dataProvider) {
356
if (this.visible) {
357
this.activate();
358
}
359
const self = this;
360
this._dataProvider = new class implements ITreeViewDataProvider {
361
private _isEmpty: boolean = true;
362
private _onDidChangeEmpty: Emitter<void> = new Emitter();
363
public onDidChangeEmpty: Event<void> = this._onDidChangeEmpty.event;
364
365
get isTreeEmpty(): boolean {
366
return this._isEmpty;
367
}
368
369
async getChildren(element?: ITreeItem): Promise<ITreeItem[] | undefined> {
370
const batches = await this.getChildrenBatch(element ? [element] : undefined);
371
return batches?.[0];
372
}
373
374
private updateEmptyState(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): void {
375
if ((nodes.length === 1) && (nodes[0] instanceof Root)) {
376
const oldEmpty = this._isEmpty;
377
this._isEmpty = (childrenGroups.length === 0) || (childrenGroups[0].length === 0);
378
if (oldEmpty !== this._isEmpty) {
379
this._onDidChangeEmpty.fire();
380
}
381
}
382
}
383
384
private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): ITreeItem[] {
385
if (childrenGroups.length === 0) {
386
return [];
387
}
388
const checkboxesUpdated: ITreeItem[] = [];
389
390
for (let i = 0; i < nodes.length; i++) {
391
const node = nodes[i];
392
const children = childrenGroups[i];
393
for (const child of children) {
394
child.parent = node;
395
if (!self.manuallyManageCheckboxes && (node?.checkbox?.isChecked === true) && (child.checkbox?.isChecked === false)) {
396
child.checkbox.isChecked = true;
397
checkboxesUpdated.push(child);
398
}
399
}
400
}
401
return checkboxesUpdated;
402
}
403
404
async getChildrenBatch(nodes?: ITreeItem[]): Promise<ITreeItem[][]> {
405
let childrenGroups: ITreeItem[][];
406
let checkboxesUpdated: ITreeItem[] = [];
407
if (nodes?.every((node): node is Required<ITreeItem & { children: ITreeItem[] }> => !!node.children)) {
408
childrenGroups = nodes.map(node => node.children);
409
} else {
410
nodes = nodes ?? [self.root];
411
const batchedChildren = await (nodes.length === 1 && nodes[0] instanceof Root ? doGetChildrenOrBatch(dataProvider, undefined) : doGetChildrenOrBatch(dataProvider, nodes));
412
for (let i = 0; i < nodes.length; i++) {
413
const node = nodes[i];
414
node.children = batchedChildren ? batchedChildren[i] : undefined;
415
}
416
childrenGroups = batchedChildren ?? [];
417
checkboxesUpdated = this.findCheckboxesUpdated(nodes, childrenGroups);
418
}
419
420
this.updateEmptyState(nodes, childrenGroups);
421
422
if (checkboxesUpdated.length > 0) {
423
self._onDidChangeCheckboxState.fire(checkboxesUpdated);
424
}
425
return childrenGroups;
426
}
427
};
428
if (this._dataProvider.onDidChangeEmpty) {
429
this._register(this._dataProvider.onDidChangeEmpty(() => {
430
this.updateCollapseAllToggle();
431
this._onDidChangeWelcomeState.fire();
432
}));
433
}
434
this.updateMessage();
435
this.refresh();
436
} else {
437
this._dataProvider = undefined;
438
this.treeDisposables.clear();
439
this.activated = false;
440
this.updateMessage();
441
}
442
443
this._onDidChangeWelcomeState.fire();
444
}
445
446
private _message: string | IMarkdownString | undefined;
447
get message(): string | IMarkdownString | undefined {
448
return this._message;
449
}
450
451
set message(message: string | IMarkdownString | undefined) {
452
this._message = message;
453
this.updateMessage();
454
this._onDidChangeWelcomeState.fire();
455
}
456
457
get title(): string {
458
return this._title;
459
}
460
461
set title(name: string) {
462
this._title = name;
463
if (this.tree) {
464
this.tree.ariaLabel = this._title;
465
}
466
this._onDidChangeTitle.fire(this._title);
467
}
468
469
private _description: string | undefined;
470
get description(): string | undefined {
471
return this._description;
472
}
473
474
set description(description: string | undefined) {
475
this._description = description;
476
this._onDidChangeDescription.fire(this._description);
477
}
478
479
private _badge: IViewBadge | undefined;
480
private readonly _activity = this._register(new MutableDisposable<IDisposable>());
481
482
get badge(): IViewBadge | undefined {
483
return this._badge;
484
}
485
486
set badge(badge: IViewBadge | undefined) {
487
488
if (this._badge?.value === badge?.value &&
489
this._badge?.tooltip === badge?.tooltip) {
490
return;
491
}
492
493
this._badge = badge;
494
if (badge) {
495
const activity = {
496
badge: new NumberBadge(badge.value, () => badge.tooltip),
497
priority: 50
498
};
499
this._activity.value = this.activityService.showViewActivity(this.id, activity);
500
} else {
501
this._activity.clear();
502
}
503
}
504
505
get canSelectMany(): boolean {
506
return this._canSelectMany;
507
}
508
509
set canSelectMany(canSelectMany: boolean) {
510
const oldCanSelectMany = this._canSelectMany;
511
this._canSelectMany = canSelectMany;
512
if (this._canSelectMany !== oldCanSelectMany) {
513
this.tree?.updateOptions({ multipleSelectionSupport: this.canSelectMany });
514
}
515
}
516
517
get manuallyManageCheckboxes(): boolean {
518
return this._manuallyManageCheckboxes;
519
}
520
521
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
522
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
523
}
524
525
get hasIconForParentNode(): boolean {
526
return this._hasIconForParentNode;
527
}
528
529
get hasIconForLeafNode(): boolean {
530
return this._hasIconForLeafNode;
531
}
532
533
get visible(): boolean {
534
return this.isVisible;
535
}
536
537
private initializeShowCollapseAllAction(startingValue: boolean = false) {
538
if (!this.collapseAllContext) {
539
this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the tree view with id {0} enables collapse all.", this.id));
540
this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);
541
}
542
return true;
543
}
544
545
get showCollapseAllAction(): boolean {
546
this.initializeShowCollapseAllAction();
547
return !!this.collapseAllContext?.get();
548
}
549
550
set showCollapseAllAction(showCollapseAllAction: boolean) {
551
this.initializeShowCollapseAllAction(showCollapseAllAction);
552
this.collapseAllContext?.set(showCollapseAllAction);
553
}
554
555
556
private initializeShowRefreshAction(startingValue: boolean = false) {
557
if (!this.refreshContext) {
558
this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, startingValue, localize('treeView.enableRefresh', "Whether the tree view with id {0} enables refresh.", this.id));
559
this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);
560
}
561
}
562
563
get showRefreshAction(): boolean {
564
this.initializeShowRefreshAction();
565
return !!this.refreshContext?.get();
566
}
567
568
set showRefreshAction(showRefreshAction: boolean) {
569
this.initializeShowRefreshAction(showRefreshAction);
570
this.refreshContext?.set(showRefreshAction);
571
}
572
573
private registerActions() {
574
const that = this;
575
this._register(registerAction2(class extends Action2 {
576
constructor() {
577
super({
578
id: `workbench.actions.treeView.${that.id}.refresh`,
579
title: localize('refresh', "Refresh"),
580
menu: {
581
id: MenuId.ViewTitle,
582
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.refreshContextKey),
583
group: 'navigation',
584
order: Number.MAX_SAFE_INTEGER - 1,
585
},
586
icon: Codicon.refresh
587
});
588
}
589
async run(): Promise<void> {
590
return that.refresh();
591
}
592
}));
593
this._register(registerAction2(class extends Action2 {
594
constructor() {
595
super({
596
id: `workbench.actions.treeView.${that.id}.collapseAll`,
597
title: localize('collapseAll', "Collapse All"),
598
menu: {
599
id: MenuId.ViewTitle,
600
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.collapseAllContextKey),
601
group: 'navigation',
602
order: Number.MAX_SAFE_INTEGER,
603
},
604
precondition: that.collapseAllToggleContextKey,
605
icon: Codicon.collapseAll
606
});
607
}
608
async run(): Promise<void> {
609
if (that.tree) {
610
return new CollapseAllAction<ITreeItem, ITreeItem, FuzzyScore>(that.tree, true).run();
611
}
612
}
613
}));
614
}
615
616
setVisibility(isVisible: boolean): void {
617
// Throughout setVisibility we need to check if the tree view's data provider still exists.
618
// This can happen because the `getChildren` call to the extension can return
619
// after the tree has been disposed.
620
621
this.initialize();
622
isVisible = !!isVisible;
623
if (this.isVisible === isVisible) {
624
return;
625
}
626
627
this.isVisible = isVisible;
628
629
if (this.tree) {
630
if (this.isVisible) {
631
DOM.show(this.tree.getHTMLElement());
632
} else {
633
DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it
634
}
635
636
if (this.isVisible && this.elementsToRefresh.length && this.dataProvider) {
637
this.doRefresh(this.elementsToRefresh);
638
this.elementsToRefresh = [];
639
}
640
}
641
642
setTimeout0(() => {
643
if (this.dataProvider) {
644
this._onDidChangeVisibility.fire(this.isVisible);
645
}
646
});
647
648
if (this.visible) {
649
this.activate();
650
}
651
}
652
653
protected activated: boolean = false;
654
protected abstract activate(): void;
655
656
focus(reveal: boolean = true, revealItem?: ITreeItem): void {
657
if (this.tree && this.root.children && this.root.children.length > 0) {
658
// Make sure the current selected element is revealed
659
const element = revealItem ?? this.tree.getSelection()[0];
660
if (element && reveal) {
661
this.tree.reveal(element, 0.5);
662
}
663
664
// Pass Focus to Viewer
665
this.tree.domFocus();
666
} else if (this.tree && this.treeContainer && !this.treeContainer.classList.contains('hide')) {
667
this.tree.domFocus();
668
} else {
669
this.domNode.focus();
670
}
671
}
672
673
show(container: HTMLElement): void {
674
this._container = container;
675
DOM.append(container, this.domNode);
676
}
677
678
private create() {
679
this.domNode = DOM.$('.tree-explorer-viewlet-tree-view');
680
this.messageElement = DOM.append(this.domNode, DOM.$('.message'));
681
this.updateMessage();
682
this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree'));
683
this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
684
const focusTracker = this._register(DOM.trackFocus(this.domNode));
685
this._register(focusTracker.onDidFocus(() => this.focused = true));
686
this._register(focusTracker.onDidBlur(() => this.focused = false));
687
}
688
689
private readonly treeDisposables: DisposableStore = this._register(new DisposableStore());
690
protected createTree() {
691
this.treeDisposables.clear();
692
const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
693
const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id));
694
this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this));
695
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
696
const aligner = this.treeDisposables.add(new Aligner(this.themeService, this.logService));
697
const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler());
698
const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes));
699
this.treeDisposables.add(renderer.onDidChangeCheckboxState(e => this._onDidChangeCheckboxState.fire(e)));
700
701
const widgetAriaLabel = this._title;
702
703
this.tree = this.treeDisposables.add(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer],
704
dataSource, {
705
identityProvider: new TreeViewIdentityProvider(),
706
accessibilityProvider: {
707
getAriaLabel(element: ITreeItem): string | null {
708
if (element.accessibilityInformation) {
709
return element.accessibilityInformation.label;
710
}
711
712
if (isString(element.tooltip)) {
713
return element.tooltip;
714
} else {
715
if (element.resourceUri && !element.label) {
716
// The custom tree has no good information on what should be used for the aria label.
717
// Allow the tree widget's default aria label to be used.
718
return null;
719
}
720
let buildAriaLabel: string = '';
721
if (element.label) {
722
const labelText = isMarkdownString(element.label.label) ? element.label.label.value : element.label.label;
723
buildAriaLabel += labelText + ' ';
724
}
725
if (element.description) {
726
buildAriaLabel += element.description;
727
}
728
return buildAriaLabel;
729
}
730
},
731
getRole(element: ITreeItem): AriaRole | undefined {
732
return element.accessibilityInformation?.role ?? 'treeitem';
733
},
734
getWidgetAriaLabel(): string {
735
return widgetAriaLabel;
736
}
737
},
738
keyboardNavigationLabelProvider: {
739
getKeyboardNavigationLabel: (item: ITreeItem) => {
740
if (item.label) {
741
return isMarkdownString(item.label.label) ? item.label.label.value : item.label.label;
742
}
743
return item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined;
744
}
745
},
746
expandOnlyOnTwistieClick: (e: ITreeItem) => {
747
return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';
748
},
749
collapseByDefault: (e: ITreeItem): boolean => {
750
return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
751
},
752
multipleSelectionSupport: this.canSelectMany,
753
dnd: this.treeViewDnd,
754
overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles
755
}));
756
757
this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e))));
758
759
this.treeDisposables.add(this.tree);
760
treeMenus.setContextKeyService(this.tree.contextKeyService);
761
aligner.tree = this.tree;
762
const actionRunner = this.treeDisposables.add(new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()));
763
renderer.actionRunner = actionRunner;
764
765
this.tree.contextKeyService.createKey<boolean>(this.id, true);
766
const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);
767
customTreeKey.set(true);
768
this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
769
770
this.treeDisposables.add(this.tree.onDidChangeSelection(e => {
771
this.lastSelection = e.elements;
772
this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive;
773
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
774
}));
775
this.treeDisposables.add(this.tree.onDidChangeFocus(e => {
776
if (e.elements.length && (e.elements[0] !== this.lastActive)) {
777
this.lastActive = e.elements[0];
778
this.lastSelection = this.tree?.getSelection() ?? this.lastSelection;
779
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
780
}
781
}));
782
this.treeDisposables.add(this.tree.onDidChangeCollapseState(e => {
783
if (!e.node.element) {
784
return;
785
}
786
787
const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element;
788
if (e.node.collapsed) {
789
this._onDidCollapseItem.fire(element);
790
} else {
791
this._onDidExpandItem.fire(element);
792
}
793
}));
794
this.tree.setInput(this.root).then(() => this.updateContentAreas());
795
796
this.treeDisposables.add(this.tree.onDidOpen(async (e) => {
797
if (!e.browserEvent) {
798
return;
799
}
800
if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {
801
return;
802
}
803
const selection = this.tree!.getSelection();
804
const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);
805
806
if (command && isTreeCommandEnabled(command, this.contextKeyService)) {
807
let args = command.arguments || [];
808
if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {
809
// Some commands owned by us should receive the
810
// `IOpenEvent` as context to open properly
811
args = [...args, e];
812
}
813
814
try {
815
await this.commandService.executeCommand(command.id, ...args);
816
} catch (err) {
817
this.notificationService.error(err);
818
}
819
}
820
}));
821
822
this.treeDisposables.add(treeMenus.onDidChange((changed) => {
823
if (this.tree?.hasNode(changed)) {
824
this.tree?.rerender(changed);
825
}
826
}));
827
}
828
829
private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {
830
let command = element?.command;
831
if (element && !command) {
832
if ((element instanceof ResolvableTreeItem) && element.hasResolve) {
833
await element.resolve(CancellationToken.None);
834
command = element.command;
835
}
836
}
837
return command;
838
}
839
840
841
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
842
this.hoverService.hideHover();
843
const node: ITreeItem | null = treeEvent.element;
844
if (node === null) {
845
return;
846
}
847
const event: UIEvent = treeEvent.browserEvent;
848
849
event.preventDefault();
850
event.stopPropagation();
851
852
this.tree!.setFocus([node]);
853
let selected = this.canSelectMany ? this.getSelection() : [];
854
if (!selected.find(item => item.handle === node.handle)) {
855
selected = [node];
856
}
857
858
const actions = treeMenus.getResourceContextActions(selected);
859
if (!actions.length) {
860
return;
861
}
862
this.contextMenuService.showContextMenu({
863
getAnchor: () => treeEvent.anchor,
864
865
getActions: () => actions,
866
867
getActionViewItem: (action) => {
868
const keybinding = this.keybindingService.lookupKeybinding(action.id);
869
if (keybinding) {
870
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
871
}
872
return undefined;
873
},
874
875
onHide: (wasCancelled?: boolean) => {
876
if (wasCancelled) {
877
this.tree!.domFocus();
878
}
879
},
880
881
getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg),
882
883
actionRunner
884
});
885
}
886
887
protected updateMessage(): void {
888
if (this._message) {
889
this.showMessage(this._message);
890
} else if (!this.dataProvider) {
891
this.showMessage(noDataProviderMessage);
892
} else {
893
this.hideMessage();
894
}
895
this.updateContentAreas();
896
}
897
898
private processMessage(message: IMarkdownString, disposables: DisposableStore): HTMLElement {
899
const lines = message.value.split('\n');
900
const result: (IRenderedMarkdown | HTMLElement)[] = [];
901
let hasFoundButton = false;
902
for (const line of lines) {
903
const linkedText = parseLinkedText(line);
904
905
if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') {
906
const node = linkedText.nodes[0];
907
const buttonContainer = document.createElement('div');
908
buttonContainer.classList.add('button-container');
909
const button = new Button(buttonContainer, { title: node.title, secondary: hasFoundButton, supportIcons: true, ...defaultButtonStyles });
910
button.label = node.label;
911
button.onDidClick(_ => {
912
this.openerService.open(node.href, { allowCommands: true });
913
}, null, disposables);
914
915
const href = URI.parse(node.href);
916
if (href.scheme === Schemas.command) {
917
const preConditions = commandPreconditions(href.path);
918
if (preConditions) {
919
button.enabled = this.contextKeyService.contextMatchesRules(preConditions);
920
disposables.add(this.contextKeyService.onDidChangeContext(e => {
921
if (e.affectsSome(new Set(preConditions.keys()))) {
922
button.enabled = this.contextKeyService.contextMatchesRules(preConditions);
923
}
924
}));
925
}
926
}
927
928
disposables.add(button);
929
hasFoundButton = true;
930
result.push(buttonContainer);
931
} else {
932
hasFoundButton = false;
933
const rendered = this.markdownRendererService.render(new MarkdownString(line, { isTrusted: message.isTrusted, supportThemeIcons: message.supportThemeIcons, supportHtml: message.supportHtml }));
934
result.push(rendered.element);
935
disposables.add(rendered);
936
}
937
}
938
939
const container = document.createElement('div');
940
container.classList.add('rendered-message');
941
for (const child of result) {
942
if (DOM.isHTMLElement(child)) {
943
container.appendChild(child);
944
} else {
945
container.appendChild(child.element);
946
}
947
}
948
return container;
949
}
950
951
private showMessage(message: string | IMarkdownString): void {
952
if (isRenderedMessageValue(this._messageValue)) {
953
this._messageValue.disposables.dispose();
954
}
955
if (isMarkdownString(message)) {
956
const disposables = new DisposableStore();
957
const renderedMessage = this.processMessage(message, disposables);
958
this._messageValue = { element: renderedMessage, disposables };
959
} else {
960
this._messageValue = message;
961
}
962
if (!this.messageElement) {
963
return;
964
}
965
this.messageElement.classList.remove('hide');
966
this.resetMessageElement();
967
if (typeof this._messageValue === 'string' && !isFalsyOrWhitespace(this._messageValue)) {
968
this.messageElement.textContent = this._messageValue;
969
} else if (isRenderedMessageValue(this._messageValue)) {
970
this.messageElement.appendChild(this._messageValue.element);
971
}
972
this.layout(this._height, this._width);
973
}
974
975
private hideMessage(): void {
976
this.resetMessageElement();
977
this.messageElement?.classList.add('hide');
978
this.layout(this._height, this._width);
979
}
980
981
private resetMessageElement(): void {
982
if (this.messageElement) {
983
DOM.clearNode(this.messageElement);
984
}
985
}
986
987
private _height: number = 0;
988
private _width: number = 0;
989
layout(height: number, width: number) {
990
if (height && width && this.messageElement && this.treeContainer) {
991
this._height = height;
992
this._width = width;
993
const treeHeight = height - DOM.getTotalHeight(this.messageElement);
994
this.treeContainer.style.height = treeHeight + 'px';
995
this.tree?.layout(treeHeight, width);
996
}
997
}
998
999
getOptimalWidth(): number {
1000
if (this.tree) {
1001
const parentNode = this.tree.getHTMLElement();
1002
// eslint-disable-next-line no-restricted-syntax
1003
const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
1004
return DOM.getLargestChildWidth(parentNode, childNodes);
1005
}
1006
return 0;
1007
}
1008
1009
private updateCheckboxes(elements: readonly ITreeItem[]): ITreeItem[] {
1010
return setCascadingCheckboxUpdates(elements);
1011
}
1012
1013
async refresh(elements?: readonly ITreeItem[], checkboxes?: readonly ITreeItem[]): Promise<void> {
1014
if (this.dataProvider && this.tree) {
1015
if (this.refreshing) {
1016
await Event.toPromise(this._onDidCompleteRefresh.event);
1017
}
1018
if (!elements) {
1019
elements = [this.root];
1020
// remove all waiting elements to refresh if root is asked to refresh
1021
this.elementsToRefresh = [];
1022
}
1023
for (const element of elements) {
1024
element.children = undefined; // reset children
1025
}
1026
if (this.isVisible) {
1027
const affectedElements = this.updateCheckboxes(checkboxes ?? []);
1028
return this.doRefresh(elements.concat(affectedElements));
1029
} else {
1030
if (this.elementsToRefresh.length) {
1031
const seen: Set<string> = new Set<string>();
1032
this.elementsToRefresh.forEach(element => seen.add(element.handle));
1033
for (const element of elements) {
1034
if (!seen.has(element.handle)) {
1035
this.elementsToRefresh.push(element);
1036
}
1037
}
1038
} else {
1039
this.elementsToRefresh.push(...elements);
1040
}
1041
}
1042
}
1043
return undefined;
1044
}
1045
1046
async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {
1047
const tree = this.tree;
1048
if (!tree) {
1049
return;
1050
}
1051
try {
1052
itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
1053
for (const element of itemOrItems) {
1054
await tree.expand(element, false);
1055
}
1056
} catch (e) {
1057
// The extension could have changed the tree during the reveal.
1058
// Because of that, we ignore errors.
1059
}
1060
}
1061
1062
isCollapsed(item: ITreeItem): boolean {
1063
return !!this.tree?.isCollapsed(item);
1064
}
1065
1066
setSelection(items: ITreeItem[]): void {
1067
this.tree?.setSelection(items);
1068
}
1069
1070
getSelection(): ITreeItem[] {
1071
return this.tree?.getSelection() ?? [];
1072
}
1073
1074
setFocus(item?: ITreeItem): void {
1075
if (this.tree) {
1076
if (item) {
1077
this.focus(true, item);
1078
this.tree.setFocus([item]);
1079
} else if (this.tree.getFocus().length === 0) {
1080
this.tree.setFocus([]);
1081
}
1082
}
1083
}
1084
1085
async reveal(item: ITreeItem): Promise<void> {
1086
if (this.tree) {
1087
return this.tree.reveal(item);
1088
}
1089
}
1090
1091
private refreshing: boolean = false;
1092
private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {
1093
const tree = this.tree;
1094
if (tree && this.visible) {
1095
this.refreshing = true;
1096
const oldSelection = tree.getSelection();
1097
try {
1098
await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
1099
} catch (e) {
1100
// When multiple calls are made to refresh the tree in quick succession,
1101
// we can get a "Tree element not found" error. This is expected.
1102
// Ideally this is fixable, so log instead of ignoring so the error is preserved.
1103
this.logService.error(e);
1104
}
1105
const newSelection = tree.getSelection();
1106
if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {
1107
this.lastSelection = newSelection;
1108
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
1109
}
1110
this.refreshing = false;
1111
this._onDidCompleteRefresh.fire();
1112
this.updateContentAreas();
1113
if (this.focused) {
1114
this.focus(false);
1115
}
1116
this.updateCollapseAllToggle();
1117
}
1118
}
1119
1120
private initializeCollapseAllToggle() {
1121
if (!this.collapseAllToggleContext) {
1122
this.collapseAllToggleContextKey = new RawContextKey<boolean>(`treeView.${this.id}.toggleCollapseAll`, false, localize('treeView.toggleCollapseAll', "Whether collapse all is toggled for the tree view with id {0}.", this.id));
1123
this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);
1124
}
1125
}
1126
1127
private updateCollapseAllToggle() {
1128
if (this.showCollapseAllAction) {
1129
this.initializeCollapseAllToggle();
1130
this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&
1131
this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));
1132
}
1133
}
1134
1135
private updateContentAreas(): void {
1136
const isTreeEmpty = !this.root.children || this.root.children.length === 0;
1137
// Hide tree container only when there is a message and tree is empty and not refreshing
1138
if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {
1139
// If there's a dnd controller then hiding the tree prevents it from being dragged into.
1140
if (!this.dragAndDropController) {
1141
this.treeContainer.classList.add('hide');
1142
}
1143
this.domNode.setAttribute('tabindex', '0');
1144
} else if (this.treeContainer) {
1145
this.treeContainer.classList.remove('hide');
1146
if (this.domNode === DOM.getActiveElement()) {
1147
this.focus();
1148
}
1149
this.domNode.removeAttribute('tabindex');
1150
}
1151
}
1152
1153
get container(): HTMLElement | undefined {
1154
return this._container;
1155
}
1156
}
1157
1158
class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {
1159
getId(element: ITreeItem): { toString(): string } {
1160
return element.handle;
1161
}
1162
}
1163
1164
class TreeViewDelegate implements IListVirtualDelegate<ITreeItem> {
1165
1166
getHeight(element: ITreeItem): number {
1167
return TreeRenderer.ITEM_HEIGHT;
1168
}
1169
1170
getTemplateId(element: ITreeItem): string {
1171
return TreeRenderer.TREE_TEMPLATE_ID;
1172
}
1173
}
1174
1175
async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<ITreeItem[][] | undefined> {
1176
if (dataProvider.getChildrenBatch) {
1177
return dataProvider.getChildrenBatch(nodes);
1178
} else {
1179
if (nodes) {
1180
return Promise.all(nodes.map(node => dataProvider.getChildren(node).then(children => children ?? [])));
1181
} else {
1182
return [await dataProvider.getChildren()].filter(children => children !== undefined);
1183
}
1184
}
1185
}
1186
1187
class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {
1188
1189
constructor(
1190
private treeView: ITreeView,
1191
private withProgress: <T>(task: Promise<T>) => Promise<T>
1192
) {
1193
}
1194
1195
hasChildren(element: ITreeItem): boolean {
1196
return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None);
1197
}
1198
1199
private batch: ITreeItem[] | undefined;
1200
private batchPromise: Promise<ITreeItem[][] | undefined> | undefined;
1201
async getChildren(element: ITreeItem): Promise<ITreeItem[]> {
1202
const dataProvider = this.treeView.dataProvider;
1203
if (!dataProvider) {
1204
return [];
1205
}
1206
if (this.batch === undefined) {
1207
this.batch = [element];
1208
this.batchPromise = undefined;
1209
} else {
1210
this.batch.push(element);
1211
}
1212
const indexInBatch = this.batch.length - 1;
1213
return new Promise<ITreeItem[]>((resolve, reject) => {
1214
setTimeout(async () => {
1215
const batch = this.batch;
1216
this.batch = undefined;
1217
if (!this.batchPromise) {
1218
this.batchPromise = this.withProgress(doGetChildrenOrBatch(dataProvider, batch));
1219
}
1220
try {
1221
const result = await this.batchPromise;
1222
resolve((result && (indexInBatch < result.length)) ? result[indexInBatch] : []);
1223
} catch (e) {
1224
if (!(<string>e.message).startsWith('Bad progress location:')) {
1225
reject(e);
1226
}
1227
}
1228
}, 0);
1229
});
1230
}
1231
}
1232
1233
interface ITreeExplorerTemplateData {
1234
readonly container: HTMLElement;
1235
readonly resourceLabel: IResourceLabel;
1236
readonly icon: HTMLElement;
1237
readonly checkboxContainer: HTMLElement;
1238
checkbox?: TreeItemCheckbox;
1239
readonly actionBar: ActionBar;
1240
}
1241
1242
class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {
1243
static readonly ITEM_HEIGHT = 22;
1244
static readonly TREE_TEMPLATE_ID = 'treeExplorer';
1245
1246
private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
1247
readonly onDidChangeCheckboxState: Event<readonly ITreeItem[]> = this._onDidChangeCheckboxState.event;
1248
1249
private _onDidChangeMenuContext: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
1250
readonly onDidChangeMenuContext: Event<readonly ITreeItem[]> = this._onDidChangeMenuContext.event;
1251
1252
private _actionRunner: MultipleSelectionActionRunner | undefined;
1253
private _hoverDelegate: IHoverDelegate;
1254
private _hasCheckbox: boolean = false;
1255
private _renderedElements = new Map<string, { original: ITreeNode<ITreeItem, FuzzyScore>; rendered: ITreeExplorerTemplateData }[]>(); // tree item handle to template data
1256
1257
constructor(
1258
private treeViewId: string,
1259
private menus: TreeMenus,
1260
private labels: ResourceLabels,
1261
private actionViewItemProvider: IActionViewItemProvider,
1262
private aligner: Aligner,
1263
private checkboxStateHandler: CheckboxStateHandler,
1264
private readonly manuallyManageCheckboxes: () => boolean,
1265
@IThemeService private readonly themeService: IThemeService,
1266
@IConfigurationService private readonly configurationService: IConfigurationService,
1267
@ILabelService private readonly labelService: ILabelService,
1268
@IContextKeyService private readonly contextKeyService: IContextKeyService,
1269
@IHoverService private readonly hoverService: IHoverService,
1270
@IInstantiationService instantiationService: IInstantiationService,
1271
) {
1272
super();
1273
this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));
1274
this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));
1275
this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));
1276
this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {
1277
this.updateCheckboxes(items);
1278
}));
1279
this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e)));
1280
}
1281
1282
get templateId(): string {
1283
return TreeRenderer.TREE_TEMPLATE_ID;
1284
}
1285
1286
set actionRunner(actionRunner: MultipleSelectionActionRunner) {
1287
this._actionRunner = actionRunner;
1288
}
1289
1290
renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
1291
container.classList.add('custom-view-tree-node-item');
1292
1293
const checkboxContainer = DOM.append(container, DOM.$(''));
1294
const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate });
1295
const icon = DOM.prepend(resourceLabel.element, DOM.$('.custom-view-tree-node-item-icon'));
1296
const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));
1297
const actionBar = new ActionBar(actionsContainer, {
1298
actionViewItemProvider: this.actionViewItemProvider
1299
});
1300
1301
return { resourceLabel, icon, checkboxContainer, actionBar, container };
1302
}
1303
1304
private getHover(label: string | IMarkdownString | undefined, resource: URI | null, node: ITreeItem): string | IManagedHoverTooltipMarkdownString | undefined {
1305
if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) {
1306
if (resource && !node.tooltip) {
1307
return undefined;
1308
} else if (node.tooltip === undefined) {
1309
if (isMarkdownString(label)) {
1310
return { markdown: label, markdownNotSupportedFallback: label.value };
1311
} else {
1312
return label;
1313
}
1314
} else if (!isString(node.tooltip)) {
1315
return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover
1316
} else if (node.tooltip !== '') {
1317
return node.tooltip;
1318
} else {
1319
return undefined;
1320
}
1321
}
1322
1323
return {
1324
markdown: typeof node.tooltip === 'string' ? node.tooltip :
1325
(token: CancellationToken): Promise<IMarkdownString | string | undefined> => {
1326
return new Promise<IMarkdownString | string | undefined>((resolve) => {
1327
node.resolve(token).then(() => resolve(node.tooltip));
1328
});
1329
},
1330
markdownNotSupportedFallback: resource ? undefined : (label ? (isMarkdownString(label) ? label.value : label) : '') // Passing undefined as the fallback for a resource falls back to the old native hover
1331
};
1332
}
1333
1334
private processLabel(label: string | IMarkdownString | undefined, matches: { start: number; end: number }[] | undefined): { label: string | undefined; bold?: boolean; italic?: boolean; strikethrough?: boolean; supportIcons?: boolean } {
1335
if (!isMarkdownString(label)) {
1336
return { label };
1337
}
1338
1339
let text = label.value.trim();
1340
let bold = false;
1341
let italic = false;
1342
let strikethrough = false;
1343
1344
function moveMatches(offset: number) {
1345
if (matches) {
1346
for (const match of matches) {
1347
match.start -= offset;
1348
match.end -= offset;
1349
}
1350
}
1351
}
1352
1353
const syntaxes = [
1354
{ open: '~~', close: '~~', mark: () => { strikethrough = true; } },
1355
{ open: '**', close: '**', mark: () => { bold = true; } },
1356
{ open: '*', close: '*', mark: () => { italic = true; } },
1357
{ open: '_', close: '_', mark: () => { italic = true; } }
1358
];
1359
1360
function checkSyntaxes(): boolean {
1361
let didChange = false;
1362
for (const syntax of syntaxes) {
1363
if (text.startsWith(syntax.open) && text.endsWith(syntax.close)) {
1364
// If there is a match within the markers, stop processing
1365
if (matches?.some(match => match.start < syntax.open.length || match.end > text.length - syntax.close.length)) {
1366
return false;
1367
}
1368
1369
syntax.mark();
1370
text = text.substring(syntax.open.length, text.length - syntax.close.length);
1371
moveMatches(syntax.open.length);
1372
didChange = true;
1373
}
1374
}
1375
return didChange;
1376
}
1377
1378
// Arbitrary max # of iterations
1379
for (let i = 0; i < 10; i++) {
1380
if (!checkSyntaxes()) {
1381
break;
1382
}
1383
}
1384
1385
return {
1386
label: text,
1387
bold,
1388
italic,
1389
strikethrough,
1390
supportIcons: label.supportThemeIcons
1391
};
1392
}
1393
1394
renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
1395
const node = element.element;
1396
const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;
1397
const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined);
1398
const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;
1399
const labelStr = treeItemLabel ? isMarkdownString(treeItemLabel.label) ? treeItemLabel.label.value : treeItemLabel.label : undefined;
1400
const matches = (treeItemLabel?.highlights && labelStr) ? treeItemLabel.highlights.map(([start, end]) => {
1401
if (start < 0) {
1402
start = labelStr.length + start;
1403
}
1404
if (end < 0) {
1405
end = labelStr.length + end;
1406
}
1407
if ((start >= labelStr.length) || (end > labelStr.length)) {
1408
return ({ start: 0, end: 0 });
1409
}
1410
if (start > end) {
1411
const swap = start;
1412
start = end;
1413
end = swap;
1414
}
1415
return ({ start, end });
1416
}) : undefined;
1417
const { label, bold, italic, strikethrough, supportIcons } = this.processLabel(treeItemLabel?.label, matches);
1418
const icon = !isDark(this.themeService.getColorTheme().type) ? node.icon : node.iconDark;
1419
const iconUrl = icon ? URI.revive(icon) : undefined;
1420
const title = this.getHover(treeItemLabel?.label, resource, node);
1421
1422
// reset
1423
templateData.actionBar.clear();
1424
templateData.icon.style.color = '';
1425
1426
let commandEnabled = true;
1427
if (node.command) {
1428
commandEnabled = isTreeCommandEnabled(node.command, this.contextKeyService);
1429
}
1430
1431
this.renderCheckbox(node, templateData);
1432
1433
if (resource) {
1434
const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
1435
const labelResource = resource ? resource : URI.parse('missing:_icon_resource');
1436
templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, {
1437
fileKind: this.getFileKind(node),
1438
title,
1439
hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon),
1440
fileDecorations,
1441
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
1442
matches: matches ? matches : createMatches(element.filterData),
1443
bold,
1444
italic,
1445
strikethrough,
1446
disabledCommand: !commandEnabled,
1447
labelEscapeNewLines: true,
1448
forceLabel: !!node.label,
1449
supportIcons
1450
});
1451
} else {
1452
templateData.resourceLabel.setResource({ name: label, description }, {
1453
title,
1454
hideIcon: true,
1455
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
1456
matches: matches ? matches : createMatches(element.filterData),
1457
bold,
1458
italic,
1459
strikethrough,
1460
disabledCommand: !commandEnabled,
1461
labelEscapeNewLines: true,
1462
supportIcons
1463
});
1464
}
1465
1466
if (iconUrl) {
1467
templateData.icon.className = 'custom-view-tree-node-item-icon';
1468
templateData.icon.style.backgroundImage = cssJs.asCSSUrl(iconUrl);
1469
} else {
1470
let iconClass: string | undefined;
1471
if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {
1472
iconClass = ThemeIcon.asClassName(node.themeIcon);
1473
if (node.themeIcon.color) {
1474
templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';
1475
}
1476
}
1477
templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';
1478
templateData.icon.style.backgroundImage = '';
1479
}
1480
1481
if (!commandEnabled) {
1482
templateData.icon.className = templateData.icon.className + ' disabled';
1483
if (templateData.container.parentElement) {
1484
templateData.container.parentElement.className = templateData.container.parentElement.className + ' disabled';
1485
}
1486
}
1487
1488
templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg;
1489
1490
const menuActions = this.menus.getResourceActions([node]);
1491
templateData.actionBar.push(menuActions, { icon: true, label: false });
1492
1493
if (this._actionRunner) {
1494
templateData.actionBar.actionRunner = this._actionRunner;
1495
}
1496
this.setAlignment(templateData.container, node);
1497
1498
// remember rendered element, an element can be rendered multiple times
1499
const renderedItems = this._renderedElements.get(element.element.handle) ?? [];
1500
this._renderedElements.set(element.element.handle, [...renderedItems, { original: element, rendered: templateData }]);
1501
}
1502
1503
private rerender() {
1504
// As we add items to the map during this call we can't directly use the map in the for loop
1505
// but have to create a copy of the keys first
1506
const keys = new Set(this._renderedElements.keys());
1507
for (const key of keys) {
1508
const values = this._renderedElements.get(key) ?? [];
1509
for (const value of values) {
1510
this.disposeElement(value.original, 0, value.rendered);
1511
this.renderElement(value.original, 0, value.rendered);
1512
}
1513
}
1514
}
1515
1516
private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData) {
1517
if (node.checkbox) {
1518
// The first time we find a checkbox we want to rerender the visible tree to adapt the alignment
1519
if (!this._hasCheckbox) {
1520
this._hasCheckbox = true;
1521
this.rerender();
1522
}
1523
if (!templateData.checkbox) {
1524
const checkbox = new TreeItemCheckbox(templateData.checkboxContainer, this.checkboxStateHandler, this._hoverDelegate, this.hoverService);
1525
templateData.checkbox = checkbox;
1526
}
1527
templateData.checkbox.render(node);
1528
} else if (templateData.checkbox) {
1529
templateData.checkbox.dispose();
1530
templateData.checkbox = undefined;
1531
}
1532
}
1533
1534
private setAlignment(container: HTMLElement, treeItem: ITreeItem) {
1535
container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem));
1536
}
1537
1538
private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean {
1539
// We always hide the resource label in favor of the iconUrl when it's provided.
1540
// When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon.
1541
return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon)));
1542
}
1543
1544
private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon {
1545
if (!icon) {
1546
return false;
1547
}
1548
1549
// If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the
1550
// icon theme and should use whatever the icon theme has provided.
1551
return !(hasResource && this.isFileKindThemeIcon(icon));
1552
}
1553
1554
private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {
1555
return ThemeIcon.isFile(icon) || ThemeIcon.isFolder(icon);
1556
}
1557
1558
private getFileKind(node: ITreeItem): FileKind {
1559
if (node.themeIcon) {
1560
switch (node.themeIcon.id) {
1561
case FileThemeIcon.id:
1562
return FileKind.FILE;
1563
case FolderThemeIcon.id:
1564
return FileKind.FOLDER;
1565
}
1566
}
1567
return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE;
1568
}
1569
1570
private onDidChangeContext(e: IContextKeyChangeEvent) {
1571
const affectsEntireMenuContexts = e.affectsSome(this.menus.getEntireMenuContexts());
1572
1573
const items: ITreeItem[] = [];
1574
for (const [_, elements] of this._renderedElements) {
1575
for (const element of elements) {
1576
if (affectsEntireMenuContexts || e.affectsSome(this.menus.getElementOverlayContexts(element.original.element))) {
1577
items.push(element.original.element);
1578
}
1579
}
1580
}
1581
if (items.length) {
1582
this._onDidChangeMenuContext.fire(items);
1583
}
1584
}
1585
1586
private updateCheckboxes(items: ITreeItem[]) {
1587
let allItems: ITreeItem[] = [];
1588
1589
if (!this.manuallyManageCheckboxes()) {
1590
allItems = setCascadingCheckboxUpdates(items);
1591
} else {
1592
allItems = items;
1593
}
1594
1595
allItems.forEach(item => {
1596
const renderedItems = this._renderedElements.get(item.handle);
1597
if (renderedItems) {
1598
renderedItems.forEach(renderedItems => renderedItems.rendered.checkbox?.render(item));
1599
}
1600
});
1601
this._onDidChangeCheckboxState.fire(allItems);
1602
}
1603
1604
disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
1605
const itemRenders = this._renderedElements.get(resource.element.handle) ?? [];
1606
const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered);
1607
1608
if (itemRenders.length === 1) {
1609
this._renderedElements.delete(resource.element.handle);
1610
} else if (itemRenders.length > 0) {
1611
itemRenders.splice(renderedIndex, 1);
1612
}
1613
1614
templateData.checkbox?.dispose();
1615
templateData.checkbox = undefined;
1616
}
1617
1618
disposeTemplate(templateData: ITreeExplorerTemplateData): void {
1619
templateData.resourceLabel.dispose();
1620
templateData.actionBar.dispose();
1621
}
1622
}
1623
1624
class Aligner extends Disposable {
1625
private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;
1626
1627
constructor(private themeService: IThemeService, private logService: ILogService) {
1628
super();
1629
}
1630
1631
set tree(tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>) {
1632
this._tree = tree;
1633
}
1634
1635
public alignIconWithTwisty(treeItem: ITreeItem): boolean {
1636
if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) {
1637
return false;
1638
}
1639
if (!this.hasIconOrCheckbox(treeItem)) {
1640
return false;
1641
}
1642
1643
if (this._tree) {
1644
const root = this._tree.getInput();
1645
let parent: ITreeItem;
1646
try {
1647
parent = this._tree.getParentElement(treeItem) || root;
1648
} catch (error) {
1649
this.logService.error(`[TreeView] Failed to resolve parent for ${treeItem.handle}`, error);
1650
return false;
1651
}
1652
if (this.hasIconOrCheckbox(parent)) {
1653
return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIconOrCheckbox(c));
1654
}
1655
return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIconOrCheckbox(c));
1656
} else {
1657
return false;
1658
}
1659
}
1660
1661
private hasIconOrCheckbox(node: ITreeItem): boolean {
1662
return this.hasIcon(node) || !!node.checkbox;
1663
}
1664
1665
private hasIcon(node: ITreeItem): boolean {
1666
const icon = !isDark(this.themeService.getColorTheme().type) ? node.icon : node.iconDark;
1667
if (icon) {
1668
return true;
1669
}
1670
if (node.resourceUri || node.themeIcon) {
1671
const fileIconTheme = this.themeService.getFileIconTheme();
1672
const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None;
1673
if (isFolder) {
1674
return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons;
1675
}
1676
return fileIconTheme.hasFileIcons;
1677
}
1678
return false;
1679
}
1680
}
1681
1682
class MultipleSelectionActionRunner extends ActionRunner {
1683
1684
constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) {
1685
super();
1686
this._register(this.onDidRun(e => {
1687
if (e.error && !isCancellationError(e.error)) {
1688
notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id));
1689
}
1690
}));
1691
}
1692
1693
protected override async runAction(action: IAction, context: TreeViewItemHandleArg | TreeViewPaneHandleArg): Promise<void> {
1694
const selection = this.getSelectedResources();
1695
let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;
1696
let actionInSelected: boolean = false;
1697
if (selection.length > 1) {
1698
selectionHandleArgs = selection.map(selected => {
1699
if ((selected.handle === (context as TreeViewItemHandleArg).$treeItemHandle) || (context as TreeViewPaneHandleArg).$selectedTreeItems) {
1700
actionInSelected = true;
1701
}
1702
return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle };
1703
});
1704
}
1705
1706
if (!actionInSelected && selectionHandleArgs) {
1707
selectionHandleArgs = undefined;
1708
}
1709
1710
await action.run(context, selectionHandleArgs);
1711
}
1712
}
1713
1714
class TreeMenus implements IDisposable {
1715
private contextKeyService: IContextKeyService | undefined;
1716
private _onDidChange = new Emitter<ITreeItem>();
1717
public readonly onDidChange = this._onDidChange.event;
1718
1719
constructor(
1720
private id: string,
1721
@IMenuService private readonly menuService: IMenuService
1722
) { }
1723
1724
/**
1725
* Gets only the actions that apply to all of the given elements.
1726
*/
1727
getResourceActions(elements: ITreeItem[]): IAction[] {
1728
const actions = this.getActions(this.getMenuId(), elements);
1729
return actions.primary;
1730
}
1731
1732
/**
1733
* Gets only the actions that apply to all of the given elements.
1734
*/
1735
getResourceContextActions(elements: ITreeItem[]): IAction[] {
1736
return this.getActions(this.getMenuId(), elements).secondary;
1737
}
1738
1739
public setContextKeyService(service: IContextKeyService) {
1740
this.contextKeyService = service;
1741
}
1742
1743
private filterNonUniversalActions(groups: Map<string, IAction>[], newActions: IAction[]) {
1744
const newActionsSet: Set<string> = new Set(newActions.map(a => a.id));
1745
for (const group of groups) {
1746
const actions = group.keys();
1747
for (const action of actions) {
1748
if (!newActionsSet.has(action)) {
1749
group.delete(action);
1750
}
1751
}
1752
}
1753
}
1754
1755
private buildMenu(groups: Map<string, IAction>[]): IAction[] {
1756
const result: IAction[] = [];
1757
for (const group of groups) {
1758
if (group.size > 0) {
1759
if (result.length) {
1760
result.push(new Separator());
1761
}
1762
result.push(...group.values());
1763
}
1764
}
1765
return result;
1766
}
1767
1768
private createGroups(actions: IAction[]): Map<string, IAction>[] {
1769
const groups: Map<string, IAction>[] = [];
1770
let group: Map<string, IAction> = new Map();
1771
for (const action of actions) {
1772
if (action instanceof Separator) {
1773
groups.push(group);
1774
group = new Map();
1775
} else {
1776
group.set(action.id, action);
1777
}
1778
}
1779
groups.push(group);
1780
return groups;
1781
}
1782
1783
public getElementOverlayContexts(element: ITreeItem): Map<string, unknown> {
1784
return new Map([
1785
['view', this.id],
1786
['viewItem', element.contextValue]
1787
]);
1788
}
1789
1790
public getEntireMenuContexts(): ReadonlySet<string> {
1791
return this.menuService.getMenuContexts(this.getMenuId());
1792
}
1793
1794
public getMenuId(): MenuId {
1795
return MenuId.ViewItemContext;
1796
}
1797
1798
private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } {
1799
if (!this.contextKeyService) {
1800
return { primary: [], secondary: [] };
1801
}
1802
1803
let primaryGroups: Map<string, IAction>[] = [];
1804
let secondaryGroups: Map<string, IAction>[] = [];
1805
for (let i = 0; i < elements.length; i++) {
1806
const element = elements[i];
1807
const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element));
1808
1809
const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true });
1810
1811
const result = getContextMenuActions(menuData, 'inline');
1812
if (i === 0) {
1813
primaryGroups = this.createGroups(result.primary);
1814
secondaryGroups = this.createGroups(result.secondary);
1815
} else {
1816
this.filterNonUniversalActions(primaryGroups, result.primary);
1817
this.filterNonUniversalActions(secondaryGroups, result.secondary);
1818
}
1819
}
1820
1821
return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) };
1822
}
1823
1824
dispose() {
1825
this.contextKeyService = undefined;
1826
this._onDidChange.dispose();
1827
}
1828
}
1829
1830
export class CustomTreeView extends AbstractTreeView {
1831
1832
constructor(
1833
id: string,
1834
title: string,
1835
private readonly extensionId: string,
1836
@IThemeService themeService: IThemeService,
1837
@IInstantiationService instantiationService: IInstantiationService,
1838
@ICommandService commandService: ICommandService,
1839
@IConfigurationService configurationService: IConfigurationService,
1840
@IProgressService progressService: IProgressService,
1841
@IContextMenuService contextMenuService: IContextMenuService,
1842
@IKeybindingService keybindingService: IKeybindingService,
1843
@INotificationService notificationService: INotificationService,
1844
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1845
@IContextKeyService contextKeyService: IContextKeyService,
1846
@IHoverService hoverService: IHoverService,
1847
@IExtensionService private readonly extensionService: IExtensionService,
1848
@IActivityService activityService: IActivityService,
1849
@ITelemetryService private readonly telemetryService: ITelemetryService,
1850
@ILogService logService: ILogService,
1851
@IOpenerService openerService: IOpenerService,
1852
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
1853
) {
1854
super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService, openerService, markdownRendererService);
1855
}
1856
1857
protected activate() {
1858
if (!this.activated) {
1859
type ExtensionViewTelemetry = {
1860
extensionId: TelemetryTrustedValue<string>;
1861
id: string;
1862
};
1863
type ExtensionViewTelemetryMeta = {
1864
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' };
1865
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' };
1866
owner: 'digitarald';
1867
comment: 'Helps to gain insights on what extension contributed views are most popular';
1868
};
1869
this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', {
1870
extensionId: new TelemetryTrustedValue(this.extensionId),
1871
id: this.id,
1872
});
1873
this.createTree();
1874
this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
1875
.then(() => timeout(2000))
1876
.then(() => {
1877
this.updateMessage();
1878
});
1879
this.activated = true;
1880
}
1881
}
1882
}
1883
1884
export class TreeView extends AbstractTreeView {
1885
1886
protected activate() {
1887
if (!this.activated) {
1888
this.createTree();
1889
this.activated = true;
1890
}
1891
}
1892
}
1893
1894
interface TreeDragSourceInfo {
1895
id: string;
1896
itemHandles: string[];
1897
}
1898
1899
export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {
1900
private readonly treeMimeType: string;
1901
private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();
1902
private dragCancellationToken: CancellationTokenSource | undefined;
1903
1904
constructor(
1905
private readonly treeId: string,
1906
@ILabelService private readonly labelService: ILabelService,
1907
@IInstantiationService private readonly instantiationService: IInstantiationService,
1908
@ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService,
1909
@ILogService private readonly logService: ILogService) {
1910
this.treeMimeType = `application/vnd.code.tree.${treeId.toLowerCase()}`;
1911
}
1912
1913
private dndController: ITreeViewDragAndDropController | undefined;
1914
set controller(controller: ITreeViewDragAndDropController | undefined) {
1915
this.dndController = controller;
1916
}
1917
1918
private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<VSDataTransfer | undefined> {
1919
return dndController.handleDrag(itemHandles, uuid, dragCancellationToken).then(additionalDataTransfer => {
1920
if (additionalDataTransfer) {
1921
const unlistedTypes: string[] = [];
1922
for (const item of additionalDataTransfer) {
1923
if ((item[0] !== this.treeMimeType) && (dndController.dragMimeTypes.findIndex(value => value === item[0]) < 0)) {
1924
unlistedTypes.push(item[0]);
1925
}
1926
}
1927
if (unlistedTypes.length) {
1928
this.logService.warn(`Drag and drop controller for tree ${this.treeId} adds the following data transfer types but does not declare them in dragMimeTypes: ${unlistedTypes.join(', ')}`);
1929
}
1930
}
1931
return additionalDataTransfer;
1932
});
1933
}
1934
1935
private addExtensionProvidedTransferTypes(originalEvent: DragEvent, itemHandles: string[]) {
1936
if (!originalEvent.dataTransfer || !this.dndController) {
1937
return;
1938
}
1939
const uuid = generateUuid();
1940
1941
this.dragCancellationToken = new CancellationTokenSource();
1942
this.treeViewsDragAndDropService.addDragOperationTransfer(uuid, this.handleDragAndLog(this.dndController, itemHandles, uuid, this.dragCancellationToken.token));
1943
this.treeItemsTransfer.setData([new DraggedTreeItemsIdentifier(uuid)], DraggedTreeItemsIdentifier.prototype);
1944
originalEvent.dataTransfer.clearData(Mimes.text);
1945
if (this.dndController.dragMimeTypes.find((element) => element === Mimes.uriList)) {
1946
// Add the type that the editor knows
1947
originalEvent.dataTransfer?.setData(DataTransfers.RESOURCES, '');
1948
}
1949
this.dndController.dragMimeTypes.forEach(supportedType => {
1950
originalEvent.dataTransfer?.setData(supportedType, '');
1951
});
1952
}
1953
1954
private addResourceInfoToTransfer(originalEvent: DragEvent, resources: URI[]) {
1955
if (resources.length && originalEvent.dataTransfer) {
1956
// Apply some datatransfer types to allow for dragging the element outside of the application
1957
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
1958
1959
// The only custom data transfer we set from the explorer is a file transfer
1960
// to be able to DND between multiple code file explorers across windows
1961
const fileResources = resources.filter(s => s.scheme === Schemas.file).map(r => r.fsPath);
1962
if (fileResources.length) {
1963
originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
1964
}
1965
}
1966
}
1967
1968
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
1969
if (originalEvent.dataTransfer) {
1970
const treeItemsData = (data as ElementsDragAndDropData<ITreeItem, ITreeItem[]>).getData();
1971
const resources: URI[] = [];
1972
const sourceInfo: TreeDragSourceInfo = {
1973
id: this.treeId,
1974
itemHandles: []
1975
};
1976
treeItemsData.forEach(item => {
1977
sourceInfo.itemHandles.push(item.handle);
1978
if (item.resourceUri) {
1979
resources.push(URI.revive(item.resourceUri));
1980
}
1981
});
1982
this.addResourceInfoToTransfer(originalEvent, resources);
1983
this.addExtensionProvidedTransferTypes(originalEvent, sourceInfo.itemHandles);
1984
originalEvent.dataTransfer.setData(this.treeMimeType,
1985
JSON.stringify(sourceInfo));
1986
}
1987
}
1988
1989
private debugLog(types: Set<string>) {
1990
if (types.size) {
1991
this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`);
1992
} else {
1993
this.logService.debug(`TreeView dragged with no supported mime types.`);
1994
}
1995
}
1996
1997
onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
1998
const dataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer!);
1999
2000
const types = new Set<string>(Array.from(dataTransfer, x => x[0]));
2001
2002
if (originalEvent.dataTransfer) {
2003
// Also add uri-list if we have any files. At this stage we can't actually access the file itself though.
2004
for (const item of originalEvent.dataTransfer.items) {
2005
if (item.kind === 'file' || item.type === DataTransfers.RESOURCES.toLowerCase()) {
2006
types.add(Mimes.uriList);
2007
break;
2008
}
2009
}
2010
}
2011
2012
this.debugLog(types);
2013
2014
const dndController = this.dndController;
2015
if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) {
2016
return false;
2017
}
2018
const dragContainersSupportedType = Array.from(types).some((value, index) => {
2019
if (value === this.treeMimeType) {
2020
return true;
2021
} else {
2022
return dndController.dropMimeTypes.indexOf(value) >= 0;
2023
}
2024
});
2025
if (dragContainersSupportedType) {
2026
return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true };
2027
}
2028
return false;
2029
}
2030
2031
getDragURI(element: ITreeItem): string | null {
2032
if (!this.dndController) {
2033
return null;
2034
}
2035
return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle;
2036
}
2037
2038
getDragLabel?(elements: ITreeItem[]): string | undefined {
2039
if (!this.dndController) {
2040
return undefined;
2041
}
2042
if (elements.length > 1) {
2043
return String(elements.length);
2044
}
2045
const element = elements[0];
2046
if (element.label) {
2047
return isMarkdownString(element.label.label) ? element.label.label.value : element.label.label;
2048
}
2049
return element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined;
2050
}
2051
2052
async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): Promise<void> {
2053
const dndController = this.dndController;
2054
if (!originalEvent.dataTransfer || !dndController) {
2055
return;
2056
}
2057
2058
let treeSourceInfo: TreeDragSourceInfo | undefined;
2059
let willDropUuid: string | undefined;
2060
if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {
2061
willDropUuid = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype)![0].identifier;
2062
}
2063
2064
const originalDataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer, true);
2065
2066
const outDataTransfer = new VSDataTransfer();
2067
for (const [type, item] of originalDataTransfer) {
2068
if (type === this.treeMimeType || dndController.dropMimeTypes.includes(type) || (item.asFile() && dndController.dropMimeTypes.includes(DataTransfers.FILES.toLowerCase()))) {
2069
outDataTransfer.append(type, item);
2070
if (type === this.treeMimeType) {
2071
try {
2072
treeSourceInfo = JSON.parse(await item.asString());
2073
} catch {
2074
// noop
2075
}
2076
}
2077
}
2078
}
2079
2080
const additionalDataTransfer = await this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid);
2081
if (additionalDataTransfer) {
2082
for (const [type, item] of additionalDataTransfer) {
2083
outDataTransfer.append(type, item);
2084
}
2085
}
2086
return dndController.handleDrop(outDataTransfer, targetNode, CancellationToken.None, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles);
2087
}
2088
2089
onDragEnd(originalEvent: DragEvent): void {
2090
// Check if the drag was cancelled.
2091
if (originalEvent.dataTransfer?.dropEffect === 'none') {
2092
this.dragCancellationToken?.cancel();
2093
}
2094
}
2095
2096
dispose(): void { }
2097
}
2098
2099
function setCascadingCheckboxUpdates(items: readonly ITreeItem[]) {
2100
const additionalItems: ITreeItem[] = [];
2101
2102
for (const item of items) {
2103
if (item.checkbox !== undefined) {
2104
2105
const checkChildren = (currentItem: ITreeItem) => {
2106
for (const child of (currentItem.children ?? [])) {
2107
if ((child.checkbox !== undefined) && (currentItem.checkbox !== undefined) && (child.checkbox.isChecked !== currentItem.checkbox.isChecked)) {
2108
child.checkbox.isChecked = currentItem.checkbox.isChecked;
2109
additionalItems.push(child);
2110
checkChildren(child);
2111
}
2112
}
2113
};
2114
checkChildren(item);
2115
2116
const visitedParents: Set<ITreeItem> = new Set();
2117
const checkParents = (currentItem: ITreeItem) => {
2118
if (currentItem.parent?.checkbox !== undefined && currentItem.parent.children) {
2119
if (visitedParents.has(currentItem.parent)) {
2120
return;
2121
} else {
2122
visitedParents.add(currentItem.parent);
2123
}
2124
2125
let someUnchecked = false;
2126
let someChecked = false;
2127
for (const child of currentItem.parent.children) {
2128
if (someUnchecked && someChecked) {
2129
break;
2130
}
2131
if (child.checkbox !== undefined) {
2132
if (child.checkbox.isChecked) {
2133
someChecked = true;
2134
} else {
2135
someUnchecked = true;
2136
}
2137
}
2138
}
2139
if (someChecked && !someUnchecked && (currentItem.parent.checkbox.isChecked !== true)) {
2140
currentItem.parent.checkbox.isChecked = true;
2141
additionalItems.push(currentItem.parent);
2142
checkParents(currentItem.parent);
2143
} else if (someUnchecked && (currentItem.parent.checkbox.isChecked !== false)) {
2144
currentItem.parent.checkbox.isChecked = false;
2145
additionalItems.push(currentItem.parent);
2146
checkParents(currentItem.parent);
2147
}
2148
}
2149
};
2150
checkParents(item);
2151
}
2152
}
2153
2154
return items.concat(additionalItems);
2155
}
2156
2157