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
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { 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 { 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 { ColorScheme } 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 { IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/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 && 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 markdownRenderer: MarkdownRenderer | undefined;
237
private elementsToRefresh: ITreeItem[] = [];
238
private lastSelection: readonly ITreeItem[] = [];
239
private lastActive: ITreeItem;
240
241
private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
242
get onDidExpandItem(): Event<ITreeItem> { return this._onDidExpandItem.event; }
243
244
private readonly _onDidCollapseItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
245
get onDidCollapseItem(): Event<ITreeItem> { return this._onDidCollapseItem.event; }
246
247
private _onDidChangeSelectionAndFocus: Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._register(new Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }>());
248
get onDidChangeSelectionAndFocus(): Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }> { return this._onDidChangeSelectionAndFocus.event; }
249
250
private readonly _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
251
get onDidChangeVisibility(): Event<boolean> { return this._onDidChangeVisibility.event; }
252
253
private readonly _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
254
get onDidChangeActions(): Event<void> { return this._onDidChangeActions.event; }
255
256
private readonly _onDidChangeWelcomeState: Emitter<void> = this._register(new Emitter<void>());
257
get onDidChangeWelcomeState(): Event<void> { return this._onDidChangeWelcomeState.event; }
258
259
private readonly _onDidChangeTitle: Emitter<string> = this._register(new Emitter<string>());
260
get onDidChangeTitle(): Event<string> { return this._onDidChangeTitle.event; }
261
262
private readonly _onDidChangeDescription: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
263
get onDidChangeDescription(): Event<string | undefined> { return this._onDidChangeDescription.event; }
264
265
private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
266
get onDidChangeCheckboxState(): Event<readonly ITreeItem[]> { return this._onDidChangeCheckboxState.event; }
267
268
private readonly _onDidCompleteRefresh: Emitter<void> = this._register(new Emitter<void>());
269
270
constructor(
271
readonly id: string,
272
private _title: string,
273
@IThemeService private readonly themeService: IThemeService,
274
@IInstantiationService private readonly instantiationService: IInstantiationService,
275
@ICommandService private readonly commandService: ICommandService,
276
@IConfigurationService private readonly configurationService: IConfigurationService,
277
@IProgressService protected readonly progressService: IProgressService,
278
@IContextMenuService private readonly contextMenuService: IContextMenuService,
279
@IKeybindingService private readonly keybindingService: IKeybindingService,
280
@INotificationService private readonly notificationService: INotificationService,
281
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
282
@IHoverService private readonly hoverService: IHoverService,
283
@IContextKeyService private readonly contextKeyService: IContextKeyService,
284
@IActivityService private readonly activityService: IActivityService,
285
@ILogService private readonly logService: ILogService,
286
@IOpenerService private readonly openerService: IOpenerService
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 && 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));
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
buildAriaLabel += element.label.label + ' ';
723
}
724
if (element.description) {
725
buildAriaLabel += element.description;
726
}
727
return buildAriaLabel;
728
}
729
},
730
getRole(element: ITreeItem): AriaRole | undefined {
731
return element.accessibilityInformation?.role ?? 'treeitem';
732
},
733
getWidgetAriaLabel(): string {
734
return widgetAriaLabel;
735
}
736
},
737
keyboardNavigationLabelProvider: {
738
getKeyboardNavigationLabel: (item: ITreeItem) => {
739
return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined);
740
}
741
},
742
expandOnlyOnTwistieClick: (e: ITreeItem) => {
743
return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';
744
},
745
collapseByDefault: (e: ITreeItem): boolean => {
746
return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
747
},
748
multipleSelectionSupport: this.canSelectMany,
749
dnd: this.treeViewDnd,
750
overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles
751
}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);
752
753
this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e))));
754
755
this.treeDisposables.add(this.tree);
756
treeMenus.setContextKeyService(this.tree.contextKeyService);
757
aligner.tree = this.tree;
758
const actionRunner = this.treeDisposables.add(new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()));
759
renderer.actionRunner = actionRunner;
760
761
this.tree.contextKeyService.createKey<boolean>(this.id, true);
762
const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);
763
customTreeKey.set(true);
764
this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
765
766
this.treeDisposables.add(this.tree.onDidChangeSelection(e => {
767
this.lastSelection = e.elements;
768
this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive;
769
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
770
}));
771
this.treeDisposables.add(this.tree.onDidChangeFocus(e => {
772
if (e.elements.length && (e.elements[0] !== this.lastActive)) {
773
this.lastActive = e.elements[0];
774
this.lastSelection = this.tree?.getSelection() ?? this.lastSelection;
775
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
776
}
777
}));
778
this.treeDisposables.add(this.tree.onDidChangeCollapseState(e => {
779
if (!e.node.element) {
780
return;
781
}
782
783
const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element;
784
if (e.node.collapsed) {
785
this._onDidCollapseItem.fire(element);
786
} else {
787
this._onDidExpandItem.fire(element);
788
}
789
}));
790
this.tree.setInput(this.root).then(() => this.updateContentAreas());
791
792
this.treeDisposables.add(this.tree.onDidOpen(async (e) => {
793
if (!e.browserEvent) {
794
return;
795
}
796
if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {
797
return;
798
}
799
const selection = this.tree!.getSelection();
800
const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);
801
802
if (command && isTreeCommandEnabled(command, this.contextKeyService)) {
803
let args = command.arguments || [];
804
if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {
805
// Some commands owned by us should receive the
806
// `IOpenEvent` as context to open properly
807
args = [...args, e];
808
}
809
810
try {
811
await this.commandService.executeCommand(command.id, ...args);
812
} catch (err) {
813
this.notificationService.error(err);
814
}
815
}
816
}));
817
818
this.treeDisposables.add(treeMenus.onDidChange((changed) => {
819
if (this.tree?.hasNode(changed)) {
820
this.tree?.rerender(changed);
821
}
822
}));
823
}
824
825
private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {
826
let command = element?.command;
827
if (element && !command) {
828
if ((element instanceof ResolvableTreeItem) && element.hasResolve) {
829
await element.resolve(CancellationToken.None);
830
command = element.command;
831
}
832
}
833
return command;
834
}
835
836
837
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
838
this.hoverService.hideHover();
839
const node: ITreeItem | null = treeEvent.element;
840
if (node === null) {
841
return;
842
}
843
const event: UIEvent = treeEvent.browserEvent;
844
845
event.preventDefault();
846
event.stopPropagation();
847
848
this.tree!.setFocus([node]);
849
let selected = this.canSelectMany ? this.getSelection() : [];
850
if (!selected.find(item => item.handle === node.handle)) {
851
selected = [node];
852
}
853
854
const actions = treeMenus.getResourceContextActions(selected);
855
if (!actions.length) {
856
return;
857
}
858
this.contextMenuService.showContextMenu({
859
getAnchor: () => treeEvent.anchor,
860
861
getActions: () => actions,
862
863
getActionViewItem: (action) => {
864
const keybinding = this.keybindingService.lookupKeybinding(action.id);
865
if (keybinding) {
866
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
867
}
868
return undefined;
869
},
870
871
onHide: (wasCancelled?: boolean) => {
872
if (wasCancelled) {
873
this.tree!.domFocus();
874
}
875
},
876
877
getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg),
878
879
actionRunner
880
});
881
}
882
883
protected updateMessage(): void {
884
if (this._message) {
885
this.showMessage(this._message);
886
} else if (!this.dataProvider) {
887
this.showMessage(noDataProviderMessage);
888
} else {
889
this.hideMessage();
890
}
891
this.updateContentAreas();
892
}
893
894
private processMessage(message: IMarkdownString, disposables: DisposableStore): HTMLElement {
895
const lines = message.value.split('\n');
896
const result: (IMarkdownRenderResult | HTMLElement)[] = [];
897
let hasFoundButton = false;
898
for (const line of lines) {
899
const linkedText = parseLinkedText(line);
900
901
if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') {
902
const node = linkedText.nodes[0];
903
const buttonContainer = document.createElement('div');
904
buttonContainer.classList.add('button-container');
905
const button = new Button(buttonContainer, { title: node.title, secondary: hasFoundButton, supportIcons: true, ...defaultButtonStyles });
906
button.label = node.label;
907
button.onDidClick(_ => {
908
this.openerService.open(node.href, { allowCommands: true });
909
}, null, disposables);
910
911
const href = URI.parse(node.href);
912
if (href.scheme === Schemas.command) {
913
const preConditions = commandPreconditions(href.path);
914
if (preConditions) {
915
button.enabled = this.contextKeyService.contextMatchesRules(preConditions);
916
disposables.add(this.contextKeyService.onDidChangeContext(e => {
917
if (e.affectsSome(new Set(preConditions.keys()))) {
918
button.enabled = this.contextKeyService.contextMatchesRules(preConditions);
919
}
920
}));
921
}
922
}
923
924
disposables.add(button);
925
hasFoundButton = true;
926
result.push(buttonContainer);
927
} else {
928
hasFoundButton = false;
929
const rendered = this.markdownRenderer!.render(new MarkdownString(line, { isTrusted: message.isTrusted, supportThemeIcons: message.supportThemeIcons, supportHtml: message.supportHtml }));
930
result.push(rendered.element);
931
disposables.add(rendered);
932
}
933
}
934
935
const container = document.createElement('div');
936
container.classList.add('rendered-message');
937
for (const child of result) {
938
if (DOM.isHTMLElement(child)) {
939
container.appendChild(child);
940
} else {
941
container.appendChild(child.element);
942
}
943
}
944
return container;
945
}
946
947
private showMessage(message: string | IMarkdownString): void {
948
if (isRenderedMessageValue(this._messageValue)) {
949
this._messageValue.disposables.dispose();
950
}
951
if (isMarkdownString(message) && !this.markdownRenderer) {
952
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
953
}
954
if (isMarkdownString(message)) {
955
const disposables = new DisposableStore();
956
const renderedMessage = this.processMessage(message, disposables);
957
this._messageValue = { element: renderedMessage, disposables };
958
} else {
959
this._messageValue = message;
960
}
961
if (!this.messageElement) {
962
return;
963
}
964
this.messageElement.classList.remove('hide');
965
this.resetMessageElement();
966
if (typeof this._messageValue === 'string' && !isFalsyOrWhitespace(this._messageValue)) {
967
this.messageElement.textContent = this._messageValue;
968
} else if (isRenderedMessageValue(this._messageValue)) {
969
this.messageElement.appendChild(this._messageValue.element);
970
}
971
this.layout(this._height, this._width);
972
}
973
974
private hideMessage(): void {
975
this.resetMessageElement();
976
this.messageElement?.classList.add('hide');
977
this.layout(this._height, this._width);
978
}
979
980
private resetMessageElement(): void {
981
if (this.messageElement) {
982
DOM.clearNode(this.messageElement);
983
}
984
}
985
986
private _height: number = 0;
987
private _width: number = 0;
988
layout(height: number, width: number) {
989
if (height && width && this.messageElement && this.treeContainer) {
990
this._height = height;
991
this._width = width;
992
const treeHeight = height - DOM.getTotalHeight(this.messageElement);
993
this.treeContainer.style.height = treeHeight + 'px';
994
this.tree?.layout(treeHeight, width);
995
}
996
}
997
998
getOptimalWidth(): number {
999
if (this.tree) {
1000
const parentNode = this.tree.getHTMLElement();
1001
const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
1002
return DOM.getLargestChildWidth(parentNode, childNodes);
1003
}
1004
return 0;
1005
}
1006
1007
private updateCheckboxes(elements: readonly ITreeItem[]): ITreeItem[] {
1008
return setCascadingCheckboxUpdates(elements);
1009
}
1010
1011
async refresh(elements?: readonly ITreeItem[], checkboxes?: readonly ITreeItem[]): Promise<void> {
1012
if (this.dataProvider && this.tree) {
1013
if (this.refreshing) {
1014
await Event.toPromise(this._onDidCompleteRefresh.event);
1015
}
1016
if (!elements) {
1017
elements = [this.root];
1018
// remove all waiting elements to refresh if root is asked to refresh
1019
this.elementsToRefresh = [];
1020
}
1021
for (const element of elements) {
1022
element.children = undefined; // reset children
1023
}
1024
if (this.isVisible) {
1025
const affectedElements = this.updateCheckboxes(checkboxes ?? []);
1026
return this.doRefresh(elements.concat(affectedElements));
1027
} else {
1028
if (this.elementsToRefresh.length) {
1029
const seen: Set<string> = new Set<string>();
1030
this.elementsToRefresh.forEach(element => seen.add(element.handle));
1031
for (const element of elements) {
1032
if (!seen.has(element.handle)) {
1033
this.elementsToRefresh.push(element);
1034
}
1035
}
1036
} else {
1037
this.elementsToRefresh.push(...elements);
1038
}
1039
}
1040
}
1041
return undefined;
1042
}
1043
1044
async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {
1045
const tree = this.tree;
1046
if (!tree) {
1047
return;
1048
}
1049
try {
1050
itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
1051
for (const element of itemOrItems) {
1052
await tree.expand(element, false);
1053
}
1054
} catch (e) {
1055
// The extension could have changed the tree during the reveal.
1056
// Because of that, we ignore errors.
1057
}
1058
}
1059
1060
isCollapsed(item: ITreeItem): boolean {
1061
return !!this.tree?.isCollapsed(item);
1062
}
1063
1064
setSelection(items: ITreeItem[]): void {
1065
this.tree?.setSelection(items);
1066
}
1067
1068
getSelection(): ITreeItem[] {
1069
return this.tree?.getSelection() ?? [];
1070
}
1071
1072
setFocus(item?: ITreeItem): void {
1073
if (this.tree) {
1074
if (item) {
1075
this.focus(true, item);
1076
this.tree.setFocus([item]);
1077
} else if (this.tree.getFocus().length === 0) {
1078
this.tree.setFocus([]);
1079
}
1080
}
1081
}
1082
1083
async reveal(item: ITreeItem): Promise<void> {
1084
if (this.tree) {
1085
return this.tree.reveal(item);
1086
}
1087
}
1088
1089
private refreshing: boolean = false;
1090
private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {
1091
const tree = this.tree;
1092
if (tree && this.visible) {
1093
this.refreshing = true;
1094
const oldSelection = tree.getSelection();
1095
try {
1096
await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
1097
} catch (e) {
1098
// When multiple calls are made to refresh the tree in quick succession,
1099
// we can get a "Tree element not found" error. This is expected.
1100
// Ideally this is fixable, so log instead of ignoring so the error is preserved.
1101
this.logService.error(e);
1102
}
1103
const newSelection = tree.getSelection();
1104
if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {
1105
this.lastSelection = newSelection;
1106
this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });
1107
}
1108
this.refreshing = false;
1109
this._onDidCompleteRefresh.fire();
1110
this.updateContentAreas();
1111
if (this.focused) {
1112
this.focus(false);
1113
}
1114
this.updateCollapseAllToggle();
1115
}
1116
}
1117
1118
private initializeCollapseAllToggle() {
1119
if (!this.collapseAllToggleContext) {
1120
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));
1121
this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);
1122
}
1123
}
1124
1125
private updateCollapseAllToggle() {
1126
if (this.showCollapseAllAction) {
1127
this.initializeCollapseAllToggle();
1128
this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&
1129
this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));
1130
}
1131
}
1132
1133
private updateContentAreas(): void {
1134
const isTreeEmpty = !this.root.children || this.root.children.length === 0;
1135
// Hide tree container only when there is a message and tree is empty and not refreshing
1136
if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {
1137
// If there's a dnd controller then hiding the tree prevents it from being dragged into.
1138
if (!this.dragAndDropController) {
1139
this.treeContainer.classList.add('hide');
1140
}
1141
this.domNode.setAttribute('tabindex', '0');
1142
} else if (this.treeContainer) {
1143
this.treeContainer.classList.remove('hide');
1144
if (this.domNode === DOM.getActiveElement()) {
1145
this.focus();
1146
}
1147
this.domNode.removeAttribute('tabindex');
1148
}
1149
}
1150
1151
get container(): HTMLElement | undefined {
1152
return this._container;
1153
}
1154
}
1155
1156
class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {
1157
getId(element: ITreeItem): { toString(): string } {
1158
return element.handle;
1159
}
1160
}
1161
1162
class TreeViewDelegate implements IListVirtualDelegate<ITreeItem> {
1163
1164
getHeight(element: ITreeItem): number {
1165
return TreeRenderer.ITEM_HEIGHT;
1166
}
1167
1168
getTemplateId(element: ITreeItem): string {
1169
return TreeRenderer.TREE_TEMPLATE_ID;
1170
}
1171
}
1172
1173
async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<ITreeItem[][] | undefined> {
1174
if (dataProvider.getChildrenBatch) {
1175
return dataProvider.getChildrenBatch(nodes);
1176
} else {
1177
if (nodes) {
1178
return Promise.all(nodes.map(node => dataProvider.getChildren(node).then(children => children ?? [])));
1179
} else {
1180
return [await dataProvider.getChildren()].filter(children => children !== undefined);
1181
}
1182
}
1183
}
1184
1185
class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {
1186
1187
constructor(
1188
private treeView: ITreeView,
1189
private withProgress: <T>(task: Promise<T>) => Promise<T>
1190
) {
1191
}
1192
1193
hasChildren(element: ITreeItem): boolean {
1194
return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None);
1195
}
1196
1197
private batch: ITreeItem[] | undefined;
1198
private batchPromise: Promise<ITreeItem[][] | undefined> | undefined;
1199
async getChildren(element: ITreeItem): Promise<ITreeItem[]> {
1200
const dataProvider = this.treeView.dataProvider;
1201
if (!dataProvider) {
1202
return [];
1203
}
1204
if (this.batch === undefined) {
1205
this.batch = [element];
1206
this.batchPromise = undefined;
1207
} else {
1208
this.batch.push(element);
1209
}
1210
const indexInBatch = this.batch.length - 1;
1211
return new Promise<ITreeItem[]>((resolve, reject) => {
1212
setTimeout(async () => {
1213
const batch = this.batch;
1214
this.batch = undefined;
1215
if (!this.batchPromise) {
1216
this.batchPromise = this.withProgress(doGetChildrenOrBatch(dataProvider, batch));
1217
}
1218
try {
1219
const result = await this.batchPromise;
1220
resolve((result && (indexInBatch < result.length)) ? result[indexInBatch] : []);
1221
} catch (e) {
1222
if (!(<string>e.message).startsWith('Bad progress location:')) {
1223
reject(e);
1224
}
1225
}
1226
}, 0);
1227
});
1228
}
1229
}
1230
1231
interface ITreeExplorerTemplateData {
1232
readonly container: HTMLElement;
1233
readonly resourceLabel: IResourceLabel;
1234
readonly icon: HTMLElement;
1235
readonly checkboxContainer: HTMLElement;
1236
checkbox?: TreeItemCheckbox;
1237
readonly actionBar: ActionBar;
1238
}
1239
1240
class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {
1241
static readonly ITEM_HEIGHT = 22;
1242
static readonly TREE_TEMPLATE_ID = 'treeExplorer';
1243
1244
private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
1245
readonly onDidChangeCheckboxState: Event<readonly ITreeItem[]> = this._onDidChangeCheckboxState.event;
1246
1247
private _onDidChangeMenuContext: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());
1248
readonly onDidChangeMenuContext: Event<readonly ITreeItem[]> = this._onDidChangeMenuContext.event;
1249
1250
private _actionRunner: MultipleSelectionActionRunner | undefined;
1251
private _hoverDelegate: IHoverDelegate;
1252
private _hasCheckbox: boolean = false;
1253
private _renderedElements = new Map<string, { original: ITreeNode<ITreeItem, FuzzyScore>; rendered: ITreeExplorerTemplateData }[]>(); // tree item handle to template data
1254
1255
constructor(
1256
private treeViewId: string,
1257
private menus: TreeMenus,
1258
private labels: ResourceLabels,
1259
private actionViewItemProvider: IActionViewItemProvider,
1260
private aligner: Aligner,
1261
private checkboxStateHandler: CheckboxStateHandler,
1262
private readonly manuallyManageCheckboxes: () => boolean,
1263
@IThemeService private readonly themeService: IThemeService,
1264
@IConfigurationService private readonly configurationService: IConfigurationService,
1265
@ILabelService private readonly labelService: ILabelService,
1266
@IContextKeyService private readonly contextKeyService: IContextKeyService,
1267
@IHoverService private readonly hoverService: IHoverService,
1268
@IInstantiationService instantiationService: IInstantiationService,
1269
) {
1270
super();
1271
this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));
1272
this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));
1273
this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));
1274
this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {
1275
this.updateCheckboxes(items);
1276
}));
1277
this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e)));
1278
}
1279
1280
get templateId(): string {
1281
return TreeRenderer.TREE_TEMPLATE_ID;
1282
}
1283
1284
set actionRunner(actionRunner: MultipleSelectionActionRunner) {
1285
this._actionRunner = actionRunner;
1286
}
1287
1288
renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
1289
container.classList.add('custom-view-tree-node-item');
1290
1291
const checkboxContainer = DOM.append(container, DOM.$(''));
1292
const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate });
1293
const icon = DOM.prepend(resourceLabel.element, DOM.$('.custom-view-tree-node-item-icon'));
1294
const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));
1295
const actionBar = new ActionBar(actionsContainer, {
1296
actionViewItemProvider: this.actionViewItemProvider
1297
});
1298
1299
return { resourceLabel, icon, checkboxContainer, actionBar, container };
1300
}
1301
1302
private getHover(label: string | undefined, resource: URI | null, node: ITreeItem): string | IManagedHoverTooltipMarkdownString | undefined {
1303
if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) {
1304
if (resource && !node.tooltip) {
1305
return undefined;
1306
} else if (node.tooltip === undefined) {
1307
return label;
1308
} else if (!isString(node.tooltip)) {
1309
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
1310
} else if (node.tooltip !== '') {
1311
return node.tooltip;
1312
} else {
1313
return undefined;
1314
}
1315
}
1316
1317
return {
1318
markdown: typeof node.tooltip === 'string' ? node.tooltip :
1319
(token: CancellationToken): Promise<IMarkdownString | string | undefined> => {
1320
return new Promise<IMarkdownString | string | undefined>((resolve) => {
1321
node.resolve(token).then(() => resolve(node.tooltip));
1322
});
1323
},
1324
markdownNotSupportedFallback: resource ? undefined : (label ?? '') // Passing undefined as the fallback for a resource falls back to the old native hover
1325
};
1326
}
1327
1328
renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
1329
const node = element.element;
1330
const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;
1331
const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined);
1332
const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;
1333
const label = treeItemLabel ? treeItemLabel.label : undefined;
1334
const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {
1335
if (start < 0) {
1336
start = label.length + start;
1337
}
1338
if (end < 0) {
1339
end = label.length + end;
1340
}
1341
if ((start >= label.length) || (end > label.length)) {
1342
return ({ start: 0, end: 0 });
1343
}
1344
if (start > end) {
1345
const swap = start;
1346
start = end;
1347
end = swap;
1348
}
1349
return ({ start, end });
1350
}) : undefined;
1351
const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;
1352
const iconUrl = icon ? URI.revive(icon) : undefined;
1353
const title = this.getHover(label, resource, node);
1354
1355
// reset
1356
templateData.actionBar.clear();
1357
templateData.icon.style.color = '';
1358
1359
let commandEnabled = true;
1360
if (node.command) {
1361
commandEnabled = isTreeCommandEnabled(node.command, this.contextKeyService);
1362
}
1363
1364
this.renderCheckbox(node, templateData);
1365
1366
if (resource) {
1367
const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
1368
const labelResource = resource ? resource : URI.parse('missing:_icon_resource');
1369
templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, {
1370
fileKind: this.getFileKind(node),
1371
title,
1372
hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon),
1373
fileDecorations,
1374
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
1375
matches: matches ? matches : createMatches(element.filterData),
1376
strikethrough: treeItemLabel?.strikethrough,
1377
disabledCommand: !commandEnabled,
1378
labelEscapeNewLines: true,
1379
forceLabel: !!node.label
1380
});
1381
} else {
1382
templateData.resourceLabel.setResource({ name: label, description }, {
1383
title,
1384
hideIcon: true,
1385
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
1386
matches: matches ? matches : createMatches(element.filterData),
1387
strikethrough: treeItemLabel?.strikethrough,
1388
disabledCommand: !commandEnabled,
1389
labelEscapeNewLines: true
1390
});
1391
}
1392
1393
if (iconUrl) {
1394
templateData.icon.className = 'custom-view-tree-node-item-icon';
1395
templateData.icon.style.backgroundImage = cssJs.asCSSUrl(iconUrl);
1396
} else {
1397
let iconClass: string | undefined;
1398
if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {
1399
iconClass = ThemeIcon.asClassName(node.themeIcon);
1400
if (node.themeIcon.color) {
1401
templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';
1402
}
1403
}
1404
templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';
1405
templateData.icon.style.backgroundImage = '';
1406
}
1407
1408
if (!commandEnabled) {
1409
templateData.icon.className = templateData.icon.className + ' disabled';
1410
if (templateData.container.parentElement) {
1411
templateData.container.parentElement.className = templateData.container.parentElement.className + ' disabled';
1412
}
1413
}
1414
1415
templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg;
1416
1417
const menuActions = this.menus.getResourceActions([node]);
1418
templateData.actionBar.push(menuActions, { icon: true, label: false });
1419
1420
if (this._actionRunner) {
1421
templateData.actionBar.actionRunner = this._actionRunner;
1422
}
1423
this.setAlignment(templateData.container, node);
1424
1425
// remember rendered element, an element can be rendered multiple times
1426
const renderedItems = this._renderedElements.get(element.element.handle) ?? [];
1427
this._renderedElements.set(element.element.handle, [...renderedItems, { original: element, rendered: templateData }]);
1428
}
1429
1430
private rerender() {
1431
// As we add items to the map during this call we can't directly use the map in the for loop
1432
// but have to create a copy of the keys first
1433
const keys = new Set(this._renderedElements.keys());
1434
for (const key of keys) {
1435
const values = this._renderedElements.get(key) ?? [];
1436
for (const value of values) {
1437
this.disposeElement(value.original, 0, value.rendered);
1438
this.renderElement(value.original, 0, value.rendered);
1439
}
1440
}
1441
}
1442
1443
private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData) {
1444
if (node.checkbox) {
1445
// The first time we find a checkbox we want to rerender the visible tree to adapt the alignment
1446
if (!this._hasCheckbox) {
1447
this._hasCheckbox = true;
1448
this.rerender();
1449
}
1450
if (!templateData.checkbox) {
1451
const checkbox = new TreeItemCheckbox(templateData.checkboxContainer, this.checkboxStateHandler, this._hoverDelegate, this.hoverService);
1452
templateData.checkbox = checkbox;
1453
}
1454
templateData.checkbox.render(node);
1455
} else if (templateData.checkbox) {
1456
templateData.checkbox.dispose();
1457
templateData.checkbox = undefined;
1458
}
1459
}
1460
1461
private setAlignment(container: HTMLElement, treeItem: ITreeItem) {
1462
container.parentElement!.classList.toggle('align-icon-with-twisty', !this._hasCheckbox && this.aligner.alignIconWithTwisty(treeItem));
1463
}
1464
1465
private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean {
1466
// We always hide the resource label in favor of the iconUrl when it's provided.
1467
// When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon.
1468
return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon)));
1469
}
1470
1471
private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon {
1472
if (!icon) {
1473
return false;
1474
}
1475
1476
// If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the
1477
// icon theme and should use whatever the icon theme has provided.
1478
return !(hasResource && this.isFileKindThemeIcon(icon));
1479
}
1480
1481
private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean {
1482
return icon?.id === FolderThemeIcon.id;
1483
}
1484
1485
private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {
1486
if (icon) {
1487
return icon.id === FileThemeIcon.id || this.isFolderThemeIcon(icon);
1488
} else {
1489
return false;
1490
}
1491
}
1492
1493
private getFileKind(node: ITreeItem): FileKind {
1494
if (node.themeIcon) {
1495
switch (node.themeIcon.id) {
1496
case FileThemeIcon.id:
1497
return FileKind.FILE;
1498
case FolderThemeIcon.id:
1499
return FileKind.FOLDER;
1500
}
1501
}
1502
return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE;
1503
}
1504
1505
private onDidChangeContext(e: IContextKeyChangeEvent) {
1506
const affectsEntireMenuContexts = e.affectsSome(this.menus.getEntireMenuContexts());
1507
1508
const items: ITreeItem[] = [];
1509
for (const [_, elements] of this._renderedElements) {
1510
for (const element of elements) {
1511
if (affectsEntireMenuContexts || e.affectsSome(this.menus.getElementOverlayContexts(element.original.element))) {
1512
items.push(element.original.element);
1513
}
1514
}
1515
}
1516
if (items.length) {
1517
this._onDidChangeMenuContext.fire(items);
1518
}
1519
}
1520
1521
private updateCheckboxes(items: ITreeItem[]) {
1522
let allItems: ITreeItem[] = [];
1523
1524
if (!this.manuallyManageCheckboxes()) {
1525
allItems = setCascadingCheckboxUpdates(items);
1526
} else {
1527
allItems = items;
1528
}
1529
1530
allItems.forEach(item => {
1531
const renderedItems = this._renderedElements.get(item.handle);
1532
if (renderedItems) {
1533
renderedItems.forEach(renderedItems => renderedItems.rendered.checkbox?.render(item));
1534
}
1535
});
1536
this._onDidChangeCheckboxState.fire(allItems);
1537
}
1538
1539
disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
1540
const itemRenders = this._renderedElements.get(resource.element.handle) ?? [];
1541
const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered);
1542
1543
if (itemRenders.length === 1) {
1544
this._renderedElements.delete(resource.element.handle);
1545
} else if (itemRenders.length > 0) {
1546
itemRenders.splice(renderedIndex, 1);
1547
}
1548
1549
templateData.checkbox?.dispose();
1550
templateData.checkbox = undefined;
1551
}
1552
1553
disposeTemplate(templateData: ITreeExplorerTemplateData): void {
1554
templateData.resourceLabel.dispose();
1555
templateData.actionBar.dispose();
1556
}
1557
}
1558
1559
class Aligner extends Disposable {
1560
private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;
1561
1562
constructor(private themeService: IThemeService) {
1563
super();
1564
}
1565
1566
set tree(tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>) {
1567
this._tree = tree;
1568
}
1569
1570
public alignIconWithTwisty(treeItem: ITreeItem): boolean {
1571
if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) {
1572
return false;
1573
}
1574
if (!this.hasIcon(treeItem)) {
1575
return false;
1576
}
1577
1578
if (this._tree) {
1579
const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput();
1580
if (this.hasIcon(parent)) {
1581
return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c));
1582
}
1583
return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c));
1584
} else {
1585
return false;
1586
}
1587
}
1588
1589
private hasIcon(node: ITreeItem): boolean {
1590
const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;
1591
if (icon) {
1592
return true;
1593
}
1594
if (node.resourceUri || node.themeIcon) {
1595
const fileIconTheme = this.themeService.getFileIconTheme();
1596
const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None;
1597
if (isFolder) {
1598
return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons;
1599
}
1600
return fileIconTheme.hasFileIcons;
1601
}
1602
return false;
1603
}
1604
}
1605
1606
class MultipleSelectionActionRunner extends ActionRunner {
1607
1608
constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) {
1609
super();
1610
this._register(this.onDidRun(e => {
1611
if (e.error && !isCancellationError(e.error)) {
1612
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));
1613
}
1614
}));
1615
}
1616
1617
protected override async runAction(action: IAction, context: TreeViewItemHandleArg | TreeViewPaneHandleArg): Promise<void> {
1618
const selection = this.getSelectedResources();
1619
let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;
1620
let actionInSelected: boolean = false;
1621
if (selection.length > 1) {
1622
selectionHandleArgs = selection.map(selected => {
1623
if ((selected.handle === (context as TreeViewItemHandleArg).$treeItemHandle) || (context as TreeViewPaneHandleArg).$selectedTreeItems) {
1624
actionInSelected = true;
1625
}
1626
return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle };
1627
});
1628
}
1629
1630
if (!actionInSelected && selectionHandleArgs) {
1631
selectionHandleArgs = undefined;
1632
}
1633
1634
await action.run(context, selectionHandleArgs);
1635
}
1636
}
1637
1638
class TreeMenus implements IDisposable {
1639
private contextKeyService: IContextKeyService | undefined;
1640
private _onDidChange = new Emitter<ITreeItem>();
1641
public readonly onDidChange = this._onDidChange.event;
1642
1643
constructor(
1644
private id: string,
1645
@IMenuService private readonly menuService: IMenuService
1646
) { }
1647
1648
/**
1649
* Gets only the actions that apply to all of the given elements.
1650
*/
1651
getResourceActions(elements: ITreeItem[]): IAction[] {
1652
const actions = this.getActions(this.getMenuId(), elements);
1653
return actions.primary;
1654
}
1655
1656
/**
1657
* Gets only the actions that apply to all of the given elements.
1658
*/
1659
getResourceContextActions(elements: ITreeItem[]): IAction[] {
1660
return this.getActions(this.getMenuId(), elements).secondary;
1661
}
1662
1663
public setContextKeyService(service: IContextKeyService) {
1664
this.contextKeyService = service;
1665
}
1666
1667
private filterNonUniversalActions(groups: Map<string, IAction>[], newActions: IAction[]) {
1668
const newActionsSet: Set<string> = new Set(newActions.map(a => a.id));
1669
for (const group of groups) {
1670
const actions = group.keys();
1671
for (const action of actions) {
1672
if (!newActionsSet.has(action)) {
1673
group.delete(action);
1674
}
1675
}
1676
}
1677
}
1678
1679
private buildMenu(groups: Map<string, IAction>[]): IAction[] {
1680
const result: IAction[] = [];
1681
for (const group of groups) {
1682
if (group.size > 0) {
1683
if (result.length) {
1684
result.push(new Separator());
1685
}
1686
result.push(...group.values());
1687
}
1688
}
1689
return result;
1690
}
1691
1692
private createGroups(actions: IAction[]): Map<string, IAction>[] {
1693
const groups: Map<string, IAction>[] = [];
1694
let group: Map<string, IAction> = new Map();
1695
for (const action of actions) {
1696
if (action instanceof Separator) {
1697
groups.push(group);
1698
group = new Map();
1699
} else {
1700
group.set(action.id, action);
1701
}
1702
}
1703
groups.push(group);
1704
return groups;
1705
}
1706
1707
public getElementOverlayContexts(element: ITreeItem): Map<string, any> {
1708
return new Map([
1709
['view', this.id],
1710
['viewItem', element.contextValue]
1711
]);
1712
}
1713
1714
public getEntireMenuContexts(): ReadonlySet<string> {
1715
return this.menuService.getMenuContexts(this.getMenuId());
1716
}
1717
1718
public getMenuId(): MenuId {
1719
return MenuId.ViewItemContext;
1720
}
1721
1722
private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } {
1723
if (!this.contextKeyService) {
1724
return { primary: [], secondary: [] };
1725
}
1726
1727
let primaryGroups: Map<string, IAction>[] = [];
1728
let secondaryGroups: Map<string, IAction>[] = [];
1729
for (let i = 0; i < elements.length; i++) {
1730
const element = elements[i];
1731
const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element));
1732
1733
const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true });
1734
1735
const result = getContextMenuActions(menuData, 'inline');
1736
if (i === 0) {
1737
primaryGroups = this.createGroups(result.primary);
1738
secondaryGroups = this.createGroups(result.secondary);
1739
} else {
1740
this.filterNonUniversalActions(primaryGroups, result.primary);
1741
this.filterNonUniversalActions(secondaryGroups, result.secondary);
1742
}
1743
}
1744
1745
return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) };
1746
}
1747
1748
dispose() {
1749
this.contextKeyService = undefined;
1750
}
1751
}
1752
1753
export class CustomTreeView extends AbstractTreeView {
1754
1755
constructor(
1756
id: string,
1757
title: string,
1758
private readonly extensionId: string,
1759
@IThemeService themeService: IThemeService,
1760
@IInstantiationService instantiationService: IInstantiationService,
1761
@ICommandService commandService: ICommandService,
1762
@IConfigurationService configurationService: IConfigurationService,
1763
@IProgressService progressService: IProgressService,
1764
@IContextMenuService contextMenuService: IContextMenuService,
1765
@IKeybindingService keybindingService: IKeybindingService,
1766
@INotificationService notificationService: INotificationService,
1767
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1768
@IContextKeyService contextKeyService: IContextKeyService,
1769
@IHoverService hoverService: IHoverService,
1770
@IExtensionService private readonly extensionService: IExtensionService,
1771
@IActivityService activityService: IActivityService,
1772
@ITelemetryService private readonly telemetryService: ITelemetryService,
1773
@ILogService logService: ILogService,
1774
@IOpenerService openerService: IOpenerService
1775
) {
1776
super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService, openerService);
1777
}
1778
1779
protected activate() {
1780
if (!this.activated) {
1781
type ExtensionViewTelemetry = {
1782
extensionId: TelemetryTrustedValue<string>;
1783
id: string;
1784
};
1785
type ExtensionViewTelemetryMeta = {
1786
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' };
1787
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' };
1788
owner: 'digitarald';
1789
comment: 'Helps to gain insights on what extension contributed views are most popular';
1790
};
1791
this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', {
1792
extensionId: new TelemetryTrustedValue(this.extensionId),
1793
id: this.id,
1794
});
1795
this.createTree();
1796
this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
1797
.then(() => timeout(2000))
1798
.then(() => {
1799
this.updateMessage();
1800
});
1801
this.activated = true;
1802
}
1803
}
1804
}
1805
1806
export class TreeView extends AbstractTreeView {
1807
1808
protected activate() {
1809
if (!this.activated) {
1810
this.createTree();
1811
this.activated = true;
1812
}
1813
}
1814
}
1815
1816
interface TreeDragSourceInfo {
1817
id: string;
1818
itemHandles: string[];
1819
}
1820
1821
export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {
1822
private readonly treeMimeType: string;
1823
private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();
1824
private dragCancellationToken: CancellationTokenSource | undefined;
1825
1826
constructor(
1827
private readonly treeId: string,
1828
@ILabelService private readonly labelService: ILabelService,
1829
@IInstantiationService private readonly instantiationService: IInstantiationService,
1830
@ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService,
1831
@ILogService private readonly logService: ILogService) {
1832
this.treeMimeType = `application/vnd.code.tree.${treeId.toLowerCase()}`;
1833
}
1834
1835
private dndController: ITreeViewDragAndDropController | undefined;
1836
set controller(controller: ITreeViewDragAndDropController | undefined) {
1837
this.dndController = controller;
1838
}
1839
1840
private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<VSDataTransfer | undefined> {
1841
return dndController.handleDrag(itemHandles, uuid, dragCancellationToken).then(additionalDataTransfer => {
1842
if (additionalDataTransfer) {
1843
const unlistedTypes: string[] = [];
1844
for (const item of additionalDataTransfer) {
1845
if ((item[0] !== this.treeMimeType) && (dndController.dragMimeTypes.findIndex(value => value === item[0]) < 0)) {
1846
unlistedTypes.push(item[0]);
1847
}
1848
}
1849
if (unlistedTypes.length) {
1850
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(', ')}`);
1851
}
1852
}
1853
return additionalDataTransfer;
1854
});
1855
}
1856
1857
private addExtensionProvidedTransferTypes(originalEvent: DragEvent, itemHandles: string[]) {
1858
if (!originalEvent.dataTransfer || !this.dndController) {
1859
return;
1860
}
1861
const uuid = generateUuid();
1862
1863
this.dragCancellationToken = new CancellationTokenSource();
1864
this.treeViewsDragAndDropService.addDragOperationTransfer(uuid, this.handleDragAndLog(this.dndController, itemHandles, uuid, this.dragCancellationToken.token));
1865
this.treeItemsTransfer.setData([new DraggedTreeItemsIdentifier(uuid)], DraggedTreeItemsIdentifier.prototype);
1866
originalEvent.dataTransfer.clearData(Mimes.text);
1867
if (this.dndController.dragMimeTypes.find((element) => element === Mimes.uriList)) {
1868
// Add the type that the editor knows
1869
originalEvent.dataTransfer?.setData(DataTransfers.RESOURCES, '');
1870
}
1871
this.dndController.dragMimeTypes.forEach(supportedType => {
1872
originalEvent.dataTransfer?.setData(supportedType, '');
1873
});
1874
}
1875
1876
private addResourceInfoToTransfer(originalEvent: DragEvent, resources: URI[]) {
1877
if (resources.length && originalEvent.dataTransfer) {
1878
// Apply some datatransfer types to allow for dragging the element outside of the application
1879
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
1880
1881
// The only custom data transfer we set from the explorer is a file transfer
1882
// to be able to DND between multiple code file explorers across windows
1883
const fileResources = resources.filter(s => s.scheme === Schemas.file).map(r => r.fsPath);
1884
if (fileResources.length) {
1885
originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
1886
}
1887
}
1888
}
1889
1890
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
1891
if (originalEvent.dataTransfer) {
1892
const treeItemsData = (data as ElementsDragAndDropData<ITreeItem, ITreeItem[]>).getData();
1893
const resources: URI[] = [];
1894
const sourceInfo: TreeDragSourceInfo = {
1895
id: this.treeId,
1896
itemHandles: []
1897
};
1898
treeItemsData.forEach(item => {
1899
sourceInfo.itemHandles.push(item.handle);
1900
if (item.resourceUri) {
1901
resources.push(URI.revive(item.resourceUri));
1902
}
1903
});
1904
this.addResourceInfoToTransfer(originalEvent, resources);
1905
this.addExtensionProvidedTransferTypes(originalEvent, sourceInfo.itemHandles);
1906
originalEvent.dataTransfer.setData(this.treeMimeType,
1907
JSON.stringify(sourceInfo));
1908
}
1909
}
1910
1911
private debugLog(types: Set<string>) {
1912
if (types.size) {
1913
this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`);
1914
} else {
1915
this.logService.debug(`TreeView dragged with no supported mime types.`);
1916
}
1917
}
1918
1919
onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
1920
const dataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer!);
1921
1922
const types = new Set<string>(Array.from(dataTransfer, x => x[0]));
1923
1924
if (originalEvent.dataTransfer) {
1925
// Also add uri-list if we have any files. At this stage we can't actually access the file itself though.
1926
for (const item of originalEvent.dataTransfer.items) {
1927
if (item.kind === 'file' || item.type === DataTransfers.RESOURCES.toLowerCase()) {
1928
types.add(Mimes.uriList);
1929
break;
1930
}
1931
}
1932
}
1933
1934
this.debugLog(types);
1935
1936
const dndController = this.dndController;
1937
if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) {
1938
return false;
1939
}
1940
const dragContainersSupportedType = Array.from(types).some((value, index) => {
1941
if (value === this.treeMimeType) {
1942
return true;
1943
} else {
1944
return dndController.dropMimeTypes.indexOf(value) >= 0;
1945
}
1946
});
1947
if (dragContainersSupportedType) {
1948
return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true };
1949
}
1950
return false;
1951
}
1952
1953
getDragURI(element: ITreeItem): string | null {
1954
if (!this.dndController) {
1955
return null;
1956
}
1957
return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle;
1958
}
1959
1960
getDragLabel?(elements: ITreeItem[]): string | undefined {
1961
if (!this.dndController) {
1962
return undefined;
1963
}
1964
if (elements.length > 1) {
1965
return String(elements.length);
1966
}
1967
const element = elements[0];
1968
return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined);
1969
}
1970
1971
async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): Promise<void> {
1972
const dndController = this.dndController;
1973
if (!originalEvent.dataTransfer || !dndController) {
1974
return;
1975
}
1976
1977
let treeSourceInfo: TreeDragSourceInfo | undefined;
1978
let willDropUuid: string | undefined;
1979
if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {
1980
willDropUuid = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype)![0].identifier;
1981
}
1982
1983
const originalDataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer, true);
1984
1985
const outDataTransfer = new VSDataTransfer();
1986
for (const [type, item] of originalDataTransfer) {
1987
if (type === this.treeMimeType || dndController.dropMimeTypes.includes(type) || (item.asFile() && dndController.dropMimeTypes.includes(DataTransfers.FILES.toLowerCase()))) {
1988
outDataTransfer.append(type, item);
1989
if (type === this.treeMimeType) {
1990
try {
1991
treeSourceInfo = JSON.parse(await item.asString());
1992
} catch {
1993
// noop
1994
}
1995
}
1996
}
1997
}
1998
1999
const additionalDataTransfer = await this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid);
2000
if (additionalDataTransfer) {
2001
for (const [type, item] of additionalDataTransfer) {
2002
outDataTransfer.append(type, item);
2003
}
2004
}
2005
return dndController.handleDrop(outDataTransfer, targetNode, CancellationToken.None, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles);
2006
}
2007
2008
onDragEnd(originalEvent: DragEvent): void {
2009
// Check if the drag was cancelled.
2010
if (originalEvent.dataTransfer?.dropEffect === 'none') {
2011
this.dragCancellationToken?.cancel();
2012
}
2013
}
2014
2015
dispose(): void { }
2016
}
2017
2018
function setCascadingCheckboxUpdates(items: readonly ITreeItem[]) {
2019
const additionalItems: ITreeItem[] = [];
2020
2021
for (const item of items) {
2022
if (item.checkbox !== undefined) {
2023
2024
const checkChildren = (currentItem: ITreeItem) => {
2025
for (const child of (currentItem.children ?? [])) {
2026
if ((child.checkbox !== undefined) && (currentItem.checkbox !== undefined) && (child.checkbox.isChecked !== currentItem.checkbox.isChecked)) {
2027
child.checkbox.isChecked = currentItem.checkbox.isChecked;
2028
additionalItems.push(child);
2029
checkChildren(child);
2030
}
2031
}
2032
};
2033
checkChildren(item);
2034
2035
const visitedParents: Set<ITreeItem> = new Set();
2036
const checkParents = (currentItem: ITreeItem) => {
2037
if (currentItem.parent && (currentItem.parent.checkbox !== undefined) && currentItem.parent.children) {
2038
if (visitedParents.has(currentItem.parent)) {
2039
return;
2040
} else {
2041
visitedParents.add(currentItem.parent);
2042
}
2043
2044
let someUnchecked = false;
2045
let someChecked = false;
2046
for (const child of currentItem.parent.children) {
2047
if (someUnchecked && someChecked) {
2048
break;
2049
}
2050
if (child.checkbox !== undefined) {
2051
if (child.checkbox.isChecked) {
2052
someChecked = true;
2053
} else {
2054
someUnchecked = true;
2055
}
2056
}
2057
}
2058
if (someChecked && !someUnchecked && (currentItem.parent.checkbox.isChecked !== true)) {
2059
currentItem.parent.checkbox.isChecked = true;
2060
additionalItems.push(currentItem.parent);
2061
checkParents(currentItem.parent);
2062
} else if (someUnchecked && (currentItem.parent.checkbox.isChecked !== false)) {
2063
currentItem.parent.checkbox.isChecked = false;
2064
additionalItems.push(currentItem.parent);
2065
checkParents(currentItem.parent);
2066
}
2067
}
2068
};
2069
checkParents(item);
2070
}
2071
}
2072
2073
return items.concat(additionalItems);
2074
}
2075
2076